Unable to set state in .then block after FireBase login with Facebook - reactjs

I can't seem to figure out what I'm doing incorrectly. Whenever I'm clicking on my Sign In button, my UserContext state does not get set accordingly.
I'm trying to get it to set, and then navigate to the next page.
Any insight on how I can best approach this and why this is happening?
import { UserContext } from "../Components/UserContext";
import { signInWithPopup, FacebookAuthProvider } from "firebase/auth";
[...]
let navigate = useNavigate();
const { user, setUser } = useContext(UserContext);
// Login from Firebase
const signInWithFacebook = () => {
const provider = new FacebookAuthProvider()
signInWithPopup(authentication, provider)
.then((result) => {
const credential = FacebookAuthProvider.credentialFromResult(result);
token = credential.accessToken;
userB = result.user;
setUser(userB)
console.log(user) // this equals null
navigate("../home") // because user state = null, "Home" does not exist in my router, then goes to my 404 page.
})
}
[.......]
return (
[...]
<button onClick={signInWithFacebook}> Sign In </button>
[...]
)
I've tried await in some portions but it never assigned itself accordingly. Along with that I've also tried to set the setUser in a different function, and called that function. That still didn't work for me.

Related

Nexts.js 13 + Supabase > What's the proper way to create a user context

I'm building an app with Next.js 13 and Supabase for the backend, and I've been stuck on figuring out the best/proper way to go about creating a context/provider for the current logged in user.
The flow to retrieve the user from Supabase is this:
Sign in with an OAuth Provider.
Grab the user ID from the session from the supabase onAuthState Changed hook.
Fetch the full user object from the supabase DB with the user ID mentioned above.
I have a supabase listener in my layout that listens for the auth state changes, and works well for setting and refreshing current session.
My initial approach was to add the fetchUser call from within the onAuthState changed hook, however I was running into late update hydration errors.
Taken directly from the examples, this is how the app looks:
// layout.tsx
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const supabase = createServerComponentSupabaseClient<Database>({
headers,
cookies,
});
const {
data: { session },
} = await supabase.auth.getSession();
return (
<html>
<head />
<body>
<NavMenu session={session} />
<SupabaseListener accessToken={session?.access_token} />
{children}
</body>
</html>
);
}
// supabase-listener.tsx
// taken directly from the supabase-auth-helpers library.
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import supabase from "../lib/supabase/supabase-browser";
export default function SupabaseListener({
accessToken,
}: {
accessToken?: string;
}) {
const router = useRouter();
useEffect(() => {
supabase.auth.onAuthStateChange(async (event, session) => {
if (session?.access_token !== accessToken) {
router.refresh();
}
});
}, [accessToken, router]);
return null;
}
I basically just need to wrap my root layout with a LoggedInUserProvider, make the fetch user call somewhere in the initial page load, and set the state of the current logged in user provider.
The other approaches I tried was making the fetch user call from the root layout, and having a LoggedInUserListener client component that takes the user as a property and simply sets the state if the profile exists. This was causing improper set state errors.
Thank you so much.
Check out this PR for a better example of how to structure the application and add a provider for sharing a single instance of Supabase client-side, as well as the session from the server 👍
If you follow a similar pattern, then your additional query for the full user record should go immediately after you get the session in examples/nextjs-server-components/app/layout.tsx. You could then pass this as a prop to the <SupabaseProvider /> and share it across the application from context's value prop.
I am following your awesome auth-helpers example but my context from the provider keeps coming back as null for user details. Is there anything wrong with the code below or is there some isLoading logic that will work better for getting that data?
Also want to confirm, does the SupabaseProvider in the root layout pass down to all other child layout components?
'use client';
import type { Session } from '#supabase/auth-helpers-nextjs';
import { createContext, useContext, useState, useEffect } from 'react';
import type { TypedSupabaseClient } from 'app/layout';
import { createBrowserClient } from 'utils/supabase-client';
import { UserDetails, CompanyDetails } from 'models/types';
type MaybeSession = Session | null;
type SupabaseContext = {
supabase: TypedSupabaseClient;
session: MaybeSession;
userDetails: UserDetails | null;
isLoading: boolean;
};
// #ts-ignore
const Context = createContext<SupabaseContext>();
//TODO get stripe subscription data
export default function SupabaseProvider({
children,
session
}: {
children: React.ReactNode;
session: MaybeSession;
}) {
const [supabase] = useState(() => createBrowserClient());
const [userDetails, setUserDetails] = useState<UserDetails | null>(null);
const [isLoading, setLoading] = useState(false);
// Hydrate user context and company data for a user
useEffect(() => {
const fetchUserDetails = async () => {
if (session && session.user) {
setLoading(true);
const { data } = await supabase
.from('users')
.select('*, organizations (*)')
.eq('id', session.user.id)
.single();
//TODO fix types
setUserDetails(data as any);
setLoading(false);
}
};
if (session) {
fetchUserDetails();
}
}, [session, supabase]);
return (
<Context.Provider value={{ supabase, session, userDetails, isLoading }}>
<>{children}</>
</Context.Provider>
);
}
export const useSupabase = () => useContext(Context);

