react native setState inside an async asyncStorage function - react-native

I am using the expo-auth-session package to make a request to the Spotify API to get access tokens, then saving to AsyncStorage.
A save function that stores the token in AsyncStorage:
const save = async (token) => {
try{
AsyncStorage.setItem('access_token', token)
}
catch(error){
console.log(error)
}
}
A getItem function that gets the access token value from AsyncStorage, and sets that value to the spotifyAccessToken state
const [spotifyAccessToken, setSpotifyAccessToken] = useState('');
const getItem = async () => {
try{
const token = await AsyncStorage.getItem('access_token')
setSpotifyAccessToken(token);
}
catch(error){
console.log(error)
}
}
Using the useAuthRequest from expo-auth-session to make a request to Spotify API, the request code below works.
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: "https://accounts.spotify.com/api/token"
};
const [request, response, promptAsync] = useAuthRequest({
// responseType: ResponseType.Token,
responseType: 'code',
clientId: client_id,
//clientSecret: client_secret,
scopes: ['user-read-recently-played'],
usePKCE: false,
redirectUri: REDIRECT_URI
}, discovery)
useEffect(() => {
if (response?.type === 'success'){
//console.log(response.params.code);
axios.request({
method: 'POST',
url: 'https://accounts.spotify.com/api/token',
headers: {
'content-type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${new Buffer.from(`${client_id}:${client_secret}`).toString('base64')}`,
},
data: {
grant_type: 'authorization_code',
code: response.params.code,
redirect_uri: REDIRECT_URI
}
}).then(res => {
save(res.data.access_token);
}).catch(err => {console.log(err)})
}
},
[response]);
A button that triggers the user to login using Spotify account, after authenticating, it redirects back to this component screen, however, I want the text below the button to be displayed from "Loading..." to the spotifyAccessToken immediately after it redirects to the component screen, but it wouldn't. After I re-run my application, the token is displayed, which means it was successfully stored in AsyncStorage, but didn't update the state immediately. How can solve this? Thanks.
const [spotifyAccessToken, setSpotifyAccessToken] = useState(null);
useEffect(()=>{
//clearTokens();
// console.log('storage: ' + getValueForfor('access_token'))
// console.log('state: ' + spotifyAccessToken)
getItem()
}, [spotifyAccessToken])
<Button title='login to spotify' onPress={() => promptAsync()}/>
{spotifyAccessToken != '' ? <Text> {spotifyAccessToken} </Text> : <Text> Loading... </Text>}

This might be happening if you are redirecting to the component with getItem too early: before the AsyncStorage is done saving the token. Due to this, at the initial render of the component(with getItem), AsyncStorage.getItem might be getting the old value of access_token and not the updated one.
To possibly fix this issue, try redirecting to the next component only after AsyncStorage.setItem promise is resolved completely. Something like this:
This is how your save function should look like: it should return a Promise value:
const save = async (token) => {
try{
await AsyncStorage.setItem('access_token', token)
}
catch(error){
console.log(error)
}
}
And redirect to the next component after the save return promise value is resolved:
...
).then(async (res) => {
await save(res.data.access_token);
// Redirect here, after save is resolved
})...
Answering the question you asked in the comments to this answer:
it's not working still, you said that the save function should return a promise value, where in the code should I put it
Using await for a Promise makes the function wait till the promise is resolved (here when setItem is done). You do not need to explicitly return a Promise value from the async function in this case. If you do not use await, the function will return prematurely (without waiting for the setItem promise). The setItem promise will still resolve concurrently just that your code wouldn't be able to know when it is resolved.
By using await for setItem here, you just propagate promise resolution to the calling function(here in the then(res => {...}) block).
In the then(res => {}) block you can either use await to wait for the save to complete before executing the next statement. Or use then/catch and add the next statement to execute after save is done in the then block.
Edit: As OP mentioned in the comments below, the redirection to the next component is done automatically. Well, in this case, setting the value in AsyncStorage and immediately getting it in the next component might not work as expected because of the above-mentioned reason.
First, you will need to check if the auto-redirection to the next component is really done after the axios request completes or before it, i.e. as soon as response?.type === 'success'. I am unable to understand why you have made the axios request after you already got success from auth request
If the redirection is happening before the axios request call then you might be able to access the token in the success condition itself:
if (response?.type === 'success'){
// Check if the token is available here?
console.debug(`Response = ${JSON.stringify(response)}`);
// If token is available here itself, then why is the axios request required?
// Save the token here itself...
// Use SessionStorage if required, implementation explained below in the answer
...
}
If you confirmed the above and the auto-redirection is really done after the axios request and NOT after response?.type === 'success' then:
You could use react-native-session-storage as volatile storage to set and get the token in the same session and use AsyncStorage in parallel to it to set and get the token in/from persistent memory.
So, the save function will look like this with SessionStorage:
import SessionStorage from 'react-native-session-storage';
...
const save = async (token) => {
try{
// Set token in SessionStorage as well to allow access to the value immediately
SessionStorage.setItem(`access_token`, token);
// Store token to AsyncStorage to persist it when the app closes.
await AsyncStorage.setItem('access_token', token);
}
catch(error){
console.log(error)
}
}
And getItem function will look like this:
import SessionStorage from 'react-native-session-storage';
...
const getItem = async () => {
try{
let token = await AsyncStorage.getItem('access_token');
// If the token is not yet set in Async Storage, fetch it from Session Storage
// If it's set in Async Storage, use that value
if(!token) // If it's null
token = SessionStorage.getItem('access_token');
setSpotifyAccessToken(token);
// Don't forget to clear both SessionStorage and AsyncStorage on logout!
}
catch(error){
console.log(error)
}
}
Why both storages?
AsyncStorage
-> to persist the token when the user re-opens the app.
SessionStorage
-> as an immediate way to R/W the value during the same session (gets cleared when the app closes).
Another solution:
Use ContextProvider, if your code structure allows it. Wrap the context over the next component to "listen" to token value state change from anywhere in the children components.

