New to react native so I'm not sure if this is just a glitch. My ultimate intention is to check if a user has been fully onboarded or not. If a user logs in, they're a returning user, so they shouldn't go through the onboarding screens again.
This is the flow:
New user? Landing -> Registration -> Onboarding -> Home
Existing user? Landing -> Login -> Home
In order to know if the user is a returning user, I check to see if the onboarded completed variable is in the firestore db and if it's true, the Onboarded function is set to true. This all works fine, except that before the login page switches to the home page upon login, the screen displays the onboarding page for a quick second. How do I stop this? As this isn't ideal in production.
Here is a snippet of the login
export function LoginScreen({ navigation }) {
const { onboarded, setOnboarded, login } = useContext(AuthContext);
const [isChecked, setChecked] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
const userCred = await login(email, password);
const docRef = doc(db, "userInfo", userCred.user.uid);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
if (docSnap.get("onboardingCompleted") === true) {
setOnboarded(true);
} else {
console.log("nothing here for you");
}
} else {
console.log("nothing exists!");
}
};
Here is a snippet of my Routes page:
const Routes = () => {
const { user, setUser, onboarded, setOnboarded } = useContext(AuthContext);
const [initializing, setInitializing] = useState(true);
const StateChanged = (user) => {
setUser(user);
if (initializing) setInitializing(false);
};
useEffect(() => {
const subscriber = onAuthStateChanged(auth, StateChanged);
return subscriber; // unsubscribe on unmount
}, []);
if (initializing) return null;
const DisplayStacks = () => {
if (user && onboarded) {
// returning user or end of onboarding; working for returning user
return <AppStack />;
} else if (user) {
//after registering; clicking create account; working fine
return <RegistrationStack />;
} else {
return <AuthStack />; // before registering; working fine
}
};
return <NavigationContainer>{DisplayStacks()}</NavigationContainer>;
};
export default Routes;
Auth Context:
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [onboarded, setOnboarded] = useState(false);
return (
<AuthContext.Provider
value={{
user,
setUser,
onboarded,
setOnboarded,
login: async (email, password) => {
try {
const res = await signInWithEmailAndPassword(auth, email, password);
return res;
} catch (e) {
console.log(e);
alert("Wrong info mane!!");
}
},
register: async (email, password) => {
try {
await createUserWithEmailAndPassword(auth, email, password);
} catch (e) {
console.log(e);
alert(e);
}
},
logout: async () => {
try {
await signOut(auth);
setOnboarded(false);
} catch (e) {
console.log(e);
}
},
}}
>
{children}
</AuthContext.Provider>
);
};
User state is created in the Auth context, but it's set in the routes page
Centralize the authentication logic and the logic to check if an authenticated user has been onboarded into the AuthProvider component. In the stateChange callback for the onAuthStateChanged event handler do an additional isOnboarded check, and then only after both the user and isOnboarded states/checks have completed is the initializing state set false. This is obviously all not tested but I believe should get you close to the UX you desire.
AuthProvider
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isOnboarded, setIsOnboarded] = useState(false);
const [initializing, setInitializing] = useState(true);
useEffect(() => {
const checkOnboardStatus = async (userId) => {
const docRef = doc(db, "userInfo", userId);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
return docSnap.get("onboardingCompleted");
} else {
console.log("nothing exists!");
}
};
const stateChanged = async (user) => {
setUser(user);
try {
if (user?.uid) {
// have a user, check isOnboarded status
const isOnboarded = await checkOnboardStatus();
if (!isOnboarded) {
console.log("nothing here for you");
}
setIsOnboarded(isOnboarded);
} else {
// no user, clear isOnboarded status
setIsOnboarded(false);
}
} catch(error) {
// handle any fetching/processing error if necessary
} finally {
setInitializing(false);
}
};
const subscriber = onAuthStateChanged(auth, stateChanged);
return subscriber; // unsubscribe on unmount
}, []);
const login = (email, password) => {
try {
// Set initializing true here each time a user is authenticating
// to make the UI wait for the onboarded check to complete
setInitializing(true);
return signInWithEmailAndPassword(auth, email, password);
} catch (e) {
console.log(e);
alert("Wrong info mane!!");
}
};
const register = (email, password) => {
try {
return createUserWithEmailAndPassword(auth, email, password);
} catch (e) {
console.log(e);
alert(e);
}
};
const logout = () => {
try {
return signOut(auth);
} catch (e) {
console.log(e);
}
}
return (
<AuthContext.Provider
value={{
user,
isOnboarded,
initializing,
login,
register,
logout,
}}
>
{children}
</AuthContext.Provider>
);
};
Routes
const Routes = () => {
const { user, isOnboarded, initializing } = useContext(AuthContext);
if (initializing) return null;
return (
<NavigationContainer>
{user
? isOnboarded ? <AppStack /> : <RegistrationStack />
: <AuthStack />
}
</NavigationContainer>
);
};
LoginScreen
export function LoginScreen({ navigation }) {
const { login } = useContext(AuthContext);
const [isChecked, setChecked] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = () => {
login(email, password);
};
...
Related
I have two users (admin and user). When I log in as user I display user home screen and when I log out and log in as admin I still see user home screen until I refresh my app, then I can see the admin home screen.
Thank you in advance.
here is my code:
import { auth, db } from '../../firebase';
const Home = ({navigation})=>{
const [modalVisible, setModalVisible]=useState(false)
const [formType, setFormType] = React.useState("")
const [user, setUser] = useState(null) // This user
const [users, setUsers] = useState([]) // Other Users
useEffect(() => {
db.collection("users").doc(auth?.currentUser.uid).get()
.then(user => {
setUser(user.data())
})
}, [])
useEffect(() => {
if (user)
db.collection("users").where("role", "==", (user?.role === "admin" ? 'admin' : null))
.onSnapshot(users => {
if (!users.empty) {
const USERS = []
users.forEach(user => {
USERS.push(user.data())
})
setUsers(USERS)
}
})
}, [user])
const handleSignOut = ()=>{
auth
.signOut()
.then(()=>{
navigation.navigate('SignIn')
})
.catch(error => alert(error.message))
}
return(
<View>
{user?.role === 'admin'? <AdminScreen />:<UserScreen/>}
</View>
)
The issue is that the user state is not being updated when the user logs in as a different account. To solve this issue, I have added a listener to the auth object to detect changes in the current user and updating the user state when the current user changes.
import { auth, db } from '../../firebase';
const Home = ({navigation})=>{
const [modalVisible, setModalVisible]=useState(false)
const [formType, setFormType] = React.useState("")
const [user, setUser] = useState(null) // This user
const [users, setUsers] = useState([]) // Other Users
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async user => {
if (user) {
const userData = await db.collection("users").doc(user.uid).get();
setUser(userData.data());
} else {
setUser(null);
}
});
return () => unsubscribe();
}, [])
useEffect(() => {
if (user) {
const unsubscribe = db.collection("users").where("role", "==", (user?.role === "admin" ? 'admin' : null))
.onSnapshot(users => {
if (!users.empty) {
const USERS = []
users.forEach(user => {
USERS.push(user.data())
})
setUsers(USERS)
}
});
return () => unsubscribe();
}
}, [user])
const handleSignOut = ()=>{
auth
.signOut()
.then(()=>{
navigation.navigate('SignIn')
})
.catch(error => alert(error.message))
}
return(
<View>
{user?.role === 'admin'? <AdminScreen />:<UserScreen/>}
</View>
)
I have a login page where if a user enters the wrong credentials or submits blank fields, an error will be displayed on the page. Currently, when a user fails to signin with the right credentials, the error will only be displayed on the second click. I'm aware that my current problem is due to the state updating asynchronously, but I'm not sure how to resolve the problem in my code:
onst Login: React.FC<Props> = () => {
const user = useAppSelector(selectUser);
const auth = useAppSelector(selectAuth);
const dispatch = useAppDispatch();
...
const [signInError, setSignInError] = useState<boolean>(false);
const handleSignInError = () => {
if (auth.error?.status === 401 && auth.error.message === Constants.Errors.WRONG_CREDENTIALS) {
setSignInError(true);
}
}
const renderSigninError = () => {
if (signInError) {
return (
<Box paddingTop={2.5}>
<Alert variant="outlined" severity="error">
{Constants.Auth.FAILED_SIGN_IN}
</Alert>
</Box>
);
} else {
return (
<div/>
);
}
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData: any = new FormData(event.currentTarget);
const loginData: LoginData = {
email: formData.get("email"),
password: formData.get("password"),
}
try {
const res: any = await dispatch(login(loginData));
const resType: string = res.type;
if (resType === "auth/login/fulfilled") {
const userPayload: UserLogin = res.payload;
const loginUser: UserInterface = {
...
}
setSignInError(false);
dispatch(setUser(loginUser))
navigate("/");
}
else {
console.log("Failed to login");
handleSignInError();
}
}
catch (error: any) {
console.log(error.message);
}
}
return (
<Box
...code omitted...
{renderSigninError()}
...
)
}
What can I do to make sure that when the app loads and the user fails to login on the first click, the state for signInError should be true and the error displays?
You have at least 2 options.
add a conditional component.
Add a useEffect listenning for signInError and handle there as you want. It will trigger everytime signInError state changes
import React from 'react';
const [signInError, setError] = useState(false)
import React from 'react';
const [signInError, setError] = useState(false)
useEffect(() => {
console.log('new signInError value >', signInError)
// do whatever you want
}, [signInError])
export function App(props) {
return (
<div className='App'>
{
signInError ? (<p>something happened</p>) : null
}
</div>
);
}
There might be better approaches. Hope this can help you
I found a work around by changing handleSignInError() to update the state directly through setSignInError(true) as in:
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData: any = new FormData(event.currentTarget);
const loginData: LoginData = {
email: formData.get("email"),
password: formData.get("password"),
}
try {
const res: any = await dispatch(login(loginData));
const resType: string = res.type;
if (resType === "auth/login/fulfilled") {
const userPayload: UserLogin = res.payload;
const loginUser: UserInterface = {
...
}
setSignInError(false);
dispatch(setUser(loginUser))
navigate("/");
}
else {
console.log("Failed to login");
setSignInError(true); //changed here
}
}
catch (error: any) {
console.log(error.message);
}
}
Could someone help me understand why using another function initially didnt work?
I have a database of users with data like id and displayName. I have two types of users in my db: one with identity of "parent" and the other "author". I want to change the content of main page and navigation bar depending of which identity the currentUser has.
I retrieve currentUser data without any problem but when I try to find the same user in db by currentUser id i recieve null. This only happens when i tried to make one global function instead of pasting to every component.
My UserContext.js looks like that
export const UserContext = createContext({
currentUser: null,
setCurrentUser: () => null,
});
export const UserProvider = ({children}) => {
const [currentUser, setCurrentUser] = useState(null);
const [user, setUser] = useState(null);
useEffect(() => {
const unsub = onAuthStateChangedListener((user) => {
// console.log(user);
if (user) {
createUserDocumentFromAuth(user);
}
setCurrentUser(user);
});
return unsub;
}, [])
useEffect(() => {
if (!currentUser) return;
const docUserRef = doc(db, "users", currentUser.uid);
const getData = async () => {
const docSnapshot = await getDoc(docUserRef);
setUser(docSnapshot.data());
console.log("snapshot: ", docSnapshot.data())
}
getData();
}, [])
const value = {currentUser, setCurrentUser, user, setUser};
return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}
And here's where I am using it
Navbar.js
const {currentUser, user} = useContext(UserContext);
const navigate = useNavigate();
console.log("user: ", user)
console.log("user current: ", currentUser)
if (currentUser && user.identity === "parent") {
return (
<Nav>
<NavItem>{currentUser.displayName}</NavItem>
<NavItem>Parent</NavItem>
</Nav>)
} else if (currentUser && user.identity === "author") {
return (
<Nav>
<NavItem>{currentUser.displayName}</NavItem>
<NavItem>Author</NavItem>
</Nav>)
}
I am using the Auth Provider to manage my Firebase auth information. I want to be able to use currentUser as soon as I sign up, but it won't set without reloading.
I tried to setCurrentUser out of the Auth Provider and set it, but I could not get it to work either.
contexts/Auth.tsx
const AuthContext = createContext<IAuthContext>(null!)
export const AuthProvider = ({
children,
}: {
children: ReactNode
}) => {
const [currentFBUser, setCurrentFBUser] = useState<firebase.User | null>(null)
const [currentUser, setCurrentUser] = useState<any>(null)
const [isLoading, setIsLoading] = useState<boolean>(true)
const { update } = useIntercom()
/**
* SUBSCRIBE user auth state from firebase
*/
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (user) => {
if (!user) {
setCurrentFBUser(null)
setIsLoading(false)
return
}
await setCurrentFBUser(user)
const storeUser = await userRepository.findById(user.uid)
if (!storeUser) {
setCurrentUser(null)
setIsLoading(false)
return
}
await setCurrentUser(storeUser)
/* UPDATE Intercom props */
if(currentUser) {
update({
name: currentUser.name,
email: currentUser.email
})
}
setIsLoading(false)
return () => {
unsubscribe()
}
})
}, [])
const logout = useCallback(() => {
const auth = getAuth();
signOut(auth).then(() => {
window.location.reload()
}).catch((err) => {
toast.error(err.message)
});
}, [])
return (
<AuthContext.Provider value={{
currentFBUser,
currentUser,
setCurrentUser,
isLoading,
logout,
}}>
{children}
</AuthContext.Provider>
)
}
export const useAuthContext = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within the AuthProvider')
}
return context
}
signup.tsx
...
const { currentFBUser, isLoading, setCurrentUser } = useAuthContext()
const signup = handleSubmit(
async (data) => {
if (data.password != data.confirmPassword) {
toast.error('Your password is not matched!')
return
}
const auth = getAuth()
createUserWithEmailAndPassword(auth, data.email, data.password)
.then((userCredential) => {
const auth = userCredential.user
if (!auth) return
const { email, uid } = auth
if (!email) return
const user = userRepository.findOrCreate(email, uid)
setCurrentUser(user)
})
.catch((err) => {
toast.error(err.message)
});
},
(err: any) => {
toast.error(err.message)
},
)
...
Try to call again getAuth() instead of using the response from createUserWithEmailAndPassword
const { currentFBUser, isLoading, setCurrentUser } = useAuthContext()
const signup = handleSubmit(
async (data) => {
if (data.password != data.confirmPassword) {
toast.error('Your password is not matched!')
return
}
const auth = getAuth()
createUserWithEmailAndPassword(auth, data.email, data.password)
.then((userCredential) => {
// const auth = userCredential.user
const auth = getAuth().currentUser
if (!auth) return
const { email, uid } = auth
if (!email) return
const user = userRepository.findOrCreate(email, uid)
setCurrentUser(user)
})
.catch((err) => {
toast.error(err.message)
});
},
(err: any) => {
toast.error(err.message)
},
)
Based on https://dev.to/bmcmahen/using-firebase-with-react-hooks-21ap I have a authentication hook to get user state and firestore hook to get user data.
export const useAuth = () => {
const [state, setState] = React.useState(() => { const user = firebase.auth().currentUser return { initializing: !user, user, } })
function onChange(user) {
setState({ initializing: false, user })
}
React.useEffect(() => {
// listen for auth state changes
const unsubscribe = firebase.auth().onAuthStateChanged(onChange)
// unsubscribe to the listener when unmounting
return () => unsubscribe()
}, [])
return state
}
function useIngredients(id) {
const [error, setError] = React.useState(false)
const [loading, setLoading] = React.useState(true)
const [ingredients, setIngredients] = React.useState([])
useEffect(
() => {
const unsubscribe = firebase
.firestore()
.collection('recipes')
.doc(id)
.collection('ingredients') .onSnapshot( snapshot => { const ingredients = [] snapshot.forEach(doc => { ingredients.push(doc) }) setLoading(false) setIngredients(ingredients) }, err => { setError(err) } )
return () => unsubscribe()
},
[id]
)
return {
error,
loading,
ingredients,
}
}
Now in my app I can use this to get user state and data
function App() {
const { initializing, user } = useAuth()
const [error,loading,ingredients,] = useIngredients(user.uid);
if (initializing) {
return <div>Loading</div>
}
return (
<userContext.Provider value={{ user }}> <UserProfile /> </userContext.Provider> )
}
Since UID is null before auth state change trigger, firebase hook is getting called with empty key.
How to fetch data in this scenario once we understand that user is logged in.
May be you can add your document read inside auth hook.
export const useAuth = () => {
const [userContext, setUserContext] = useState<UserContext>(() => {
const context: UserContext = {
isAuthenticated: false,
isInitialized: false,
user: auth.currentUser,
userDetails: undefined
};
return context;
})
function onChange (user: firebase.User | null) {
if (user) {
db.collection('CollectionName').doc(user.uid)
.get()
.then(function (doc) {
//set it to context
})
});
}
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(onChange)
return () => unsubscribe()
}, [])
return userContextState
}
You can use some loading spinner in your provider to wait for things to complete.