How can I check for an already logged-in user on initial render using Firestore?

I'm seeing an issue where Firestore's auth.currentUser is always null on my app's initial render. Here's a simplified version of my component:
import ...
const [isInitialRender, setIsInitialRender] = useState(true);
const firebaseConfig = {...}
const app = initializeApp(firebaseConfig);
// Auth
export const auth = getAuth();
connectAuthEmulator(auth, "http://localhost:9099"); // Maybe part of the problem?
// DB
export const db = getFirestore();
connectFirestoreEmulator(db, "localhost", 8080);
useEffect(() => {
if (isInitialRender) {
setIsInitialRender(() => false);
//Attempt to check existing authentication
if (auth.currentUser) console.log("Logged in"); // Never logs
return;
}
if (auth.currentUser) console.log("Logged in"); // Works fine
}
I stole my login function from the docs. It looks something like this:
const signInWithGoogle = () => {
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
.then((result) => {})
.catch((error) => {
const errorMessage = error.message;
console.log("Google login encountered error:");
console.log(errorMessage);
});
};
This seems to work just fine - I can log in and console log user info, as well as log out and confirm auth.currentUser is null. It just seems like the code in useEffect() runs before currentUser is populated.
Any thoughts on how to get around this? Or maybe a better way to check for authentication on page load?
Answering this as community wiki.As mentioned above in comments by Mises
The onAuthStateChanged() observer is triggered on sign-in or sign-out.Set an authentication state observer and get user data
For each of your app's pages that need information about the signed-in
user, attach an observer to the global authentication object. This
observer gets called whenever the user's sign-in state changes.
Attach the observer using the onAuthStateChanged method. When a user
successfully signs in, you can get information about the user in the
observer.
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});

How to configure between pages role control automation in Next.js?