Related

Update Next.js to React 18 breaks my API calls using next-auth

This is a strange one, but here's the situation.
I'm using Next.js with the Next-auth package to handle authentication.
I'm not using Server-Side rendering, it's an admin area, so there is no need for SSR, and in order to authenticate users, I've created a HOC to wrap basically all components except for the "/sign-in" route.
This HOC all does is check if there's a session and then adds the "access token" to the Axios instance in order to use it for all async calls, and if there is no session, it redirects the user to the "sign-in" page like this ...
const AllowAuthenticated = (Component: any) => {
const AuthenticatedComponent = () => {
const { data: session, status }: any = useSession();
const router = useRouter();
useEffect(() => {
if (status !== "loading" && status === "unauthenticated") {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
router.push("/signin");
} else if (session) {
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${session.accessToken.accessToken}`;
}
}, [session, status]);
if (status === "loading" || status === "unauthenticated") {
return <LoadingSpinner />;
} else {
return <Component />;
}
};
return AuthenticatedComponent;
};
export default AllowAuthenticated;
And in the Axios instance, I'm checking if the response is "401", then I log out the user and send him to the "sign-in" screen, like this ...
axiosInstance.interceptors.response.use(
response => response,
error => {
const { status } = error.response;
if (status === 401) {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
return Promise.reject(error);
}
return Promise.reject(error);
},
);
Very simple stuff, and it works like a charm until I decided to upgrade my project to use "react 18.1.0" and "react-dom 18.1.0", then all of a sudden, my API calls doesn't get the "Authorization" header and they return "401" and the user gets logged out :(
If I tried to make an API call inside the HOC right after I set the Auth headers it works, sot I DO get the "token" from the session, but all the async dispatch calls inside the wrapped component return 401.
I forgot to mention, that this issue happens on page refresh, if I didn't refresh the page after I sign in, everything works great, but once I refresh the page the inner async dispatch calls return 401.
I Updated all the packages in my project including Axios and next-auth, but it didn't help.
I eventually had to downgrade back to "react 17.0.2" and everything works again.
Any help is much appreciated.
For those of you who might come across the same issue.
I managed to solve this by not including the logic for adding the token to the "Authorization" header inside the HOC, instead, I used a solution by #kamal-choudhary on a post on Github talking about how to add "JWT" to every axios call using next-auth.
Using #jaketoolson help at that Github post, he was able to attach the token to every "Axios" call.
The solution is basically to create an Axios instance and add an interceptor like I was doing above, but not just for the response, but also for request.
You'll add an interceptor for every request and check if there's a session, and then attach the JWT to the Authorization header.
That managed to solve my issue, and now next-auth works nicely with react 18.
Here's the code he's using ...
import axios from 'axios';
import { getSession } from 'next-auth/react';
const baseURL = process.env.SOME_API_URL || 'http://localhost:1337';
const ApiClient = () => {
const defaultOptions = {
baseURL,
};
const instance = axios.create(defaultOptions);
instance.interceptors.request.use(async (request) => {
const session = await getSession();
if (session) {
request.headers.Authorization = `Bearer ${session.jwt}`;
}
return request;
});
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(`error`, error);
},
);
return instance;
};
export default ApiClient();
Don't forget to give them a thumbs up for their help if it works for you ...
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1898233

Using a state variable in Axios put request in React Native

I am modifying a state variable (setTokens(tokens - 1)) in React native and then using the modified variable in an axios PUT request (library_tokens: tokens).
The problem is, the put request is successful, but it puts the old value of the variable instead of the modified one in the database after the PUT request.
The following is the code:
const [tokens,setTokens] = useState(0)
const lendBook = book => {
// Add book to the IssueReturn collection in the database
issuesApi
.post('/bookissue', {
booksIssued: book._id,
image: book.image,
user: context.stateUser.user.userId,
})
.then(response => {
if (response.status === 200) {
// Decrement user's library token
setTokens(tokens - 1);
// Modify the user's library tokens
userProfileApi
.put(
`/${context.stateUser.user.userId}`,
{
library_tokens: tokens,
},
{
headers: {Authorization: `Bearer ${jwtToken}`},
},
)
.then(response => {
console.log('tokens', tokens);
});
}
})
.catch(err => {
console.log(err);
});
};
You can use useEffect like this.
useEffect(() => {
console.log(tokens) // do something after state has updated
}, [tokens])
After state updation the useEffect will be called, so you can call userProfileApi inside useEffect.
setTokens it not a synchron function. code executed afterwards will not have the updated state. either use a local variable to store the result of token - 1 or make use of the useEffect hook.

No token being sent with request

I have a Vuex action that gets run each time a page loads (not router.push), this function seems to run fine in the sense that it checks for a token and if the token exists it moves on. My issue is that I am dispatching another action which requires this token.
Okay so a bit clearer, I am using Axios with Vue.js to send API request. I have the authorization header set to a Vuex store value in my main.js file. I then have my App.uve load which triggers a default action to run which checks for the existance of a token (JWT). This default action also dispatches another action called storeUser which sends off a GET request to a user info API endpoint. When sending this user info API call I am seeing on my back end that it is not an authorized API call. Checking into the headers I need the authorization header is undefined. Below is what I believe to be the relavent code.
Default action that runs on App.vue load
tryAutoLogin({commit, dispatch}) {
const token = localStorage.getItem('token')
if(!token) {return}
commit('authUser',{
token
})
dispatch('storeUser')
},
second action that is causing the issue
storeUser({commit, state}, userData) {
if(!state.token) return
axios.get('/user/userInfo')
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
},
main.js needed parts
import store from './store/store.js'
axios.defaults.headers.common['Authorization'] = store.token
new Vue({
render: h => h(App),
store,
router
}).$mount('#app')
I cut out a ton in main.js to make it cleaner but those are the related parts to this issue. I don't think that there is anything else.
store.js state
state: {
token: null,
name: '',
companyName: ''
},
Thanks to Phil in the comments on the OP I have the below code working.
axios.interceptors.request.use(function (config) {
config.headers.Authorization = store.state.token;
return config;
}, function (error) {
return Promise.reject(error);
});

Vuex, best practice with a global errors and notifications handling

here is what i do, and i'am not realy sure its correct :
//store
async addUser({commit}) {
try {
const {data} = await apiService.addUser()
commit('SET_USER', data)
commit('SET_NOTIFICATION', {type:'success', message: 'user successfuly created'})
} catch (error) {
commit('SET_NOTIFICATION', {type:'error', message:error})
}
}
SET_USER(state, user) {
state.users.push(user)
}
//my component:
async addUser() {
this.isLoading = true
await this.$store.dispatch('updatePatient', this.form)
this.isLoading = false
}
is it legit ?
sometimes i think i would need more logic inside my component depending on the succes or rejected api request. Should i put all the logic in my actions ? like i do at the moment ?
Maybe should I add a status state for each actions, for example :
state {
users: []
postUserSuccess: null
postUserError: false
updateUserSuccess: null
updateUserError: false
// ...
}
and do what i want in the component with a computed property mapped to the store ?
What do you think ?
I don't know if it's a best practice but I let the components the exception handling. That method has its pros (you don't have to pollute the state with error management) and cons (you have to repeat the error management code for every action call).
All service calls will be made in actions
The state will only be set in mutations.
All service calls will return a promise with a resolve(data to load in the state) and a reject(message errors to present).
There will be an interceptor to reject the response in case there's a custom error (here you can put if the response has an error prop reject the response and send as an error the error prop, now you don't have to deconstruct the response in the action).
I'm going to give you a simplified example (I use axios, you can learn how to do it with the library that you use).
Actions in Vuex are asynchronous. So you don't need to try/catch them.
ApiService - Add User
const addUser = () => {
return new Promise((resolve, reject) => {
axios
.post(url, user)
.then(response => resolve(response.data))
.catch(error => reject(error));
});
};
store
async addUser({commit}) {
const data = await apiService.addUser();
commit('SET_USER', data);
return data;
}
if the promise in apiService.addUser is resolved the commit is going to be made if is rejected axios will return the promise and you can catch the error in the component that calls the action.
Component
async addUser() {
this.isLoading = true;
try {
await this.$store.dispatch('updatePatient', this.form);
} catch (error) {
// here goes the code to display the error or do x if there is an error,
// sometimes I store an errors array in the data of the component other times I do x logic
}
this.isLoading = false;
}
State
Your state will be cleaner now that you don't need to store those errors there.
state {
users: []
}

Middleware executing before Vuex Store restore from localstorage

In nuxtjs project, I created an auth middleware to protect page.
and using vuex-persistedstate (also tried vuex-persist and nuxt-vuex-persist) to persist vuex store.
Everything is working fine when navigating from page to page, but when i refresh page or directly land to protected route, it redirect me to login page.
localStorage plugin
import createPersistedState from 'vuex-persistedstate'
export default ({ store }) => {
createPersistedState({
key: 'store-key'
})(store)
}
auth middleware
export default function ({ req, store, redirect, route }) {
const userIsLoggedIn = !!store.state.auth.user
if (!userIsLoggedIn) {
return redirect(`/auth/login?redirect=${route.fullPath}`)
}
return Promise.resolve()
}
I solved this problem by using this plugin vuex-persistedstate instead of the vuex-persist plugin. It seems there's some bug (or probably design architecture) in vuex-persist that's causing it.
With the Current approach, we will always fail.
Actual Problem is Vuex Store can never be sync with server side Vuex store.
The fact is we only need data string to be sync with client and server (token).
We can achieve this synchronization with Cookies. because cookies automatically pass to every request from browser. So we don't need to set to any request. Either you just hit the URL from browser address bar or through navigation.
I recommend using module 'cookie-universal-nuxt' for set and remove of cookies.
For Setting cookie after login
this.$cookies.set('token', 'Bearer '+response.tokens.access_token, { path: '/', maxAge: 60 * 60 * 12 })
For Removing cookie on logout
this.$cookies.remove('token')
Please go through the docs for better understanding.
Also I'm using #nuxt/http module for api request.
Now nuxt has a function called nuxtServerInit() in vuex store index file. You should use it to retrieve the token from request and set to http module headers.
async nuxtServerInit ({dispatch, commit}, {app, $http, req}) {
return new Promise((resolve, reject) => {
let token = app.$cookies.get('token')
if(!!token) {
$http.setToken(token, 'Bearer')
}
return resolve(true)
})
},
Below is my nuxt page level middleware
export default function ({app, req, store, redirect, route, context }) {
if(process.server) {
let token = app.$cookies.get('token')
if(!token) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Not Provided'}})
} else if(!isTokenValid(token.slice(7))) { // slice(7) used to trim Bearer(space)
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Expired'}})
}
return Promise.resolve()
}
else {
const userIsLoggedIn = !!store.state.auth.user
if (!userIsLoggedIn) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath}})
// return redirect(`/auth/login?redirect=${route.fullPath}`)
} else if (!isTokenValid(store.state.auth.tokens.access_token)) {
return redirect({path: '/auth/login', query: {redirect: route.fullPath, message: 'Token Expired'}})
// return redirect(`/auth/login?redirect=${route.fullPath}&message=Token Expired`)
} else if (isTokenValid(store.state.auth.tokens.refresh_token)) {
return redirect(`/auth/refresh`)
} else if (store.state.auth.user.role !== 'admin')
return redirect(`/403?message=Not having sufficient permission`)
return Promise.resolve()
}
}
I have write different condition for with different source of token, as in code. On Server Process i'm getting token from cookies and on client getting token store. (Here we can also get from cookies)
After this you may get Some hydration issue because of store data binding in layout. To overcome this issue use <no-ssr></no-ssr> wrapping for such type of template code.