When I learn authentication flow of react navigation 6.0, I read the sample code which used redux, it used dummy token, but In my react native project, I have to get the real token from server, So I tried to add some code in the sample project.
import * as React from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { useMutation, gql } from '#apollo/client';
import {
ApolloClient,
ApolloProvider,
createHttpLink,
InMemoryCache
} from '#apollo/client';
import { setContext } from 'apollo-link-context';
const SIGNIN_USER = gql`
mutation signIn($email: String!, $password: String!) {
signIn(email: $email, password: $password)
}
`;
// for example, my server address
const API_URI='https://xxxx.herokuapp.com/api';
const cache = new InMemoryCache();
const client = new ApolloClient({
uri: API_URI,
cache: new InMemoryCache()
});
const AuthContext = React.createContext();
function SplashScreen() {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
function HomeScreen() {
const { signOut } = React.useContext(AuthContext);
return (
<View>
<Text>Signed in!</Text>
<Button title="Sign out" onPress={signOut} />
</View>
);
}
function SignInScreen() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
return (
<View>
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Sign in" onPress={() => signIn({ username, password })} />
</View>
);
}
const Stack = createStackNavigator();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
// Restore token stored in `SecureStore` or any other encrypted storage
// userToken = await SecureStore.getItemAsync('userToken');
} catch (e) {
// Restoring token failed
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async ({username, password}) => {
useMutation(SIGNIN_USER, {
variables:{
email: username,
password: password
},
onCompleted: data => {
console.log(data);
}
});
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: data.signIn });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<ApolloProvider client={client}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
// We haven't finished checking for the token yet
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.userToken == null ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
// When logging out, a pop animation feels intuitive
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</ApolloProvider>
</AuthContext.Provider>
);
}
And I get the error,
WARN Possible Unhandled Promise Rejection (id: 0):
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
I searched it, it is because I use hooks in useMemo.
https://reactjs.org/warnings/invalid-hook-call-warning.html
To avoid confusion, it’s not supported to call Hooks in other cases:
🔴 Do not call Hooks in class components.
🔴 Do not call in event handlers.
🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
But, I have to send some data to server to get a token, and save token by Securestore or something else, If I can't write this section here, where?
I have no idea about where to write this section of code. My understanding to Redux in this situation:
when a user click button of 'sign in', call the function of signIn which is declared in authContext, useMemo, in use useMemo, get the token, save the token, then dispatch to notify data change, then go to the reducer.
After read a basic tutorial of redux, I have finished that by:
In SignInScreen, get the web token by 'login' function, which is triggered by the button of 'Sign in'
After execute it successfully, invoke the 'signIn' to update the state(by call dispatch).
I was confused with the name of 'signIn' function before, which is the same with my eventHandler function of 'signIn' button in SignInScreen. Actually, they are in duty of different logic.
import * as React from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { useMutation, gql } from '#apollo/client';
import {
ApolloClient,
ApolloProvider,
createHttpLink,
InMemoryCache
} from '#apollo/client';
import { setContext } from 'apollo-link-context';
const SIGNIN_USER = gql`
mutation signIn($email: String!, $password: String!) {
signIn(email: $email, password: $password)
}
`;
const API_URI='https://jseverywhere.herokuapp.com/api';
const cache = new InMemoryCache();
const client = new ApolloClient({
uri: API_URI,
cache: new InMemoryCache()
});
const AuthContext = React.createContext();
function SplashScreen() {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
function HomeScreen() {
const { signOut } = React.useContext(AuthContext);
return (
<View>
<Text>Signed in!</Text>
<Button title="Sign out" onPress={signOut} />
</View>
);
}
function SignInScreen() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
const [logIn, {loading, error}] = useMutation(SIGNIN_USER, {
variables:{
email: username,
password: password
},
onCompleted: data => {
console.log(data);
signIn(data);
}
});
if(loading)
return <Text>Loading...</Text>;
if(error)
return <Text>Error--{error.message}</Text>;
return (
<View>
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Sign in" onPress={() => logIn()} />
</View>
);
}
const Stack = createStackNavigator();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
// Restore token stored in `SecureStore` or any other encrypted storage
// userToken = await SecureStore.getItemAsync('userToken');
} catch (e) {
// Restoring token failed
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async (data) => {
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: data.signIn });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<ApolloProvider client={client}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
// We haven't finished checking for the token yet
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.userToken == null ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
// When logging out, a pop animation feels intuitive
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</ApolloProvider>
</AuthContext.Provider>
);
}
I am trying to store the user email with async storage using a remember me toggle switch. I am not sure why I am retrieving "null" with my getRememberedUser call? What am I doing wrong? This is React Native , and I also have a redux store (not really using here), but not sure if that is important. Thanks!
const [userEmail, setuserEmail] = useState("");
const [rememberMe, setrememberMe] = useState(false);
const rememberUser = async () => {
try {
await AsyncStorage.setItem("userEmail", userEmail);
console.log("stored");
} catch (error) {
// Error saving data
}
};
const forgetUser = async () => {
try {
await AsyncStorage.removeItem("userEmail");
console.log("forgotten");
} catch (error) {
// Error removing
}
};
const getRememberedUser = async () => {
try {
const value = await AsyncStorage.getItem("userEmail");
console.log(value)
if (value !== null) {
// We have username!!
setuserEmail(value);
}
} catch (error) {
// Error retrieving data
}
};
<Input
id="email"
label="E-Mail"
keyboardType="email-address"
required
email
autoCapitalize="none"
errorText="Please enter a valid email address."
onInputChange={inputChangeHandler}
initialValue={userEmail}
/>
<Switch
value={rememberMe}
onValueChange={(value) => toggleRememberMe(value)}
/>
<Text>Remember Me</Text>
<Button
title="get uersname"
onPress={() => {
getRememberedUser();
}}
></Button>
You need to stringify when saving,
await AsyncStorage.setItem("userEmail", JSON.stringify(userEmail));
and parse when retrieving
const value = await AsyncStorage.getItem("userEmail");
const userEmail = JSON.parse(value);
Nextjs Firebase Phone Auth
First attempt useEffect()
useEffect(() => {
window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha', {
'size': 'invisible',
'callback': (response) => {
console.log("This is not fired on loading", response)
}
})
}, [])
return (
<>
<div id="recaptcha"></div>
<button onClick={clicked}> Click me </button>
</>
)
This runs, however the recaptcha doesn't work... User is forced to pick fire hydrants.
Second attempt: React Component
Inspiration: https://stackoverflow.com/a/63860925/7451631
Import this to Login page
class Recap extends Component {
constructor(props) {
super(props);
this.signIn = this.signIn.bind(this);
}
componentDidMount() {
window.reCaptchaVerifier = new firebase.auth.RecaptchaVerifier(this.recaptcha, {
'size': 'invisible',
'callback': function (response) {
console.log("Magic", response)
}
})
}
signIn() {
firebase.auth().signInWithPhoneNumber(phoneNumber, window.reCaptchaVerifier).catch((error) => {
console.log(error)
})
}
render() {
return (
<>
<div ref={(ref) => this.recaptcha = ref} onClick={this.signIn}> Clik meeeee </div>
</>
)
}
}
Works! I got a ugly solution while typing up this question. If anyone knows how to make it nicer or can explain why the first attempt did not work that would be dope.
here is my solutions:
import { createFirebaseApp } from '#utils/firebase';
import { getAuth, PhoneAuthProvider, RecaptchaVerifier, signInWithCredential } from 'firebase/auth';
import { useState } from 'react';
export default function Example() {
const app = createFirebaseApp();
const auth = getAuth(app);
const [code, setCode] = useState('');
const [verificationId, setVerificationId] = useState('');
const signInWithPhone1 = async () => {
const applicationVerifier = new RecaptchaVerifier(
'sign-in-button',
{
size: 'invisible',
},
auth,
);
const provider = new PhoneAuthProvider(auth);
const vId = await provider.verifyPhoneNumber('+855012000001', applicationVerifier);
setVerificationId(vId);
};
const verify = async () => {
const authCredential = PhoneAuthProvider.credential(verificationId, code);
const userCredential = await signInWithCredential(auth, authCredential);
console.log('verify: ', userCredential);
};
return (
<>
<button id="sign-in-button" onClick={signInWithPhone1}>
SignIn With Phone1
</button>
<div>
<input type="text" value={code} onChange={(v) => setCode(v.target.value)} />
<button onClick={verify}>Verify</button>
</div>
</>
);
}
I'm using nextjs and having a problem with firebase authentication. When I log in it, I the session is stored at IndexedDB (I guess), and then I have a context that has an useEffect with the method onAuthStateChanged, which updates the user when it is changed.
Let's say I have a /login and a /dashboard (private page), when the login occurs, it should send me to /dashboard. That works fine. The problem comes when I try to go to /login (by typing the link in browser, thus refreshing) without logging off, which should send me back to /dashboard again. Instead of making the component be blank till the data is fetched, it loads the login page, only then renders the dashboard again.
const Dashboard = dynamic(() => import('../pages/dashboard'))
const router = useRouter()
const { signIn, user } = useAuth()
const { addToast } = useToast()
const formRef = useRef<FormHandles>(null)
useEffect(() => {
console.log(user)
if (!user) return
router.replace('/login', '/dashboard', { shallow: true })
}, [user])
I use dynamic to render the page conditionally
return (
<>
{user ? (
<Dashboard />
) : (
<>
<Head>
<title>Login</title>
</Head>
<Container>
<Content>
<img src={Logo} width={245} alt="Imobiliária Predial Primus" />
<Form ref={formRef} onSubmit={handleSubmit}>
<h1>Faça seu login</h1>
<Input name="email" placeholder="E-mail" />
<Input name="password" type="password" placeholder="Senha" />
<Button type="submit">Entrar</Button>
Esqueci minha senha
</Form>
</Content>
<Background />
</Container>
</>
)}
</>
)
As you can see, "user" is what conditionally renders the page, but since it comes null everytime the page loads, this problem occur.
This is the Auth.tsx context, which I wrap around the app.
import React, {
createContext,
useCallback,
useContext,
useState,
useEffect
} from 'react'
import { useRouter } from 'next/router'
// Firebase
import { auth } from '../config/firebase'
import { User } from '../interfaces'
interface SignInCredentials {
email: string
password: string
}
interface AuthContextData {
user: User
signIn(credentials: SignInCredentials): Promise<void>
signOut(): void
}
const AuthContext = createContext<AuthContextData>({} as AuthContextData)
const AuthProvider: React.FC = ({ children }) => {
const router = useRouter()
const [user, setUser] = useState(auth.currentUser)
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
console.log('user', user)
setUser(user)
if (!user) {
router.push('/login')
}
})
return () => {
unsubscribe()
}
}, [])
const signIn = useCallback(async (data: SignInCredentials) => {
await auth.signInWithEmailAndPassword(data.email, data.password)
}, [])
const signOut = useCallback(() => {
auth.signOut()
router.push('/login')
}, [])
return (
<AuthContext.Provider value={{ user, signIn, signOut }}>
{children}
</AuthContext.Provider>
)
}
function useAuth(): AuthContextData {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
export { AuthProvider, useAuth }
Fixed utilizing Cookies and NextPage, with getServerSideProps
I would like to use a GraphQL API on a React-Native App with react-apollo.
My server-side API is functional. All my tests with the playground works perfectly.
On the other hand, since react-native things get complicated: how to reuse what was set up server side? (mutation in particular) Do I have to duplicate the GraphQL code?
I exported my schema graphql using CodeGen (great tool!), But how to use it on react-native ?
My config on React-Native :
const httpLink = createHttpLink({
uri: API_GRAPHQL_URL,
clientState: {
typeDefs
}
});
const getToken = async () => await AsyncStorage.getItem('userToken');
const authLink = setContext(async (_, { headers }) => {
const token = await getToken();
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
My typeDefs is schema exported on CodeGen, for example :
But, how use Mutation with my exported configuration ? I use react-apollo-hooks.
The client part of GraphQL is not very clear for me, despite a great reaction-apollo documentation.
Would anyone help me or have a reference article on the subject?
Thank you so much !
in order to reuse you can create a schema.js file and export your queries from there in to to relevant screens, with regards to Mutations here is an example of a signUp page that uses Mutation.
import FormInput from '../components/FormInput';
import FormButton from '../components/FormButton';
import { useMutation } from '#apollo/client';
import { gql } from 'apollo-boost'
const SIGNUP_USER = gql`
mutation SignupMutation($email: String!, $password: String!, $name: String!) {
signupWithEmail(email: $email, password: $password, name: $name) {
user {
email
name
}
}
}`
function SignUp (){
export default function SignUp() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [signupWithEmail] = useMutation(SIGNUP_USER);
const navigation = useNavigation();
const navigation = useNavigation();
return (
<ScrollView>
<View style={{justifyContent: 'center', alignItems: 'center'}}>
<FormInput
style={styles.input}
maxLength={15}
placeholder="name"
onChangeText={name => setName(name)}
value={name}
/>
<FormInput
style={styles.input}
placeholder="email"
onChangeText={email => setEmail(email)}
value={email}
autoCorrect={false}
keyboardType='email-address'
autoCapitalize='none'
/>
<FormInput
style={styles.input}
maxLength={15}
secureTextEntry={true}
placeholder="password"
onChangeText={password => setPassword(password)}
value={password}
/>
<FormButton
title="Signup"
modeValue="contained"
color="#2D374F"
labelStyle={styles.loginButtonLabel}
onPress={() => signupWithEmail({variables: {email, password, name}})}
/>
</View>
</ScrollView>
);
}
your server side resolver should look like this please note im using firebase here.
Mutation: {
signupWithEmail: async (_, { email, password }) => {
const user = firebase.auth()
.createUserWithEmailAndPassword(email, password)
.then((userCredentials) => { return userCredentials.user.updateProfile
({displayName: name})
})
return { user }
},
},
and your schema should look like this:
type Mutation {
signupWithEmail(email: String!, password: String!, name: String!): AuthPayload!
loginWithEmail(email: String!, password: String!): AuthPayload!
}
type User {
uid : ID!
email: String!
name: String!
}
type AuthPayload {
user: User!
}