social login - how to use variables outside of function in react? - reactjs

I try to use social login.
if I success to login in kakao. they give me access_token and I use it to mutation to my server
below is my code
import { useMutation } from "react-apollo-hooks";
import { KAKAO_LOGIN } from "./AuthQuery";
export default () => {
const kakaoLoginMutation = useMutation(KAKAO_LOGIN, {
variables: { provder: "kakao", accessToken: authObj.access_token },
});
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function (authObj) {
console.log(authObj.access_token);
},
});
};
if (authObj.access_token !== "") {
kakaoLoginMutation();
}
return (
<a href="#" onClick={kakaoLogin}>
<h1>카카오로그인</h1>
</a>
);
};
if I success to login using by function kakaoLogin, it give authObj.
console.log(authObj.access_token) show me access_token
and I want to use it to useMutation. but it show to me authObj is not defined.

Looks like you're looking for a local state to hold authObj
const [authObj, setAuthObj] = useState({});
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function(authObj) {
setAuthObj(authObj);
},
});
};
const kakaoLoginMutation = useMutation(KAKAO_LOGIN, {
variables: {
provder: "kakao",
accessToken: authObj.access_token
},
});
if (authObj.access_token !== "") {
kakaoLoginMutation();
}

After reading Apollo auth docs (you read it, right?) you should know tokens should be sent using headers.
... if auth link is used and reads the token from localStorage ... then any login function (mutation, request) result should end storing token in localStorage (to be passed by header in next queries/mutations) ... it should be obvious.
In this case
... we have a bit different situation - kakao token is passed into login mutation as variable ...
We can simply, directly (not using state, effect, rerenderings) pass the kakao token to 'mutation caller' (kakaoLoginMutation):
// mutation definition
const [kakaoLoginMutation] = useMutation(KAKAO_LOGIN);
// event handler
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function(authObj) {
// run mutation
kakaoLoginMutation({
variables: {
provder: "kakao",
accessToken: authObj.access_token
}
})
},
});
};
When required, login mutation (KAKAO_LOGIN) result can be processed within onCompleted handler:
const [kakaoLoginMutation] = useMutation(KAKAO_LOGIN,
onCompleted = (data) => {
// save mutation result in localStorage
// set some state to force rerendering
// or redirection
}
);

Related

Axios throwing CanceledError with Abort controller in react

