How to know when a user was last active? - react-admin

Trying to build an auto-logout feature, and have got a timer going but looking for a way to start/restart the timer based on user's last active time.
The scenario is that the user has been away from their keyboard for X amount of time, and we want to log them out.
Adding the code I've come up with since then:
// main.js
import React from 'react'
import { Admin } from 'react-admin'
import authProvider from '../../auth-provider'
import restClient from '../../rest-client'
import buildResources from './build-resources'
import { setLastActive } from './last-active'
const customReducers = {
lastActive,
}
function lastActive() {
const now = Date.now()
setLastActive(now)
return now
}
export const Main = () => (
<Admin
authProvider={authProvider}
dataProvider={restClient}
customReducers={customReducers}
>
{buildResources}
</Admin>
)
// last-active.js
let lastActive
export const setLastActive = (timestamp = Date.now()) =>
(lastActive = timestamp)
export const getLastActive = () => lastActive || null
It's not all the code, but enough to show the gist. I can know use the redux store, or the singleton to grab when they were last active. It's a bit flawed since most, not all actions go through the redux store.

react-admin works well with API-structured Apps.
Based on that fact, am assuming your App consumes endpoints (a lot!!).
Here's a possible process to auto-logout a user.
Set the token in authProvider.login method
Within your authprovider.checkError method, use setTimeout method to clear the token. This timeout can be based on last CRUD method executed.
Once the token is cleared, react-admin redirects the user to logout.Thus requiring the user to re-login.

Related

Expo React Native - How come returning null on App.js initialization function is good behaviour?