enter image description here
every time the route information changes, it should get the user's role information and then check it before going to the page he wants to go to.
If the permissions of the page and the user role match, it should load the page and allow access.
If there is no access permission, it should redirect the user to the error page without loading the page.
I need an algorithm that will capture all pages in "user_app" in "Next.js" and manage access by providing control.
1- When the user wants to enter a page, I got the path information.
2- I got the data where the page permissions are defined in the process.env with the path information.
3- I got the user role information from the cookie/token.
4- I compared the role information with the page permissions. If the user has permission, they can enter the page, if not, they will be redirected to the 404 page.
Problem: This control structure works when the user enters the page he wants to go to. and it makes that page accessible for a very short time. (while the control function is running)... as I added in the picture, the new page should be checked before loading and after the permission is granted, the page data should be loaded and run.
note: this function is triggered in app_js every time the page changes.
import React from 'react';
import axios from 'axios';
import UserLogout from '../UserLogout';
import decrypted from '../crypto/decrypted';
import { useRouter } from 'next/router';
export default async function RolePageCheck() {
try {
const router = useRouter();
//kullanıcı hangi sayfada onu alıyoruz.
const path = router.pathname;
//public erişebilir sayfaları aldık
//sadece izinle girilebilen sayfaları aldık
const pageRoles = process.env.pageLinks[path];
const res = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/token/${"GET"}`).then((value) => {
//cookie var ise kullanıcı bilgilerini alıyoruz.
if(value.data.Acoount !== null && value.data.Acoount !== undefined && value.data.success === true){
const decryptedValue = decrypted(value.data.Acoount);
const userRole = decryptedValue.role;
if(pageRoles.includes(userRole) && ( !pageRoles.includes(0) || path==="/")){
return true;
}
else{
return router.replace("/404");
}
}
else if(pageRoles.includes(0)){
return true;
}
else{
return router.replace("/404");
}
});
} catch (error) {
return { success: false, message: error.message };
}
}
you can achieve this with router.events
useEffect(() => {
// on initial load - run auth check
authCheck(router.asPath);
// on route change start - hide page content by setting authorized to false
const hideContent = () => setAuthorized(false);
const logAction = () => {
console.log('routeChangeStart');
};
router.events.on('routeChangeStart', logAction);
router.events.on('routeChangeStart', hideContent);
// on route change complete - run auth check
router.events.on('routeChangeComplete', authCheck);
// unsubscribe from events in useEffect return function
return () => {
router.events.off('routeChangeStart', logAction);
router.events.off('routeChangeStart', hideContent);
router.events.off('routeChangeComplete', authCheck);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

How to redirect back to private route after login in Next.js?

I am building a Next.js project where I want to implement private route similar to react-private route. In React this can be done by using react-router, but in Next.js this cannot be done. next/auth has the option to create private route I guess, but I am not using next/auth.
I have created a HOC for checking if the user is logged-in or not, but I'm not able to redirect the user to the private he/she wants to go after successfully logging in. How to achieve this functionality in Next.js? Can anybody help me in this?
This is the HOC, I used the code from a blog about private routing.
import React from 'react';
import Router from 'next/router';
const login = '/login'; // Define your login route address.
/**
* Check user authentication and authorization
* It depends on you and your auth service provider.
* #returns {{auth: null}}
*/
const checkUserAuthentication = () => {
const token = typeof window !== "undefined" && localStorage.getItem('test_token');
if(!token) {
return { auth: null };
} else return {auth:true};
// change null to { isAdmin: true } for test it.
};
export default WrappedComponent => {
const hocComponent = ({ ...props }) => <WrappedComponent {...props} />;
hocComponent.getInitialProps = async (context) => {
const userAuth = await checkUserAuthentication();
// Are you an authorized user or not?
if (!userAuth?.auth) {
// Handle server-side and client-side rendering.
if (context.res) {
context.res?.writeHead(302, {
Location: login,
});
context.res?.end();
} else {
Router.replace(login);
}
} else if (WrappedComponent.getInitialProps) {
const wrappedProps = await WrappedComponent.getInitialProps({...context, auth: userAuth});
return { ...wrappedProps, userAuth };
}
return { userAuth };
};
return hocComponent;
};
This is my private route code:
import withPrivateRoute from "../../components/withPrivateRoute";
// import WrappedComponent from "../../components/WrappedComponent";
const profilePage = () => {
return (
<div>
<h1>This is private route</h1>
</div>
)
}
export default withPrivateRoute(profilePage);
To redirect back to the protected route the user was trying to access, you can pass a query parameter with the current path (protected route path) when redirecting to the login page.
// Authentication HOC
const loginPath = `/login?from=${encodeURIComponent(context.asPath)}`;
if (context.res) {
context.res.writeHead(302, {
Location: loginPath
});
context.res.end();
} else {
Router.replace(loginPath);
}
In the login page, once the login is complete, you can access the query parameter from router.query.from and use that to redirect the user back.
// Login page
const login = () => {
// Your login logic
// If login is successful, redirect back to `router.query.from`
// or fallback to `/homepage` if login page was accessed directly
router.push(router.query.from && decodeURIComponent(router.query.from) ?? '/homepage');
}
Note that encodeURIComponent/decodeURIComponent is used because the asPath property can contain query string parameters. These need to be encoded when passed to the login URL, and then decoded back when the URL is used to redirect back.

Check if State is empty when transferring to a new page

This is my /chat page this is called from the /login page. I pass this chat page some data with
// this is inside of the page /login
history.push({
pathname: '/chat',
state: {
email: email,
name: password,
},
})
The problem is when I access /chat from /login everything works, but when I access /chat only I get the error TypeError: Cannot destructure property 'email' of 'state' as it is undefined.
Is there an option to query if state is empty? And if so take null?
import React, { useState, useEffect } from "react";
import axios from 'axios';
import { useLocation } from "react-router-dom";
function Chat() {
const { state } = useLocation();
const { email } = state;
const [person, setPerson] = useState([]);
const test_test = { email }
useEffect(() => {
axios.get('localhost:8000/'.concat('FILLER_', email.toString()))
.then((res) => {
const datapersons = res.data;
setPerson( datapersons );
})
.catch(error => {
console.log(error)
})
}, []);
return (
<div>
<h1>Welcome {person.givenname}</h1>
</div>
);
}
export default Chat
Check whether state exists before accessing it:
const email = state == null ? null : state.email;
If you're using TypeScript you can use the ?. syntax:
const email = state?.email;
The issue is when you go to /chat by itself you are not passing the link data to the new route anymore. This means that when you try to get state from useLocation() it won't be there as you didn't pass the data from login.
A solution I would recommend would be to cache the data in the browser and if you don't have useLocation() state you can then get it from the cached data. When you login you can save the information you want to the local cache here is some more detailed information on how to accomplish that. Wherever you login I would use
window.localStorage.setItem('email', 'userEmail');
and then in your component
const { email } = state ? state : window.localStorage.getItem('email');
And then when you log out make sure to clear out local storage unless you want to keep the user logged in between sessions you could use localStorage to accomplish that.

Resources