I have built an axios private instance with interceptors to manage auth request.
The system has a custom axios instance:
const BASE_URL = 'http://localhost:8000';
export const axiosPrivate = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
A custom useRefreshToken hook returns accessToken using the refresh token:
const useRefreshToken = () => {
const { setAuth } = useAuth();
const refresh = async () => {
const response = await refreshTokens();
// console.log('response', response);
const { user, roles, accessToken } = response.data;
setAuth({ user, roles, accessToken });
// return accessToken for use in axiosClient
return accessToken;
};
return refresh;
};
export default useRefreshToken;
Axios interceptors are attached to this axios instance in useAxiosPrivate.js file to attached accessToken to request and refresh the accessToken using a refresh token if expired.
const useAxiosPrivate = () => {
const { auth } = useAuth();
const refresh = useRefreshToken();
useEffect(() => {
const requestIntercept = axiosPrivate.interceptors.request.use(
(config) => {
// attach the access token to the request if missing
if (!config.headers['Authorization']) {
config.headers['Authorization'] = `Bearer ${auth?.accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
const responseIntercept = axiosPrivate.interceptors.response.use(
(response) => response,
async (error) => {
const prevRequest = error?.config;
// sent = custom property, after 1st request - sent = true, so no looping requests
if (error?.response?.status === 403 && !prevRequest?.sent) {
prevRequest.sent = true;
const newAccessToken = await refresh();
prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequest);
}
return Promise.reject(error);
}
);
// remove the interceptor when the component unmounts
return () => {
axiosPrivate.interceptors.response.eject(responseIntercept);
axiosPrivate.interceptors.request.eject(requestIntercept);
};
}, [auth, refresh]);
return axiosPrivate;
};
export default useAxiosPrivate;
Now, this private axios instance is called in functional component - PanelLayout which is used to wrap around the pages and provide layout.
Here, I've tried to use AbortControllers in axios to terminate the request after the component is mounted.
function PanelLayout({ children, title }) {
const [user, setUser] = useState(null);
const axiosPrivate = useAxiosPrivate();
const router = useRouter();
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const signal = controller.signal;
const getUserProfile = async () => {
try {
const response = await axiosPrivate.get('/api/identity/profile', {
signal,
});
console.log(response.data);
isMounted && setUser(response.data.user);
} catch (error) {
console.log(error);
router.push({
pathname: '/seller/auth/login',
query: { from: router.pathname },
});
}
};
getUserProfile();
return () => {
isMounted = false;
controller.abort();
};
}, []);
console.log('page rendered');
return (
<div className='flex items-start'>
<Sidebar className='h-screen w-[10rem]' />
<section className='min-h-screen flex flex-col'>
<PanelHeader title={title} classname='left-[10rem] h-[3.5rem]' />
<main className='mt-[3.5rem] flex-1'>{children}</main>
</section>
</div>
);
}
export default PanelLayout;
However, the above code is throwing the following error:
CanceledError {message: 'canceled', name: 'CanceledError', code: 'ERR_CANCELED'}
code: "ERR_CANCELED"
message: "canceled"
name: "CanceledError"
[[Prototype]]: AxiosError
constructor: ƒ CanceledError(message)
__CANCEL__: true
[[Prototype]]: Error
Please suggest how to avoid the above error and get axios to work properly.
I also encountered the same issue and I thought that there was some flaw in my logic which caused the component to be mounted twice. After doing some digging I found that react apparently added this feature with with the new version 18 in StrictMode where useEffect was being run twice. Here's a link to the article clearly explaining this new behaviour.
One way you could solve this problem is by removing StrictMode from your application (Temporary Solution)
Another way is by using useRef hook to store some piece of state which is updated when your application is mounted the second time.
// CODE BEFORE USE EFFECT
const effectRun = useRef(false);
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const signal = controller.signal;
const getUserProfile = async () => {
try {
const response = await axiosPrivate.get('/api/identity/profile', {
signal,
});
console.log(response.data);
isMounted && setUser(response.data.user);
} catch (error) {
console.log(error);
router.push({
pathname: '/seller/auth/login',
query: { from: router.pathname },
});
}
};
// Check if useEffect has run the first time
if (effectRun.current) {
getUserProfile();
}
return () => {
isMounted = false;
controller.abort();
effectRun.current = true; // update the value of effectRun to true
};
}, []);
// CODE AFTER USE EFFECT
Found the solution from this YouTube video.
I, too, encountered this issue. What made it worse is that axios doesn't provide an HTTP status code when the request has been canceled, although you do get error.code === "ERR_CANCELED". I solved it by handling the abort within the axios interceptor:
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.code === "ERR_CANCELED") {
// aborted in useEffect cleanup
return Promise.resolve({status: 499})
}
return Promise.reject((error.response && error.response.data) || 'Error')
}
);
As you can see, I ensure that the error response in the case of an abort supplies a status code of 499.
I faced the same problem in similar project, lets start by understanding first the root cause of that problem.
in react 18 the try to make us convenient to the idea of mounting and unmounting components twice for future features that the are preparing, the the useEffect hook now is mounted first time then unmounted the mounted finally.
so they need from us adapt our projects to the idea of mount and unmount of components twice
so you have two ways, adapting these changes and try to adapt your code to accept mounting twice, or making some turn around code to overcome mounting twice, and I would prefer the first one.
here in your code after first mount you aborted your API request in clean up function, so when the component dismount and remount again it face an error when try to run previously aborted request, so it throw exception, that's what happens
1st solution (adapting to react changing):
return () => {
isMounted = false
isMounted && controller.abort()
}
so in above code we will abort controller once only when isMounted is true, and thats will solve your problem
2nd solution (turn around to react changing):
by using useRef hook and asign it to a variable and update its boolean value after excuting the whole code only one time.
const runOnce = useRef(true)
useEffect(()=>{
if(runOnce.current){
//requesting from API
return()=>{
runOnce.current = false
}
}
},[])
3rd solution (turn around to react changing):
remove React.StrictMode from index.js file

Why is my state not being updated in my callbacks?

I'm trying to understand the state in React, having a background with Vue.js.
I'm building a login which fetches a JWT-token, then stores the token in a global store so we can use it for subsequent API-calls. I'm using an axios interceptor to resolve the token from the store. However, the token is always an old version/from previous render.
I've read about the React lifecycle but this function is not used in the rendering. It's used as a callback. I understand that setting the state is async. I've tried wrapping the interceptor in a useEffect(.., [tokenStore.token]) and using setTimeout(). I feel like I'm missing something.
Why is my state not being updated in my callbacks? Am I going about this in an non-idiomatic way?
Usage:
<button onPress={() => loginWithToken('abc')}>
Sign In
</button>
User hook:
export function useUserState() {
const api = useApi();
function loginWithToken(token) {
tokenState.setToken(token);
api
.request('get', 'currentUser')
.then((data) => {
console.log(data);
})
.catch((errors) => {
console.log(errors);
});
}
}
The api:
export default function useApi(hasFiles = false) {
const tokenState = useTokenState();
const client = axios.create(/*...*/);
client.interceptors.request.use(function (config) {
config.headers!.Authorization = tokenState.token
? `Bearer ${tokenState.token}`
: '';
return config;
});
// ...
}
Token store using hookstate:
const tokenState = createState({
token: null,
});
export function useTokenState() {
const state = useState(tokenState);
return {
token: state.token.get(),
setToken: (token: string | null) => {
console.log('setToken: ' + token);
state.set({ token });
},
};
}
I solved it by updating the defaults instead:
setToken: token => {
apiClient.defaults.headers.Authorization = token
? `Bearer ${token}`
: undefined;
// ...

Firebase Passwordless Email Authentication Error in Expo App

I am setting up passwordless Auth in my Expo app using the Firebase SDK. I've gotten to the point where emails are being sent to the user's desired address with a redirect link back to the app. When the user clicks the link, they are indeed redirected but they are not being authenticated. I am receiving a generic error in the console :
ERROR: [Error: An internal error has occurred.]
But I know that my credentials are passing through properly as I have logged them out when the function runs:
isSignInWithEmailLink:true, url: exp://10.0.0.27:19000?apiKey=AIzaSyAmpd5DdsjOb-MNfVH3MgF1Gn2nT3TBcnY&oobCode=7FJTfBjM28gkn6GfBSAdgAk7wOegg9k4D5poVcylhSYAAAF8BO5gHQ&mode=signIn&lang=en
I am calling useEffect on this function:
useEffect(() => {
signInWithEmailLink();
}, []);
Send Link To Email (WORKING)
const sendSignInLinkToEmail = (email) => {
return auth
.sendSignInLinkToEmail(email, {
handleCodeInApp: true,
url: proxyUrl,
})
.then(() => {
return true;
});
};
User clicks on a link from the email to redirect to the app to Authenticate (NOT WORKING)
const signInWithEmailLink = async () => {
const url = await Linking.getInitialURL();
if (url) {
handleUrl(url);
}
Linking.addEventListener('url', ({ url }) => {
handleUrl(url);
});
};
(RETURNING ERROR)
const handleUrl = async (url) => {
const isSignInWithEmailLink = auth.isSignInWithEmailLink(url);
console.log('isSignInWithEmailLink: ', isSignInWithEmailLink, 'url', url);
if (isSignInWithEmailLink) {
try {
await auth.signInWithEmailLink(email, url);
} catch (error) {
console.log('ERROR:', error);
}
}
};
Have you enabled email sign in in your firebase console?
Are you storing the email in localStorage? It looks undefined in your logic.
Your listener should be in the useEffect hook.
I've code my code working looking like this:
const handleGetInitialURL = async () => {
const url = await Linking.getInitialURL()
if (url) {
handleSignInUrl(url)
}
}
const handleDeepLink = (event: Linking.EventType) => {
handleSignInUrl(event.url)
}
useEffect(() => {
handleGetInitialURL()
Linking.addEventListener('url', handleDeepLink)
return () => {
Linking.removeEventListener('url', handleDeepLink)
}
}, [])
You should use the onAuthStateChanged within useEffect rather than try and log the user in at that point in time. useEffect is used when you need your page to re-render based on changes.
For example:
useEffect(() => {
// onAuthStateChanged returns an unsubscriber
const unsubscribeAuth = auth.onAuthStateChanged(async authenticatedUser => {
try {
await (authenticatedUser ? setUser(authenticatedUser) : setUser(null));
setIsLoading(false);
} catch (error) {
console.log(error);
}
});
// unsubscribe auth listener on unmount
return unsubscribeAuth;
}, []);
You should invoke the user sign in method through other means such as a button to sign in, or validate user credentials at some other point within your app.
custom function:
const onLogin = async () => {
try {
if (email !== '' && password !== '') {
await auth.signInWithEmailAndPassword(email, password);
}
} catch (error) {
setLoginError(error.message);
}
};
Source: https://blog.jscrambler.com/how-to-integrate-firebase-authentication-with-an-expo-app

Using an async function on useEffect that has as an argument a State that's also async setted

I'm not sure how to title this question, please feel free to edit it.
I'm consuming an api that supplies projects (uniquely related to a login/password) and a list of status related to the tasks of those projects. It has two routes:
project route: receives a login and a password and returns its id and name:
{idProject: string, nome: string}
status route: receives an idProject and returns an array of its status
[{idStatus: string, area: string, descricao: string}]
I know mixing languages and not using a token is a very bad practice but i'm not the one coding the backend, i'm just consuming it. Since I'm using typescript, I decided not to translate the variables names otherwise types would mess up, I'm sorry about it.
I created a services.tsx to consume it:
import axios from "axios";
const api = axios.create({
baseURL: "", // omitted for safety reasons
});
export const getUserPromise = async (login: string, password: string) => {
const userPromise = api.get(`/projetos?login=${login}&senha=${password}`);
return userPromise;
};
export const getAllStatusPromise = async (idProjeto: string) => {
const AllStatusPromise = api.get(`/projetos/${idProjeto}/statusAreas`);
return AllStatusPromise;
};
My App.tsx defines functions getUser() and getAllStatus(), for handeling the promises and storing them in user and allStatus states.
It also defines an async getRequests() function, that's used on useEffect() for await calling getUser()and getAllStatus().
It also prints the results on the html:
import React, { useEffect, useState } from "react";
import { getUserPromise, getAllStatusPromise } from "./services";
const App: React.FC = () => {
const [user, setUser] = useState({
idProjeto: "0000",
nome: "Fallback User",
});
const [allStatus, setAllStatus] = useState([
{
idStatus: "0000",
area: "Fallback AllStatus",
descricao: "Fallback allStatus",
},
]);
useEffect(() => {
getRequests();
}, []);
const getRequests = async () => {
const mocked_login = ""; // Omitted for safety reasons
const mocked_password = ""; // Omitted for safety reasons
await getUser(mocked_login, mocked_password);
await getAllStatus(user.idProjeto);
};
const getUser = async (login: string, password: string) => {
try {
const userPromise = await getUserPromise(login, password);
const user = userPromise.data;
setUser(user);
} catch (error) {
console.log(error);
}
};
const getAllStatus = async (idProjeto: string) => {
try {
const allStatusPromise = await getAllStatusPromise(idProjeto);
const allStatus = allStatusPromise.data;
setAllStatus(allStatus);
} catch (error) {
console.log(error);
}
};
return (
<div>
<p>
user.nome: {user.nome} | user.idProjeto: {user.idProjeto}
</p>
<p>Number of status: {allStatus.length}</p>
{allStatus.map((status) => {
return (
<ul>
Status:
<li>status.area: {status.area}</li>
<li>status.descricao: {status.descricao}</li>
<li>status.idStatus: {status.idStatus}</li>
</ul>
);
})}
</div>
);
};
export default App;
I'm using user state as an argument for getAllStatus().
The piece of code works:
user was successfully set when rendering the page: user related to my mocked login and password has projectId === 12345.
However, allStatus does not have the expected values. It requested the fallback projectId 0000, which returns 63 status on the api. However, projecId 12345 does not have 63 status. It has only 5.
That means something is wrong with my async calls. user.projectId state is not set to 1245 when getAllStatus() is called. But it's when the page's rendered, that's why the rendering user works but the sequential call for getAllStatus don't.
How do I force getAllStatus() to wait for the state changing in getUser() to be finished?
useState sometimes does not change state immediately. In your case, you can return user and get it in your getRequests function.
const getRequests = async () => {
const mocked_login = ""; // Omitted for safety reasons
const mocked_password = ""; // Omitted for safety reasons
const user = await getUser(mocked_login, mocked_password); // get the returned user
await getAllStatus(user.idProjeto);
};
const getUser = async (login: string, password: string) => {
try {
const userPromise = await getUserPromise(login, password);
const user = userPromise.data;
setUser(user);
return user; // return it here
} catch (error) {
console.log(error);
}
};

Value from state not there | React

My Problem
I am using React and Apollo in my frontend application. When I login as a regular user I want to be navigated to a certain path, but before that I need to set my providers state with my selectedCompany value. However, sometimes I get my value from my provider in my components CDM and sometimes I don't, so I have to refresh in order to get the value.
I've tried to solve this, but with no luck. So I am turning to the peeps at SO.
My setup (code)
In my login component I have my login mutation, which looks like this:
login = async (username, password) => {
const { client, qoplaStore, history } = this.props;
try {
const result = await client.mutate({
mutation: LOGIN,
variables: {
credentials: {
username,
password,
}}
});
const authenticated = result.data.login;
const { token, roles } = authenticated;
sessionStorage.setItem('jwtToken', token);
sessionStorage.setItem('lastContactWithBackend', moment().unix());
qoplaStore.setSelectedUser(authenticated);
qoplaStore.setUserSessionTTL(result.data.ttlTimeoutMs);
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany();
history.push("/admin/shops");
}
} catch (error) {
this.setState({ errorMessage: loginErrorMessage(error) });
}
};
So if my login is successful I check the users roles, and in this case, I will get into the else statement. The function getAndSetSelectedCompany look like this:
getAndSetSelectedCompany = () => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0]);
});
};
So I am fetching my companies try to set one of them in my providers state with the function setSelectedCompany. selectedValues is what im passing down from my consumer to all my routes in my router file:
<QoplaConsumer>
{(selectedValues) => {
return (
...
)
}}
</QoplaConsumer
And in my provider I have my setSelectedCompany function which looks like this:
setSelectedCompany = company => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
})
};
And my selectedValues are coming from my providers state.
And in the component that has the route I'm sending the user to I have this in it's CDM:
async componentDidMount() {
const { client, selectedValues: { selectedCompany, authenticatedUser } } = this.props;
console.log('authenticatedUser', authenticatedUser)
if (selectedCompany.id === null) {
console.log('NULL')
}
Sometimes I get into the if statement, and sometimes I don't. But I rather always come into that if statement. And that is my current problem
All the help I can get is greatly appreciated and if more info is needed. Just let me know.
Thanks for reading.
Your getAndSetSelectedCompany is asynchronous and it also calls another method that does setState which is also async.
One way to do this would be to pass a callback to the getAndSetSelectedCompany that would be passed down and executed when the state is actually set.
changes to your login component
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany(()=>history.push("/admin/shops"));
}
changes to the two methods that are called
getAndSetSelectedCompany = (callback) => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0], callback);
});
};
setSelectedCompany = (company, callback) => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
}, callback)
};

Resources