This is mainly a comprehension problem:
Considering the following expo docs, which aren't explaining what is going on under the hood,
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
SplashScreen.preventAutoHideAsync();
export default function App() {
const [fontsLoaded] = useFonts({
'Inter-Black': require('./assets/fonts/Inter-Black.otf'),
});
const onLayoutRootView = useCallback(async () => {
if (fontsLoaded) {
await SplashScreen.hideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) {
return null;
}
return (
// Your app tree there.
);
}
What is going on here from my understanding is that you prevent the splashscreen from going away while you retrieve certain assets, like fonts.
When you do have your assets, you make the splashscreen go away and you launch your app by returning your components' tree.
The part I don't understand is why is it ok to return null if the fonts don't get loaded?
Since it is the initialization function, my mind want to think that if you return null to it, your app doesn't start and the user will be left wondering why...
Is there an explanation, a logic behind the hood actually refreshing something to recheck until it is ok and the assets are indeed loaded or something I don't know?
I know I have the case of my app not starting correctly every time right now and I'm wondering if this "return null" on the App.js function could be the culprit.

React Native (Expo) - Ability to update state of top component from any (nested) child component

I am working on an Expo app, that uses authentication with JWTs that are stored using SecureStore. The top-level component initially displays a Login screen but also checks if a JWT exists in SecureStore. If a JWT exists the app verifies that it is still valid and if it is the app takes the user to the Landing page, from which the user can navigate to many other pages that fetch and display all sorts of data.
I am looking for a way to handle an expired JWT, so that if the user is navigating to a page that tries to fetch some data and the API response returns e.g. 401 the app should take the user back to the login screen.
The top component uses this state to decide what page to show
const [appState, setAppState] = useState(appStates.STARTUP_SETUP);
with valid values for appState being:
const appStates = {
STARTUP_SETUP: "StartupSetup", // Initial state, during which the app checks for an existing valid JWT
SHOW_LOGIN_SCREEN: "ShowLoginScreen", // There is no stored JWT, app shows login screen
SHOW_SIGNUP_SCREEN: "ShowSignupScreen", // There is no stored JWT, app shows signup screen
SHOW_SIGNUP_CONFIRMATION_SCREEN: "ShowSignupConfirmationScreen", // There is no stored JWT, user just registered and is prompted to check their email for verification
USER_LOGGED_IN: "UserLoggedIn", // user logged in, JWT is stored
}
The component uses appState in the following way:
if (appState === appStates.USER_LOGGED_IN) {
comp = <Landing onLogout={logUserOut} />;
} else if (appState === appStates.SHOW_LOGIN_SCREEN) {
comp = <Login onSuccessfulLogin={updateUser} onSignup={() => setAppState(appStates.SHOW_SIGNUP_SCREEN)} />;
} else if (appState === appStates.SHOW_SIGNUP_SCREEN) {
comp = <Signup onSuccessfulSignup={() => setAppState(appStates.SHOW_SIGNUP_CONFIRMATION_SCREEN)} onLogin={() => setAppState(appStates.SHOW_LOGIN_SCREEN)} />;
} else if (appState === appStates.SHOW_SIGNUP_CONFIRMATION_SCREEN) {
comp = <SignupConfirmation onLogin={() => setAppState(appStates.SHOW_LOGIN_SCREEN)} />
}
With Landing having its own tree of child components.
I am basically looking for a way to be able to do
setAppState(appStates.SHOW_LOGIN_SCREEN)
from anywhere in my app.
One possibility would be to pass that hook from the top component to Landing and every child that has but I feel there should be an easier way.
Edit - Solution
In my top component I created a method that deletes the token and sets the appState to SHOW_LOGIN_SCREEN
const deleteStoredToken = () => {
deleteToken();
setAppState(appStates.SHOW_LOGIN_SCREEN);
}
const appStateValue = { deleteStoredToken };
I then created a context (with a default value)
export const AppContext = React.createContext({
deleteStoredToken: () => { }
});
I used this context to wrap the children of the top component by providing appStateValue as its value
return (
...
<AppContext.Provider value={appStateValue}>
{children}
</AppContext.Provider>
...
)
And now in any child component I can do
const { deleteStoredToken } = useContext(AppContext);
and use deleteStoredToken()
It sounds like you're looking for a state management solution. A React Context is a low-effort solution to this - be sure to read the caveats in the documentation though.
There are tens if not hundreds of libraries that offer different ways to do this. The most popular are probably Redux and Mobx; these offer dedicated ways to share complex state between components. However, if it's only for one value, and it won't be updated frequently, a Context is perfectly fine.

How to stop Vue.js 3 watch() API triggering on exit

I have implemented a watch within a Vue component that displays product information. The watch watches the route object of vue-router for a ProductID param to change. When it changes, I want to go get the product details from the back-end API.
To watch the route, I do this in Product.vue:
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute();
async function getProduct(ProductID) {
await axios.get(`/api/product/${ProductID}`).then(..do something here)
}
// fetch the product information when params change
watch(() => route.params.ProductID, async (newID, oldID) => {
await getProduct(newId)
},
//watch options
{
deep: true,
immediate: true
}
)
},
}
The above code works, except that if a user navigates away from Product.vue, for example using the back button to go back to the homepage, the watch is triggered again and tries to make a call to the API using undefined as the ProductID (becaues ProductID param does not exist on the homepage route) e.g. http://localhost:8080/api/product/undefined. This causes an error to be thrown in the app.
Why does the watch trigger when a user has navigated away from Product.vue?
How can this be prevented properly? I can do it using if(newID) { await getProduct(newId) } but it seems counterintuitive to what the watch should be doing anyway.
UPDATE & SOLUTION
Place the following at the top replacing the name for whatever your route is called:
if (route.name !== "YourRouteName") {
return;
}
That will ensure nothing happens if you are not on the route you want to watch.
I ran into the same problem. Instead of watching the current route, use vue-router onBeforeRouteUpdate, which only gets called if the route changed and the same component is reused.
From https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
export default {
setup() {
// same as beforeRouteLeave option with no access to `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// cancel the navigation and stay on the same page
if (!answer) return false
})
const userData = ref()
// same as beforeRouteUpdate option with no access to `this`
onBeforeRouteUpdate(async (to, from) => {
// only fetch the user if the id changed as maybe only the query or the hash changed
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
},
}
watch registers the watcher inside an vue-internal, but component-independent object. I think it's a Map. So destroying the component has no effect on the reactivity system.
Just ignore the case where newID is undefined, like you already did. But to prevent wrapping your code in a big if block just use if(newID === undefined)return; at the beginning of your callback. If your ids are always truthy (0 and "" are invalid ids) you can even use if(!newID)return;.
well, in your use case the best approach would be to have a method or function which makes the api call to the server, having watch is not a really good use of it, because it will trigger whenever route changes and you do not want that to happen, what you want is simply get the productID from route and make the api call,
so it can be done with getting the productID in the created or mounted and make the api call!

