I'm currently building a firebase login system with a verified email screen.
My problem is that I have a reload user button on the verified email screen that updates the user's credentials so that my root directory redirects the user to the AppStack if currentUser.emailVerified === true.
but the reload button isn't being triggered once pressed so that my root directory is still currentUser.emailVerified === false and not redirecting the user to the AppStack.
Login-System/context/AuthContext.js:
import React, { createContext, useContext, useState, useEffect } from 'react';
import { auth } from '../config';
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function sendVerification() {
return currentUser.sendEmailVerification();
}
const getUser = () => auth.currentUser;
const reloadUser = () => getUser().reload();
const reload = async () => {
try {
await reloadUser();
const user = getUser();
setCurrentUser(user);
} catch (error) {}
return reload;
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user);
setLoading(false);
});
return () => {
unsubscribe();
};
}, []);
const value = {
currentUser,
loading,
reload,
sendVerification,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
Login-System/screens/VerifyEmailScreen.js:
import React from 'react';
import { Text, StyleSheet } from 'react-native';
import { useAuth } from '../contexts/AuthContext';
import { View, Button } from '../components';
import { Colors } from '../config';
export const VerifyEmailScreen = () => {
const { currentUser, reload, sendVerification } = useAuth();
const handleSendVerification = async () => {
try {
await sendVerification();
} catch (error) {}
return handleSendVerification;
};
return (
<>
<View isSafe style={styles.container}>
<View style={styles.center}>
<Text style={styles.screenTitle}>Check your email</Text>
<Text style={styles.screenInfo}>{currentUser.email}</Text>
<Text style={styles.screenInfo}>
We sent you an email with instructions on how to verify your email
address. Click on the link in the email to get started.
</Text>
<Button
style={styles.button}
onPress={() => handleSendVerification()}>
<Text style={styles.buttonText}>Resend</Text>
</Button>
<Button style={styles.button} onPress={reload}>
<Text style={styles.buttonText}>Done</Text>
</Button>
</View>
</View>
</>
);
};
I did some similar works to check if the user email is verified or not you can use this function:
export function getUserEmailVerified() {
const user = firebase.auth().currentUser;
return !!user ? (user.emailVerified ? 'Yes' : 'No') : 'No';
}
To trigger an email verification you can use this method. This method will trigger an email verification and refresh the user.
export function verificationEmail(email, onSuccess) {
refreshUser();
const user = firebase.auth().currentUser;
const finishAction = message => {
onSuccess();
showToast(message);
};
user
.sendEmailVerification()
.then(() => {
refreshUser();
})
.catch(error => finishAction(error.message));
}
And to refresh the user you can use this method.
export function refreshUser() {
let user = firebase.auth().currentUser;
if (!!user && !user?.emailVerified) {
interval = setInterval(() => {
user?.reload().then();
}, 3000);
}
}
You also need to use onUserChanged from firebase to detect whether user information is changed or not. It returns a listener if your user email verified field is changed anyway it will get here is an example.
export function onUserChanged(listener) {
firebase.auth().onUserChanged(listener);
}
Related
I'm learning React as I need to write an AWS app using Cognito. This series of videos is very helpful (https://www.youtube.com/watch?v=R-3uXlTudSQ&list=PLDckhLrNepPR8y-9mDXsLutiwsLhreOk1&index=3&t=300s) but it doesn't explain how you redirect your app after you've logged in.
my App.js is this:
export default () => {
return (
<Account>
<Status />
<Signup />
<Login />
<ForgotPassword />
<Settings />
</Account>
);
};
The Settings component will only appear for an authenticated user. However, once you've logged in it doesn't appear until you refresh the page. How do I get it to show the settings page without having to refresh the page?
The settings component is:
export default () => {
return (
<Account>
<Status />
<Signup />
<Login />
<ForgotPassword />
<Settings />
<SearchParms/>
</Account>
);
};
And the Accounts component is this:
import React, { createContext } from "react";
import { CognitoUser, AuthenticationDetails } from "amazon-cognito-identity-js";
import Pool from "../UserPool";
const AccountContext = createContext();
const Account = props => {
const getSession = async () =>
await new Promise((resolve, reject) => {
const user = Pool.getCurrentUser();
if (user) {
user.getSession(async (err, session) => {
if (err) {
reject();
} else {
const attributes = await new Promise((resolve, reject) => {
user.getUserAttributes((err, attributes) => {
if (err) {
reject(err);
} else {
const results = {};
for (let attribute of attributes) {
const { Name, Value } = attribute;
results[Name] = Value;
}
resolve(results);
}
});
});
resolve({
user,
...session,
...attributes
});
}
});
} else {
reject();
}
});
const authenticate = async (Username, Password) =>
await new Promise((resolve, reject) => {
Username = "nick.wright#maintel.co.uk";
Password = "C411m3di4**&";
const user = new CognitoUser({ Username, Pool });
//const authDetails = new AuthenticationDetails({ Username, Password });
const authDetails = new AuthenticationDetails({ Username, Password });
user.authenticateUser(authDetails, {
onSuccess: data => {
console.log("onSuccess:", data);
resolve(data);
},
onFailure: err => {
console.error("onFailure:", err);
reject(err);
},
newPasswordRequired: data => {
console.log("newPasswordRequired:", data);
resolve(data);
}
});
});
const logout = () => {
const user = Pool.getCurrentUser();
if (user) {
user.signOut();
}
};
return (
<AccountContext.Provider
value={{
authenticate,
getSession,
logout
}}
>
{props.children}
</AccountContext.Provider>
);
};
export { Account, AccountContext };
In Settings I have
import React, { useState, useEffect, useContext } from "react";
import { AccountContext } from "./Accounts";
import ChangePassword from "./ChangePassword";
import ChangeEmail from "./ChangeEmail";
// eslint-disable-next-line import/no-anonymous-default-export
export default () => {
const [loggedIn, setLoggedIn] = useState(false);
const { getSession } = useContext(AccountContext);
useEffect(() => {
getSession().then(() => {
setLoggedIn(true);
}).catch((err) => console.log("Catch", err) )
}, [getSession]);;
return (
<div>
{loggedIn && (
<>
<h1>Settings</h1>
<ChangePassword />
<ChangeEmail />
</>
)}
</div>
);
};
and at this line:
const { getSession } = useContext(AccountContext);
I'm getting an "AccountContext is not defined" error.
I haven't been able to find any online examples that solve this issue. Is there a way of dynamically showing/hiding each element when the login button is clicked.
In this case, there's no need to define getSession, authenticate and logout on the context. You can put those functions inside a auth folder and call them whenever you need wihout having to define them in the context. What you need to define in the context is whether the user is logged in or not, because that's the information that you want to share in your whole application. Regarding the AccountContext is not defined, I don't see any issues from what you have shared. https://codesandbox.io/s/adoring-sun-kz30yr?file=/src/Settings.js
We link our website in a Tiktok profile similar to https://SKKNBYKIM.COM in this profile https://www.tiktok.com/#kimkardashian. When using the Tiktok mobile app and our website is clicked in a Tiktok profile, sometimes our BeatLoader loading component is displayed indefinitely. However, sometimes the website loads successfully (there doesn't seem to be a pattern).
However, if you visit the Tiktok profile on web browser and click the link to our website in the profile, it always loads. Also, the profile always loads the link when clicked directly through web browser, mobile browser, and from an Instagram profile link.
I suspect when the website doesn't load, it might be due to being unable to authenticate on Firebase since the line {(currUser && !isPullingUser) is never becoming true (verified by changing the BeatLoader component to something else and seeing it was displayed indefinitely as well). Is there any way to determine what the console looks like when the website is opened through the Tiktok app or some other way to diagnose what might be causing the problem?
/* global chrome */
import React, { useEffect } from "react";
import { fade, makeStyles } from "#material-ui/core/styles";
import UserChest from "../../components/chest/userChest";
import Box from "#material-ui/core/Box";
import {
BrowserRouter as Router,
useHistory,
useParams,
} from "react-router-dom";
import { db, firebase } from "../../api/firebase/firebase";
import ProductHeader from "../../components/headers/productHeader";
import { BeatLoader } from "react-spinners";
function Home(props) {
const classes = useStyles();
const [currUser, setCurrUser] = React.useState(null);
const [me, setMe] = React.useState(null);
const [allUsers, setAllUsers] = React.useState([]);
const [isMe, setIsMe] = React.useState(true);
const [isPullingUser, setIsPullingUser] = React.useState(false);
const [isPullingMe, setIsPullingMe] = React.useState(true);
let { username } = useParams();
const history = useHistory();
const handleFirstTimeSignUp = async (user) => {
await db.collection("users").doc(user.email).update({
isFirstSignIn: false,
});
};
const selectUser = () => {
setIsMe(false);
};
/*
getToken creates an https request that automatically authenticates
a user on our chrome extension
*/
const getToken = async (uid) => {
let callToken = firebase.functions().httpsCallable("generateLoginToken")(
uid
);
await callToken
.then((result) => {
// Read result of the Cloud Function.
let token = result.data;
var editorExtensionId = "OUR_CHROME_EXTENSION_EDITOR_ID";
chrome.runtime?.sendMessage(
editorExtensionId,
{ token: token },
function (response) {
if (response?.status === "success") {
console.log("success: auth token sent", response);
} else {
console.log("failure: sending auth token failed", response);
}
}
);
})
.catch((error) => {
console.log(error);
});
};
const getUserbyUsername = async username => {
let user = {};
const snapshot = await db
.collection("users")
.where("username", "==", username)
.get();
snapshot.forEach((doc) => {
user = doc.data();
});
return user;
};
useEffect(() => {
setIsPullingUser(true)
if (username) {
let unsubscribeUser;
(async () => {
let currentUser =
username !== "home"
? await getUserbyUsername(username)
: firebase.auth().currentUser;
if (username === 'home' && !currentUser) {
history.replace("/login")
}
let user = null;
let email = currentUser?.email;
// eslint-disable-next-line no-undef
const queryUser = db.collection("users").doc(email);
unsubscribeUser = queryUser.onSnapshot(
(doc) => {
let userPulled = doc.data();
if (userPulled) {
user = userPulled;
// if(user.username === username || !username)
setCurrUser(user);
setIsPullingUser(false)
}
}, (error) =>
console.log(error)
);
})();
return () => {
unsubscribeUser();
};
} else {
setCurrUser(null);
setIsPullingUser(false)
}
}, [username]);
useEffect(() => {
let currentUser = firebase.auth().currentUser;
if (currentUser && currentUser.uid) {
getToken(currentUser.uid);
let user = null;
let email = currentUser?.email;
// eslint-disable-next-line no-undef
async function getUser(doc) {
let userPulled = doc.data();
if (userPulled) {
user = userPulled;
setMe(user);
setIsPullingUser(false);
setIsPullingMe(false);
if (userPulled.isFirstSignIn) {
handleFirstTimeSignUp(user).then((r) =>
console.log("welcome to company name!")
);
}
} else {
history.push('/username')
}
}
const queryUser = db.collection("users").doc(email)
const unsubscribeUser = queryUser.onSnapshot(getUser, (error) =>
console.log(error)
);
return () => {
console.log('unsubscribe email')
unsubscribeUser();
};
} else {
setIsPullingMe(false);
}
}, []);
return (
<div style={{ minHeight: "100vh" }}>
{(currUser && !isPullingUser) ? (
<div>
<ProductHeader
me={me}
user={currUser}
selectUser={selectUser}
allUsers={allUsers}
tab={"home"}
/>
{ !isPullingMe &&
<UserChest
me={me}
isMe={isMe}
user={currUser}
/>
}
</div>
) :
<Box
style={{ margin: 10 }}
justifyContent={"center"}
alignItems={"center"}
>
<BeatLoader color={"pink"} size={18} margin={2} />
</Box>
}
</div>
);
}
export default Home;
Consider the following code:
const Home = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(authUser => {
if(authUser) {
setUser(authUser);
} else {
setUser(null)
}
});
return () => unsubscribe();
}, []);
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
The Login component has all the functions which handles all the Sign Up, Login and Third-Party Authentications using Firebase.
The problems are:
When I reload the page and if the user is already logged in, it shows the component for some time, and then renders the component, which gives a bad UX.
Also, when I sign in using Google or Facebook, again this component is rendered before finally rendering the component.
Please throw some light into this issue. Your help will be highly appreciated!
Edit:
Problem 1 is solved, but problem 2 is not. Here is the relevant code for problem 2:
Login.js
<div style={{ marginBottom: "2%" }}>
<GoogleSignup />
</div>
GoogleSignup.js
import { GoogleLoginButton } from "react-social-login-buttons";
import firebase from "firebase";
import fire from "../fire";
const GoogleSignup = ({ extensionId }) => {
const OnSubmitButton = async () => {
var provider = new firebase.auth.GoogleAuthProvider();
fire
.auth()
.signInWithPopup(provider)
.then((result) => {
const credential = result.credential;
const token = credential.accessToken;
const user = result.user;
})
.catch((error) => {
console.log(error);
});
};
return (
<div>
<GoogleLoginButton
style={{ fontSize: "17px" }}
text={"Continue with Google"}
align={"center"}
onClick={OnSubmitButton}
/>
</div>
);
};
export default GoogleSignup;
These lines:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(authUser => {
if(authUser) {
setUser(authUser);
} else {
setUser(null)
}
});
return () => unsubscribe();
}, []);
can be replaced with just:
useEffect(() => auth.onAuthStateChanged(setUser), []);
Next, instead of passing in just null to the useState, pass in current user.
const [user, setUser] = useState(null);
becomes
const [user, setUser] = useState(auth.currentUser);
This results in:
const Home = () => {
const [user, setUser] = useState(auth.currentUser);
useEffect(() => auth.onAuthStateChanged(setUser), []);
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
Personally, I tend to use undefined/null/firebase.auth.User using:
const Home = () => {
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const loadingUser = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
if (loadingUser)
return null; // or show loading icon, etc.
return (
<div>
{user ? (
<Hero />
) : (
<Login />
)}
</div>
)
}
export default Home
After the popup has closed, Firebase Authentication still needs to handle the authentication flow of exchanging the provider's authentication token for a Firebase User token. While this is taking place, you should show some form of loading screen in your component. In the below code sample, I change the "Continue with Google" text to "Signing in..." and disable the onClick events for each button while the sign in process takes place.
import { GoogleLoginButton } from "react-social-login-buttons";
import firebase from "firebase";
import fire from "../fire";
const PROVIDER_ID_GOOGLE = firebase.auth.GoogleAuthProvider.PROVIDER_ID;
const ignoreOnClick = () => {};
const GoogleSignup = ({ extensionId }) => {
const [activeSignInMethod, setActiveSignInMethod] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (activeSignInMethod === null)
return; // do nothing.
let disposed = false, provider;
switch (activeSignInMethod) {
case PROVIDER_ID_GOOGLE:
provider = new firebase.auth.GoogleAuthProvider();
break;
default:
// this is here to help catch when you've added a button
// but forgot to add the provider as a case above
setError("Unsupported authentication provider");
return;
}
fire.auth()
.signInWithPopup(provider)
.then((result) => {
// const credential = result.credential;
// const token = credential.accessToken;
// const user = result.user;
if (!disposed) {
setError(null);
setActiveSignInMethod(null);
}
})
.catch((error) => {
console.error(`Failed to sign in using ${activeSignInMethod}`, error);
if (!disposed) {
setError("Failed to sign in!");
setActiveSignInMethod(null);
}
});
return () => disposed = true; // <- this is to prevent any "updating destroyed component" errors
}, [activeSignInMethod]);
return (
{ error && (<div key="error">{error}</div>) }
<div key="signin-list">
<GoogleLoginButton
style={{ fontSize: "17px" }}
text={
activeSignInMethod == PROVIDER_ID_GOOGLE
? "Signing in..."
: "Continue with Google"
}
align={"center"}
onClick={
activeSignInMethod === null
? () => setActiveSignInMethod(PROVIDER_ID_GOOGLE)
: ignoreOnClick
}
/>
</div>
);
};
export default GoogleSignup;
I'm attempting to run a function within the useEffect hook, so that on screen load it automatically calls the context and works out what to do.
But for whatever reason, the function just isn't firing. The screen loads successfully and renders, no errors, but just doesn't do anything.
Here's my component I'm calling the context from:
import React, { useContext, useEffect } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import { AuthContext } from '../context/AuthContext';
const LoadingScreen = ({ navigation }) => {
const { userSignedIn } = useContext(AuthContext)
useEffect(() => {
userSignedIn()
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
And my context file:
import React, { useState, useContext } from 'react';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('API key');
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState([]);
const userSignedIn = () => {
return async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
}
};
return (
<AuthContext.Provider value={{ user, userSignedIn }}>
{ children }
</AuthContext.Provider>
)
Can anyone point out what I'm doing wrong? Feels a simple one.. But can't figure it out.
You are returning an async function when calling userSignedIn so the following should work for you by making userSignedIn itself async to work for those await calls inside.
const userSignedIn = async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
};
I am creating a Reat Native app which connects to an API from which it gets data.
I am using React Navigation to handle navigation. The app has a Stack Navigator and a Bottom Tab Navigator. The StackNavigator has 4 screens:
SignupScreen which handles creating account;
LoginScreen for handlong log in;
SplashScreen that checks for a local token and logs in the user automatically;
A LoadingScreen that triggers the initial fetch call to the API, stores the response in state and navigates to the MainFlow screen;
A MainFlow screen that contains the TabNavigator.
The TabNavigator has two screens, FeedScreen, Account and More where the initial screen is FeedScreen.
The signup/login/local flows are all working fine.
The issue: Once the user is logged in successfully the LoadingScreen is triggering the API call but the MainFlow components are being rendered before the data is in state. Because the components in MainFlow need the data, an error is thrown. How can I render the FeedScreen components only once the data is there?
In the LoadingScreen I am triggering an API call on useEffect from a context object, QuestionContext:
const LoadingScreen = ({ navigation }) => {
const [loading, setLoading] = useState(true);
const { state: authState } = useContext(AuthContext);
const { getQuestionsForUser, getAllQuestions } = useContext(QuestionContext);
useEffect(() => {
getAllQuestions();
}, []);
return (
<View style={styles.container}>
<YonStatusBar backgroundColor="#310B3B" />
<Image source={splashLogo} containerStyle={styles.splashLogo} />
<ActivityIndicator />
</View>
);
};
export default LoadingScreen;
getAllQuestions is a function in QuestionContext which makes the API call and navigates to FeedScreen:
const getAllQuestions = (dispatch) => {
return async () => {
try {
const token = await AsyncStorage.getItem('token');
const config = { headers: { Authorization: `Bearer ${token}` } };
const response = await yonyonApi.get(`/questions`, config);
dispatch({ type: 'GET_ALL_QUESTIONS', payload: response.data });
RootNavigation.navigate('MainFlow');
} catch (e) {
console.log(e);
}
};
};
getAllQuestions is working fine: the API call is successful and I can see that the response is stored in state. However, it navigates to MainFlow before that happens.
Finally, this is the FeedScreen:
const FeedScreen = () => {
const { state: questionState } = useContext(QuestionContext);
return (
<ScrollView style={styles.container}>
{console.log(questionState.questions)}
<View style={styles.listContainer}>
<QuestionCard />
</View>
</ScrollView>
);
};
export default FeedScreen;
The FeedScreen renders a QuestionCard which needs the data in questionState. This is what throwing the error: the QuestionCard is being rendered before the data is in state.
How can I make the navigation only navigate to FeedScreen once the necessary data is in state? Or alternatively, render something else than the QuestionCard while the data is not there and once the data is in questionState render the QuestionCard?
For me i will use screen instead of two screens as follows :
const FeedScreen = () => {
const [loading, setLoading] = useState(true);
const { state: authState } = useContext(AuthContext);
const [data, setData] = useState([]);
const getAllQuestions = (dispatch) => {
return async () => {
try {
const token = await AsyncStorage.getItem('token');
const config = { headers: { Authorization: `Bearer ${token}` } };
const response = await yonyonApi.get(`/questions`, config);
setData(response.data)
setLoading(false)
} catch (e) {
console.log(e);
}
};
};
useEffect(() => {
getAllQuestions();
}, []);
return (
<ScrollView style={styles.container}>
{
(loading)?
<ActivityIndicator/>
:
<View style={styles.listContainer}>
<QuestionCard data={data}/>
</View>
}
</ScrollView>
);
};
export default FeedScreen;
Why don't you set the initial state of your context to null and render your component if it is not null ?
const [questionState, setQuestionState] = useState(null);
...
const FeedScreen = () => {
const { state: questionState } = useContext(QuestionContext);
return (
<ScrollView style={styles.container}>
{!!questionState?.questions && console.log(questionState.questions)}
<View style={styles.listContainer}>
<QuestionCard />
</View>
</ScrollView>
);
};
export default FeedScreen;