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>
</>
);
}
Related
I have this component made for react-admin v3 that allows me to generate an id code. Now I'm upgrading to version 4 of react-admin and I don't know how to replace the part of the code where I do.
form.change("referredCode", code);
Here is the complete code of the component.
import React, { useEffect, useState } from "react";
import { TextInput, useDataProvider, LoadingIndicator } from "react-admin";
import { useForm } from "react-final-form";
import { randomIdGenerator } from "../../helpers/randomIdGenerator";
export default function UserReferredCode({ record }) {
const { referredCode } = record;
const [code, setCode] = useState("");
const [isLoading, setIsLoading] = useState(false);
const dataProvider = useDataProvider();
const form = useForm();
useEffect(() => {
if (!referredCode) {
// Generar id aleatorio
setIsLoading(true);
setCode(randomIdGenerator(6));
}
}, []);
useEffect(() => {
if (code) {
dataProvider
.getList("users", {
pagination: { page: 1, perPage: 1 },
filter: { referredCode: code },
})
.then(({ data }) => {
if (data.length > 0) {
setCode(randomIdGenerator(6));
} else {
setIsLoading(false);
}
})
.catch((e) => {
setIsLoading(false);
console.log(e);
});
}
form.change("referredCode", code);
}, [code]);
return (
<>
{isLoading ? (
<LoadingIndicator />
) : (
<TextInput
disabled
source="referredCode"
name="referredCode"
type="text"
placeholder="Code"
initialValue={referredCode || code}
/>
)}
</>
);
}
You should have a Form component wrapping all of this.
Check the Form documentation https://marmelab.com/react-admin/doc/4.0/Form.html
then you you can access it from useFormContext() (from react-hook-form).
the form has action setValue that accepts name and value
https://react-hook-form.com/api/useform/setvalue
So I have an issue where I set up the frontend so that when the user logs in, the Login button in the menu bar will change to display the user's name currently logged in with a drop-down list showing both the user profile and logout buttons. But every time I'm logging in with a user, the Login in the menu bar does not change, and even though in the console it says that the user has logged in, I can still go back to the login screen and log in again.
Below is the code:
Navbar
function Navbar() {
const [showLinks, setShowLinks] = useState(false);
const { state, dispatch: ctxDispatch } = useContext(Store);
const { cart, userInfo } = state;
const handleLogout = () => {
ctxDispatch({ type: 'USER_LOGOUT' });
localStorage.removeItem('userInfo');
};
console.log(userInfo);
<div className="rightSide">
<div className="linksTwo">
{/* {userInfo ? (
<NavDropdown id="nav-dropdown-light" title= {userInfo.name}>
<LinkContainer to="/profile">
<NavDropdown.Item> User Profile</NavDropdown.Item>
</LinkContainer>
<NavDropdown.Divider />
<Link
className="dropdown-item"
to="#logout"
onClick={handleLogout}
>
{' '}
Logout
</Link>
</NavDropdown>
) : ( */}
<Link to="/login">
Login <LoginIcon />
</Link>
{/* )} */}
</div>
</div>
Login
import Axios from 'axios';
import { useContext, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import Container from 'react-bootstrap/Container';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import { Helmet } from 'react-helmet-async';
import { Store } from '../components/Store';
import { toast } from 'react-toastify';
import { getError } from '../utils';
export default function Login() {
const navigate = useNavigate();
const { search } = useLocation();
const redirectInUrl = new URLSearchParams(search).get('redirect');
const redirect = redirectInUrl ? redirectInUrl : '/profile';
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { state, dispatch: ctxDispatch } = useContext(Store);
const { userInfo } = state;
function validateForm() {
return email.length > 0 && password.length > 0;
}
const handleSubmit = async (e) => {
e.preventDefault();
if (!password) {
toast.warn('Password is not correct');
return;
}
try {
const { data } = await Axios.post('/api/users/login', {
email,
password,
});
ctxDispatch({ type: 'USER_LOGIN', payload: data });
localStorage.setItem('userInfo', JSON.stringify(data));
navigate(redirect || '/profile');
toast.success(email + ' has logged in successfully');
} catch (err) {
toast.error(getError(err));
}
};
return (
<Container className="small-container">
<Helmet>
<title>Login</title>
</Helmet>
<h1 className="my-3">Login</h1>
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
required
onChange={(e) => setEmail(e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
required
onChange={(e) => setPassword(e.target.value)}
autoComplete="on"
/>
</Form.Group>
<div className="m-3">
<Button
type="submit"
className="btn btn-outline-success me-2"
disabled={!validateForm()}
>
Login
</Button>
</div>
<div className="m-3">
New customer? <Link to={`/register`}>Register a new account</Link>
</div>
</Form>
</Container>
);
}
In Login component you are doing:
const { state, dispatch: ctxDispatch } = useContext(Store);
In Navbar Component you are doing:
const { state, userInfo, dispatch: ctxDispatch } = useContext(Store);
I guess that you are just destructuring your context bad in Navbar, try:
const { state, dispatch: ctxDispatch } = useContext(Store);
const { userInfo } = state;
Or you could also abbreviate it in : const { state: { userInfo }, dispatch: ctxDispatch } = useContext(Store);
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 have the following React Native modules:
_localStorage.js
import AsyncStorage from '#react-native-community/async-storage';
const _storeData = async (key, value) => {
try {
await AsyncStorage.setItem(key, value);
} catch (error) {
console.log(error);
}
}
const _retrieveData = async (key) => {
try {
await AsyncStorage.getItem(key);
} catch (error) {
console.log(error);
}
}
export {_storeData, _retrieveData};
AppHeader.js
import React from 'react';
import {Button} from 'react-native-paper';
import {_retrieveData, _storeData} from '../../utils/_localStorage'
const LoginButton = () => {
return (
<Button icon='login' color='yellow' onPress={() => navigation.navigate('Login')}>
Login
</Button>
)
}
const UserButton = (user) => {
return (
<Button color='yellow' onPress={() => console.log('Botón usuario presionado...')}>
text
</Button>
)
}
const AppHeader = ({navigation, route}) => {
const user = _retrieveData('user');
console.log(user);
return user === ''? <LoginButton />: <UserButton user={user} />;
}
export default AppHeader;
I expect _retrieveData() to return the value of the key parameter, or null if it doesn't exits, but what I am getting in the console is this: {"_U": 0, "_V": 0, "_W": null, "_X": null}.
This is not how documentation of AsyncStorage indicates it works.
It's because you're not waiting for _retrieveData to finish. You're just setting user to the async function instead of waiting for its returned value.
Try something like this:
const AppHeader = ({navigation, route}) => {
const [user, setUser] = useState();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
fetchUser();
}, [])
const fetchUser = async () => {
setIsLoading(true);
const userData = await _retrieveData('user');
setUser(userData);
setIsLoading(false);
}
if (isLoading) return <LoadingIndicator />
if (!!user) return <UserButton user={user} />
return <LoginButton />;
}
I've called fetchUser in the initial useEffect that gets called when the AppHeader component is first loaded. It sets a loading boolean to true and then requests the user data. When the userData is returned it sets it in state and sets loading to false.
You don't need the loading bit but I included it otherwise your app would show the login button while it's fetching the data. You'll have to create the LoadingIndicator component yourself.
_retrieveData is returning promise here. You need to await for that promise to resolve. Try writing it like this:
const _retrieveData = async (key) => {
try {
const data = await AsyncStorage.getItem(key);
return data;
} catch (error) {
console.log(error);
}
}
AppHeader.js
const AppHeader = ({navigation, route}) => {
_retrieveData('user').then((user)=>{
console.log(user);
return user === ''? <LoginButton />: <UserButton user={user} />;
});
}
Read this for more clarity : https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
You're not returning anything from your _retrieveData function. Try writing it like so:
const _retrieveData = async (key) => {
try {
const data = await AsyncStorage.getItem(key);
return data;
} catch (error) {
console.log(error);
}
}
I have a function that looks like the following and I am trying to test the rowDoubleClicked function.
I mock the axios resolved value and I can see that the getAccountData function is being covered which should mean the update to dataArray.isLoading would be false.
however in my test when I debug the wrapper. It always hits the if statement that renders the loading div instead of the grid component and Im trying to figure out how to make it render the grid so that i can call the rowDoubleClicked function.
I've tried updating the wrapper, but it stays the same.
I've also tried doing an awat waitForElement on the component but it just gets timed out
import React, { useState } from 'react';
import axios from 'axios';
const MyComponent = (props) => {
let grid;
const dataArray = {
errorText: '',
rowData: '',
isLoading: true,
};
const [data, setData] = useState();
if (undefined !== data) {
dataArray.errorText = data.errorText;
dataArray.isLoading = data.isLoading;
dataArray.rowData = data.rowData;
}
const setShow = props.functions;
const getAccountData = async () => {
await axios
.get(props.endpoint)
.then((result) => {
dataArray.rowData = result;
})
.catch((error) => {
dataArray.errorText = error;
});
dataArray.isLoading = false;
setData(dataArray);
};
const handleClose = () => setShow(false);
const rowDoubleClicked = () => {
//some action
};
if (dataArray.errorText !== '') {
grid = (
<div>
<p>Error</p>
</div>
);
} else if (dataArray.isLoading) {
getAccountData();
grid = (
<div className="loading">
<p>Loading</p>
</div>
);
} else if (dataArray.rowData !== '') {
grid = <Grid handleRowDoubleClicked={rowDoubleClicked} />;
}
return (
<div>
<Modal visible={props.show} closable onCancel={handleClose}>
<div>{grid}</div>
</Modal>
</div>
);
};
export default MyComponent;
MyComponentView
import React from 'react'
import MyComponent from ''
const MyComponentView = (props) => {
const [select, setSelect] = React.useState('')
const [show, setShow] = React.useState(false)
const [selectedSearchBy, setSearchBy] = React.useState('')
const [selectedValue, setSearchByValue] = React.useState('')
const handleSearchIconClick = () => {
setShow(true)
}
const handleOnChange = (e) => {
setSearchBy(e.selectedOptionVal)
setSearchByValue(e.value)
}
return (
<div>
<form
action={`${endpoint`}
method='post'
onSubmit={handleSubmit}
>
<input type='hidden' id='searchBy' name='searchBy' value={selectedSearchBy} />
<input type='hidden' id='searchValue' name='searchValue' value={selectedValue} />
<Button data-testid='accessButton' id='accessButton' block color='primary'>
Search
</Button>
</form>
{show && (
<MyComponent
show
functions={setShow}
onModalApplyClick={handleApply}
endpoint={endpoint}
/>
)}
</div>
</div>
)
}
export default MyComponentView
here is my current test
it('performs double click on grid', async () => {
let wrapper;
let grid;
axios.get.mockResolvedValue(dataJSON);
wrapper = mount(
<MyComponent {...props} show>
<Modal>
<Grid {...gridProps} />
</Modal>
</MyComponent>
);
grid = wrapper.find(Grid);
wrapper.update();
await waitForElement(() => expect(grid).toBeTruthy());
grid.invoke('handleRowDoubleClicked')();
await act(() => Promise.resolve());
});
So it seems like your axios.get.mockResolvedValue is not working as intended. In such situations, I, personally, just use axios-mock-adapter.
Also, seems like waitForElement has been deprecated. How about a simple setTimeout with jest's done()?
import MockAdapter from 'axios-mock-adapter';
it('...', (done) => {
const mock = new MockAdapter(axios);
mock.onPost().reply(200, dataJSON);
//your test's logic
setTimeout(() => {
expect(grid).toBeTruthy();
}, 1000); //or any reasonable delay
});