How to tell if Vue-Router navigation hook was triggered by "push"?

I need to perform a certain action whenever a call to router.push is triggered.
I'm trying to use Vue-Router's navigation guards for this, but there is no way to tell, inside of its callback, what method (go/push/back/replace) triggered it.
router.beforeResolve((to, from) => {
// Do something only if this was triggered by "push"
});
Just hook vue-router push/back/replace function before Vue.use(vue-router),
then get navigation type by call
this.$router.customNaviType;
import Vue from 'vue'
import Router from 'vue-router'
const routerPush = Router.prototype.push;
// rewrite push
Router.prototype.push = function push(location) {
this.customNaviType = "push";
return routerPush.call(this, location).catch(error => error)
}
const routerBack = Router.prototype.back;
// rewrite back
Router.prototype.back = function back() {
this.customNaviType = "back";
return routerBack.call(this);
}
const routerReplace = Router.prototype.replace;
// rewrite replace
Router.prototype.replace = function replace(location) {
this.customNaviType = "replace";
return routerReplace.call(this, location).catch(error => error)
}
Vue.use(Router)
https://router.vuejs.org/guide/essentials/navigation.html
By my understanding, only push method will increase the history.length by one.
Maybe use a global variable to keep check window.history.length. If it increase, then you can trigger your push event.
For example:
router.beforeResolve((to, from) => {
if (window.history.length > window.last_history_length) {
trigger_your_push_event();
}
window.last_history_length = window.history.length;
}
});
It can not be catched by navigation guards directly:
But,
using the state of your app you can have a better idea which operation is done. You can do it by saving the current and your last route to your state using the guard afterEach. Once you know them, you can compare them with from and to parameters in e.g. beforeEach to get know whether it is a push or back. You can probably add more metadata to your routes so you get know more about your routes to decide whether it is replaced by some kind of other type route:

Why can't I access state in ComponentDidMount?

I have this code for React Native:
componentWillMount() {
shiftKeys = []; // need to clear previously set data or we will get dupicate array key errors
// get the current user from firebase
const userData = firebaseApp.auth().currentUser;
const profileRef = firebaseApp.database().ref('userdata').child(userData.uid);
profileRef.on('value', snapshot => {
if (snapshot.val().hasOwnProperty('licenseType') && snapshot.val().hasOwnProperty('licenseState') && snapshot.val().hasOwnProperty('distance')) {
this.setState({
licenseType: snapshot.val().licenseType,
licenseState: snapshot.val().licenseState,
distancePref: snapshot.val().distance,
});
console.log('State1', this.state.distancePref)
} else {
// redirect back to profile screens because we need three values above to search.
this.props.navigation.navigate('Onboarding1')
}
});
}
componentDidMount() {
console.log('State2', this.state.distancePref)
var geoQuery = geoFire.query({
center: [45.616422, -122.580453],
radius: 1000// need to set dynamically
});
I think this is some kind of scope issue?
When I look at the console log, State 1 is set correctly, but State 2 prints nothing.
In my app I need to look up a users distance preference, then use that to run a query.
How do I pass the value from componentWillMount to componentDidMount?
https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle
setState in componentWillMount - bad way. You do not solve the problem this way, because state will not be updated until componentDidMount (see lifecycle). Check your condition when creating the state, in the constructor.
Or you can solve the problem using redux.
The root issue with this problem had to do with my not understanding how react and react native render the code.
I was trying to get users info from firebase, then set preferences, then use those preferences to run a search.
I added redux and thunk to handle the getting and saving of the users preferences separately from (and before) the user has a chance to search.