Using Redux-Saga how do I save my auth token? - reactjs

I'm using Redux-Saga in a React Native app. When I get the authentication token back from the server, how do I save it to local storage?
I tried using
await AsyncStorage.setItem("token", token);
but React Native complained and said await was a reserved word.
Am I misunderstanding something? Is the saga code not where I should be doing this?
Here is my code
function* loginFlow(action) {
try {
let username = action.username;
let password = action.password;
const response = yield call(getUser, username, password);
let token = response.headers.get("access-token");
const result = yield response.json();
if (token) {
console.log("success: ", token);
yield put({ type: LOGIN_SUCCESS, result });
} else {
if (result.error) {
yield put({ type: LOGIN_FAILURE, error: result.error });
}
}
} catch (e) {
yield put({ type: LOGIN_FAILURE, error: e.message });
console.log("error", e);
}
}
Edit:
Here is the getUser function:
const getUser = (username, password) => {
return fetch(`${apiURL}/${apiVersion}/${apiType}/${apiEndpoint_auth}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
email: username,
password: password
})
});
};

This is how i managed to store token inside redux-saga generator.
function* loginFlow(email, password) {
try {
// get your token
const token = yield call(loginApi, email, password);
// store token to local storage
yield call(storeToken, token);
yield put({ type: LOGIN_SUCCESS });
} catch (error) {
yield put({ type: LOGIN_ERROR, error });
}
}
function loginApi(email, password) {
return fetch('https://yourApiUrl', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
})
.then(response => response.json())
.then(json => json)
.catch((error) => {
throw error;
});
}
async function storeToken(token) {
try {
await AsyncStorage.setItem('token', token);
} catch (error) {
console.log('AsyncStorage error during token store:', error);
}
}
Note: Store your token before you dispatch your LOGIN_SUCCESS action. So that you will get your token in React Component by re-rendering made by LOGIC_SUCCESS action.

Since each method of the AsyncStorage API returns a Promise object, you could use redux-saga call(fn, ...args) function.
From the documentation of call(fn, ...args), you could use it on a normal function that returns a Promise as a result.
Creates an Effect description that instructs the middleware to call the function fn with args as arguments.
fn: Function - A Generator function, or normal function which either returns a Promise as result, or any other value.
args: Array - An array of values to be passed as arguments to fn
In this case, we could use yield call(fn, ...args) this way:
yield call(AsyncStorage.setItem, "token", token)
This would have the same effect as await, where it would block the execution until the Promise is resolved / rejected.
Full code snippet with minor comments:
function* loginFlow(action) {
try {
let username = action.username;
let password = action.password;
const response = yield call(getUser, username, password);
let token = response.headers.get("access-token");
const result = yield response.json();
if (token) {
console.log("success: ", token);
// Wait / block until the Promise is resolved
yield call(AsyncStorage.setItem, "token", token);
// Will be only executed once the previous line have been resolved
yield put({ type: LOGIN_SUCCESS, result });
} else {
if (result.error) {
yield put({ type: LOGIN_FAILURE, error: result.error });
}
}
} catch (e) {
yield put({ type: LOGIN_FAILURE, error: e.message });
console.log("error", e);
}
}
Reference:
https://redux-saga.js.org/docs/api/#callfn-args

Remember, the await keyword is only valid inside async functions. If you use it outside of an async function's body, you will get a SyntaxError.
Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
The function generator needs to have async before function.

Pass your auth token in below function.
saveToken = async (token) => {
try {
AsyncStorage.setItem("token", "" + token);
} catch (error) {
console.log('AsyncStorage error: ' + error.message);
}
}

Related

the post dispatch won't show up in redux, try gets the output but then catch throws an error and invalidates the token. It's an old codebase

I have been trying to refactor it , we have an async await function receiving credentials from the form component file which dispatches the credentials onSubmit
dispatch(submitLogin(credentials));
the submitLogin function is an exported async function with a try catch block
try {
//post login credentials
const res = await axios.post("seller/auth/login", credentials);
//extract token from data
const token = res.data.data.token;
//store token in local storage
localStorage.setItem("token", token);
//set token
setToken(token);
const decoded = {
name: res.data.data.name,
id: res.data.data.id,
};
const decodedJwt = jwt_decode(token);
localStorage.setItem("id", decoded.id);
localStorage.setItem("types", decodedJwt.types);
localStorage.setItem("sellerName", decoded.name);
const secondResponse = await axios.get("seller/auth/check-token",{
headers: {
"Authorization": `Bearer ${token}`
}
});
localStorage.setItem("isItValid", secondResponse.data.message);
dispatch({
type: GET_ERRORS,
payload: {},
});
dispatch({
type: SET_CURR_SELLER,
payload: decoded,
});
} catch (error) {
dispatch({
type: GET_ERRORS,
payload: error,
});
}
when I console.log(action) from reducers file it only logs GET_ERRORS
and the error message says
Invalid token specified: Cannot read properties of undefined (reading 'replace')

How can i handle axios error in redux-saga

Is there a way to capture the error response from axios in a try/catch redux-saga?
I have the following axios logging function:
export const login = (username, password) => {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const body = JSON.stringify({ username, password });
return axios
.post('http://127.0.0.1:8000/accounts-api/login', body, config)
.then(response => response.data)
.catch(error => error.response.data)
};
which returns
{user: {…}, token: "b5d36ec9086f7f2403a02c2e8d70695e85a48cad2d81e39c9b31fddaa759a79e"}
when the password and username is correct or
{non_field_errors: ["Incorrect Credentials"]}
when password or username is not correct.
The login function is called in the following redux-saga generator:
export function* signIn({ payload: { username, password } }) {
try {
const response = yield login(username, password)
let { user, token } = yield response
yield console.log(user)
yield console.log(token)
} catch(e) {
yield console.log(e)
}
}
Is there a way to capture the error response from axios ( in this case {non_field_errors: ["Incorrect Credentials"]} )in redux-saga catch?
Now with incorect user credential user and token are set to undefined and function is not moving on to redux-saga catch.

How to handle common fetch actions inside saga

I'm developping an API consuming web front site.
The problem
All my API saga were like this :
export function* login(action) {
const requestURL = "./api/auth/login"; // Endpoint URL
// Select the token if needed : const token = yield select(makeSelectToken());
const options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + btoa(JSON.stringify({ login: action.email, password: action.password })),
}
};
try {
// The request helper from react-boilerplate
const user = yield call(request, requestURL, options);
yield put(loginActions.loginSuccess(user.token);
yield put(push('/'));
} catch (err) {
yield put(loginActions.loginFailure(err.detailedMessage));
yield put(executeErrorHandler(err.code, err.detailedMessage, err.key)); // Error handling
}
}
And I had the same pattern with all my sagas :
Select the token if I need to call a private function in the start of the saga
const token = yield select(makeSelectToken());
Handle errors on the catch part
export const executeErrorHandler = (code, detailedMessage, key) => ({
type: HTTP_ERROR_HANDLER, status: code, detailedMessage, key
});
export function* errorHandler(action) {
switch (action.status) {
case 400:
yield put(addError(action.key, action.detailedMessage));
break;
case 401:
put(push('/login'));
break;
//other errors...
}
}
export default function* httpError() {
yield takeLatest(HTTP_ERROR_HANDLER, errorHandler);
}
The solution I came up with
Remove the token parts and error handling part and puth them inside the call helper :
export function* login(action) {
const url = `${apiUrl.public}/signin`;
const body = JSON.stringify({
email: action.email,
password: action.password,
});
try {
const user = yield call(postRequest, { url, body });
yield put(loginSuccess(user.token, action.email));
yield put(push('/'));
} catch (err) {
yield put(loginFailure());
}
}
// post request just call the default request with a "post" method
export function postRequest({ url, headers, body, auth = null }) {
return request(url, 'post', headers, body, auth);
}
export default function request(url, method, headers, body, auth = null) {
const options = { method, headers, body };
return fetch(url, addHeader(options, auth)) // add header will add the token if auth == true
.then(checkStatus)
.then(parseJSON)
.catch(handleError); // the error handler
}
function handleError(error) {
if (error.code === 401) {
put(push('/login')); // <-- Here this doesn't work
}
if (error.code == 400) {
displayToast(error);
}
}
function addHeader(options = {}, auth) {
const newOptions = { ...options };
if (!options.headers) {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
...options.headers,
};
}
if (auth) {
const token = yield select(makeSelectToken()); // <-- here it doesn't work
newOptions.headers.Authorization = `Bearer ${auth}`;
}
return newOptions;
}
I know the solution is between generator functions, side effects, yield call / select but I tried so many things it didn't work. For example, if I wrap everything inside generator functions, the token load is executed after the code continues and call the API.
Your help would be appreciated.
You need to run any and all effects (e.g. yield select) from a generator function, so you'll need generators all the way down to the point in your call stack where you yield an effect. Given that I would try to push those calls as high as possible. I assume you may have getRequest, putRequest etc. in addition to postRequest so if you want to avoid duplicating the yield select you'll want to do it in request. I can't fully test your snippet but I believe this should work:
export function* postRequest({ url, headers, body, auth = null }) {
return yield call(request, url, 'post', headers, body, auth); // could yield directly but using `call` makes testing eaiser
}
export default function* request(url, method, headers, body, auth = null) {
const options = { method, headers, body };
const token = auth ? yield select(makeSelectToken()) : null;
try {
const response = yield call(fetch, url, addHeader(options, token));
const checkedResponse = checkStatus(response);
return parseJSON(checkedResponse);
} catch (e) {
const errorEffect = getErrorEffect(e); // replaces handleError
if (errorEffect) {
yield errorEffect;
}
}
}
function addHeader(options = {}, token) {
const newOptions = { ...options };
if (!options.headers) {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
...options.headers,
};
}
if (token) {
newOptions.headers.Authorization = `Bearer ${token}`;
}
return newOptions;
}
function getErrorEffect(error) {
if (error.code === 401) {
return put(push('/login')); // returns the effect for the `request` generator to yeild
}
if (error.code == 400) {
return displayToast(error); // assuming `displayToast` is an effect that can be yielded directly
}
}

React Saga Generator yield call undefined object

So I am using axios to call my server and get response, and tried it with redux-saga but with no success. When I console log inside my axios call I got response, but signInUser in yield call is undefined forever. What can be wrong here?
const signInUserM = async (email, password) => {
await axios
.get("https://localhost:44320/Account/token")
.then(async function(response) {
const { data } = response;
axios.defaults.headers.common = {
Authorization: `Bearer ${data.token}`
};
await axios
.post("https://localhost:44320/Login", {
email: email,
password: password
})
.then(authUser => {
console.log(authUser); // got response
return authUser;
})
.catch(error => {
console.log(error);
return error;
});
})
.catch(error => {
console.log(error);
return error;
});
};
function* signInUserG({ payload }) {
const { email, password } = payload;
try {
const signInUser = yield call(
signInUserM,
email,
password
);
console.log(signInUser); // undefined forever
if (signInUser) {
// never gets here
yield put(userSignInSuccess(signInUser.id));
}
} catch (error) {
console.log(error);
}
}
Thanks for any help!
You forgot return in signInUserM and in front of the other await as well I think.

Fetch Post Request not returning payload but return status code (200)

So I am trying to create a user using redux-form. I have an express post route on the backend. NOTE: using redux-thunk for middleware, whatwg-fetch with webpack and babel-polyfill.
routes.post('/signup', async (req, res) => {
try {
const createdUser = await userController.createUser(req.body);
const JSONCreatedUser = JSON.stringify(createdUser);
res.json({
confirmation: 'success',
result: createdUser,
});
return JSONCreatedUser;
} catch (error) {
res.statusMessage = error.toString();
res.status(409).json({
confirmation: 'failure',
error: error.toString(),
});
}
});
So the problem I am having is that when I use postman. I will get the entire user object back.
But when I submit it using form I only get
Apimanager.js
export const signUserUpApi = async (url, params) => {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
const { status, statusText } = response;
if (status === 409) {
throw new Error(statusText);
}
return response;
} catch (error) {
throw new Error(error.toString());
}
};
action.js
import constants from '../constants';
import { signUserUpApi } from '../utils/APIManager';
const signUserUpUrl = process.env.SIGN_USER_UP_URL || 'http://localhost:3000/user/signup';
export const signUserUp = (user) => {
return async (dispatch) => {
try {
const createdUser = await signUserUpApi(signUserUpUrl, user);
dispatch({
type: constants.SIGN_USER_UP,
user: createdUser,
});
return createdUser;
} catch (error) {
throw new Error(error);
}
};
};
export const signUserIn = (user) => {
return {
type: constants.SIGN_USER_UP,
user,
};
};
What I am trying to do is to get the User Object I created when I submit the form and redirect back to the page.
This is what I get back and it did create the user.
First thing, I need is why am I getting the https status code back and not the user object?
Second thing, what are the ways to redirect to the home page when a user successfully signed up logged in.

Resources