Facebook and Google requires two login click to login with Firebase Auth - reactjs

I have React web application with firebase auth (mail, Facebook, Google).
Google and Facebook work only after 2 login clicks.
The code is equal, just the provider is different.
import React from 'react';
import firebase from "firebase/app";
import { app } from "../../../../config/firebase";
const signupWithGoogle = (user, userInfo)=>{
app.firestore().collection('users').doc(user.uid).set({
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name});
const batch = app.firestore().batch();
const initData = [
{ Applied: { positionIds: [], title: 'Applied' } },
{ Contract: { positionIds: [], title: 'Contract' } },
{ Denied: { positionIds: [], title: 'Denied' } },
{ InProgress: { positionIds: [], title: 'In Progress' } },
{ ReceivedTask: { positionIds: [], title: 'Received Task' } },
];
initData.forEach((doc) => {
const docRef = app
.firestore()
.collection('users')
.doc( user.uid)
.collection('columns')
.doc(Object.keys(doc)[0]);
batch.set(docRef, Object.values(doc)[0]);
});
const batchCommit= batch.commit();
return batchCommit;
}
export const googleLogin = async (
history
) => {
var provider = new firebase.auth.GoogleAuthProvider();
await firebase.auth()
.signInWithPopup(provider)
.then( resp => {
let {user, credential,additionalUserInfo: userInfo} = resp;
if (userInfo.isNewUser) signupWithGoogle(user, userInfo);
}).then(()=>
history.push('/')
)
.catch((error) => {
console.error(error.message);
});
};
I saw this question, but didn't help.(Firebase Authentication Requires Two 'Login' Calls)

I had the same problem with Firebase Authentication with Facebook, I had to register two times to make it works.
The problem was in my HTLM, I used a form.
I changed for a simpler code, and it worked.

While waiting for where you call your function from, as your issue would relate to improper state management, here are some edits you can make to the code you have shared so far to squash some problems that it has.
In your signupWithGoogle function, you create a floating promise that should be included in the write batch that you use to create the /users/{userId}/columns collection. Because you use Object.keys(doc)[0] and Object.values(doc)[0], you should consider using an array of [docId, docData] pairs or a JSON-like object structure like so:
// prepare data to add to the user's columns collection
const initColumnsData = {
Applied: { positionIds: [], title: 'Applied' },
Contract: { positionIds: [], title: 'Contract' },
Denied: { positionIds: [], title: 'Denied' },
InProgress: { positionIds: [], title: 'In Progress' },
ReceivedTask: { positionIds: [], title: 'Received Task' }
};
// queue columns data upload
Object.entries(initColumnsData)
.forEach(([docId, docData]) => {
const docRef = userDocRef
.collection('columns')
.doc(docId);
batch.set(docRef, docData);
});
As you mentioned that a lot of your code is shared aside from the provider implementation, you should consider extracting the common code from those functions:
const initUserData = (user, userDocData) => {
// init write batch
const batch = app.firestore().batch();
// init ref to user data
const userDocRef = app.firestore().collection('users').doc(user.uid);
// queue user data upload
batch.set(userDocRef, userDocData);
// prepare data to add to the user's columns collection
const initColumnsData = {
Applied: { positionIds: [], title: 'Applied' },
Contract: { positionIds: [], title: 'Contract' },
Denied: { positionIds: [], title: 'Denied' },
InProgress: { positionIds: [], title: 'In Progress' },
ReceivedTask: { positionIds: [], title: 'Received Task' }
};
// queue columns data upload
Object.entries(initColumnsData)
.forEach(([docId, docData]) => {
const docRef = userDocRef
.collection('columns')
.doc(docId);
batch.set(docRef, docData);
});
// make the changes
return batch.commit();
}
const initUserDataForGoogle(user, userInfo) {
return initUserData(user, {
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name
});
}
const initUserDataForFacebook(user, userInfo) {
return initUserData(user, {
firstName: /* ... */,
lastName: /* ... */
});
}
When exporting a function to be called elsewhere, avoid causing "side effects" (like navigating using the History API) and don't trap errors (using .catch() without rethrowing the error). The calling code should handle the result and any errors itself.
export const loginWithGoogle = async () => {
const provider = new firebase.auth.GoogleAuthProvider();
return firebase.auth()
.signInWithPopup(provider)
.then(async resp => {
const {user, credential, additionalUserInfo: userInfo} = resp;
if (userInfo.isNewUser)
await initUserDataForGoogle(user, userInfo);
return user;
});
};
Then in your components, you'd use:
setLoading(true);
/* await/return */ loginWithGoogle()
.then(() => {
history.push('/');
// or
// setLoading(false)
// then do something
})
.catch((err) => {
console.error("loginWithGoogle failed: ", err);
setLoading(false);
setError("Failed to log in with Google!"); // <- displayed in UI to user
});

Related

Next-Auth with firebase Authentication

just wanna have my custom credential provider which authenticate the entered username and password with Firebase Authentication on sign in page
pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth"
import { getDatabase } from "firebase/database"
import { DB } from "../../../constants/firebase"
import { FirebaseAdapter } from "#next-auth/firebase-adapter"
import * as firestoreFunctions from "firebase/firestore"
import CredentialsProvider from "next-auth/providers/credentials"
export default NextAuth({
session: {
strategy: "database",
},
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "somebody#gmail.com",
},
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
const database = getDatabase()
console.log(database)
const user = {
id: 1,
usename: "j",
password: "123456789",
}
if (
credentials?.username === user.usename &&
credentials.password === "123456789"
) {
return user
}
return null
},
}),
],
adapter: FirebaseAdapter({
db: DB,
...firestoreFunctions,
}),
// pages: {
// signIn: "/auth/signin",
// signOut: "/auth/signout",
// error: "/auth/error", // Error code passed in query string as ?error=
// verifyRequest: "/auth/verify-request", // (used for check email message)
// newUser: "/auth/new-user", // New users will be directed here on first sign in (leave the property out if not of interest)
// },
callbacks: {
async jwt({ token, user }) {
if (user) {
token.email = user.email
}
return token
},
async session({ session, token, user }) {
if (token) {
session.user!.email = token.email
}
return session
},
redirect({ url, baseUrl }) {
if (url.startsWith(baseUrl)) return url
else if (url.startsWith("/"))
return new URL(url, baseUrl).toString()
return baseUrl
},
},
})
firebase.ts
import { initializeApp, getApp, getApps } from "firebase/app"
import { getAnalytics } from "firebase/analytics"
import { getFirestore } from "#firebase/firestore"
import { getStorage } from "#firebase/storage"
import getFirebaseObject from "./firebaseConfig"
const app = !getApps.length ? initializeApp(getFirebaseObject()) : getApp()
const DB = getFirestore(app)
const storages = getStorage()
const analytics = getAnalytics(app)
export { app, DB, analytics, storages }
as you see
const user = {
id: 1,
usename: "j",
password: "123456789",
}
in fact except of these static data wanna search and get right user info from the Firebase
I know there are a some other way of doing this but I like working with next-auth for last change wanna make sure there's a spot of light in this was ;)
i found this public repository where the author does something similar to what you want to achieve, which is create a custom token with your database credentials.
May be this repository can help you. It has a few errors, but it gave me a general idea about what to do, as I had a similar case.
try {
if (user !== null) {
await customTokenSignIn(user.id, user.email);
(await getUser(user.id)) ??
(await createUser(toReqUser(user, account)));
const data = await getUser(user.id);
setResUser(user, data as ResUser);
return true;
}
return false;
} catch (e) {
console.error(e);
return false;
}
const customTokenSignIn = async (id: string, email: string) => {
const hash = toHash(id);
const customToken = await adminAuth.createCustomToken(hash);
await auth.signInWithCustomToken(customToken).then((res) => {
res.user?.updateEmail(email);
});
await adminAuth.setCustomUserClaims(hash, { sid: id });
await createUserToken({ id: id, firebaseUid: hash });
};

React/Axios/Lambda/DynamoDB: Each child in a list should have a unique "key" prop

I have an issue with adding data from react app to DynamoDB. I have this code in my react app to submit data from a form to DynamoDB using axios:
export const addTodo = createAsyncThunk(
'todoApp/todos/addTodo',
async (todo, { dispatch, getState }) => {
const response = await axios.post('https://aws.us-west-2.amazonaws.com/default/todoApp-newTodo', todo);
const data = await response.data;
dispatch(getTodos());
return data;
}
);
and my Lambda function is this:
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient({region: "us-west-2"});
exports.handler = (event, context, callback) => {
console.log("Processing...");
const params = {
Item: {
id: "",
title: "",
notes: "",
startDate: "new Date(2018, 8, 3)",
dueDate: new Date(2018, 8, 5),
completed: false,
starred: false,
important: false,
deleted: false,
labels: [1]
},
TableName: "new-todo"
};
const response = {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
},
body: JSON.stringify('Hello from new Lambda!'),
};
docClient.put(params, function(err, data) {
if(err){
callback(err, null);
} else {
callback(null, data);
}
})
};
When I run the app, and submit the form, I get error message for: unique "key" prop.
I tried following code, and it successfully adds random key in database, but the info I entered in the form, will be gone.
this.setState({ id: response.data.id });
Have you tried adding unique value with "key" property?
And you can use this code for update state.
this.setState(prevState => ({
...prevState, // rest of the object
value: 'something' // the value we want to update
}}))

Accessing the values from Promise inside useEffect hook

const useOnfidoFetch = (URL) => {
useEffect(() => {
const appToken = axios.get('http://localhost:5000/post_stuff')
.then((response) => response.data.data.data.json_data)
.then((json_data) => {
const id = json_data.applicant_id;
const token = json_data.onfido_sdk_token;
return {id, token};
});
if (appToken) {
console.log('this is working!');
OnfidoSDK.init({
// the JWT token you generated above
token: null,
containerId: "root",
steps: [
{
type: 'welcome',
options: {
title: 'Open your new bank account',
},
},
'document'
],
onComplete: function (data) {
console.log('everything is complete');
axios.post('https://third/party/api/v2/server-api/anonymous_invoke?aid=onfido_webapp', {
params: {
applicant_id: appToken.applicant_id
}
});
}
});
}
}, [URL]);
}
export default function() {
const URL = `${transmitAPI}/anonymous_invoke?aid=onfido_webapp`;
const result = useOnfidoFetch(URL, {});
return (
<div id={onfidoContainerId} />
);
}
I have refactored this dozens of times already, I am getting back some values from the appToken Promise, but I need to provide the token value from that Promise to that token property inside of Onfido.init({}) and I need to provide the id to the applicant_id property and I continue to get undefined.
If you need the token for something else as well, then i would suggest storing it in useState, and triggering OnfidoSDK.init when the value of that state changes.
Like this:
const useOnfidoFetch = (URL) => {
const [token, setToken] = useState();
useEffect(() => {
axios.get('http://localhost:5000/post_stuff')
.then((response) => response.data.data.data.json_data)
.then((json_data) => {
const token = json_data.onfido_sdk_token;
setToken(token);
})
}, [URL])
useEffect(() => {
if (!token) return;
OnfidoSDK.init({
// the JWT token you generated above
token,
containerId: "root",
steps: [
{
type: 'welcome',
options: {
title: 'Open your new bank account',
},
},
'document'
],
onComplete: function (data) {
console.log('everything is complete');
axios.post('https://third/party/api/v2/server-api/anonymous_invoke?aid=onfido_webapp', {
params: {
applicant_id: appToken.applicant_id
}
});
}
});
}, [token]);
}
Move the entire if(appToken){ ... } inside the body of the second .then((json_data) => { ... })
Something like this:
const useOnfidoFetch = (URL) => {
useEffect(() => {
const appToken = axios.get('http://localhost:5000/post_stuff')
.then((response) => response.data.data.data.json_data)
.then((json_data) => {
const id = json_data.applicant_id;
const token = json_data.onfido_sdk_token;
// Here. This code will be executed after the values are available for id and token
if (appToken) {
console.log('this is working!');
OnfidoSDK.init({
// the JWT token you generated above
token: null,
containerId: "root",
steps: [
{
type: 'welcome',
options: {
title: 'Open your new bank account',
},
},
'document'
],
onComplete: function (data) {
console.log('everything is complete');
axios.post('https://third/party/api/v2/server-api/anonymous_invoke?aid=onfido_webapp', {
params: {
applicant_id: appToken.applicant_id
}
});
}
});
}
return {id, token};
});
// In here the promise is not yet finished executing, so `id` and `token` are not yet available
}, [URL]);
};
export default function() {
const URL = `${transmitAPI}/anonymous_invoke?aid=onfido_webapp`;
const result = useOnfidoFetch(URL, {});
return (
<div id={onfidoContainerId} />
);
}
For better readability, you could also move the if(appToken){ ... } block inside a separate function that takes id, token as arguments, which you can call from inside the promise.then block.

How to test a redux-thunk action that contains multiple API requests and array transformations?

I have a redux-thunk action that contains multiple API-requests that take data fetched from one endpoint to fetch other relevant data from a different endpoint and I also have a couple of array transformations to merge some of the data together.
Although I'm not sure if this is the best practice, for now, it does what I need. However, it has been difficult to test as I'm not sure what the correct approach is to test it. I have scoured the internet and looked at many different variations of "thunk" tests but mine is failing with every approach so far.
I will really appreciate some guidance on how to test a thunk action such as mine or perhaps better practices in implementing what I have if it makes testing easier.
My thunk-Action...
export const fetchTopStreamsStartAsync = () => {
return async dispatch => {
try {
const headers = {
'Client-ID': process.env.CLIENT_ID
};
const url = 'https://api.twitch.tv/helix/streams?first=5';
const userUrl = 'https://api.twitch.tv/helix/users?';
let userIds = '';
dispatch(fetchTopStreamsStart());
const response = await axios.get(url, { headers });
const topStreams = response.data.data;
topStreams.forEach(stream => (userIds += `id=${stream.user_id}&`));
userIds = userIds.slice(0, -1);
const userResponse = await axios.get(userUrl + userIds, { headers });
const users = userResponse.data.data;
const completeStreams = topStreams.map(stream => {
stream.avatar = users.find(
user => user.id === stream.user_id
).profile_image_url;
return stream;
});
const mappedStreams = completeStreams.map(
({ thumbnail_url, ...rest }) => ({
...rest,
thumbnail: thumbnail_url.replace(/{width}x{height}/gi, '1280x720')
})
);
dispatch(fetchTopStreamsSuccess(mappedStreams));
} catch (error) {
dispatch(fetchTopStreamsFail(error.message));
}
};
};
One of the many test approaches that have failed...
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import axios from 'axios';
import moxios from 'moxios';
import {
fetchTopStreamsStart,
fetchTopStreamsSuccess,
fetchTopStreamsStartAsync
} from './streams.actions';
const mockStore = configureMockStore([thunk]);
describe('thunks', () => {
describe('fetchTopStreamsStartAsync', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
it('creates both fetchTopStreamsStart and fetchTopStreamsSuccess when api call succeeds', () => {
const responsePayload = [{ id: 1 }, { id: 2 }, { id: 3 }];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: responsePayload
});
});
const store = mockStore();
const expectedActions = [
fetchTopStreamsStart(),
fetchTopStreamsSuccess(responsePayload)
];
return store.dispatch(fetchTopStreamsStartAsync()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});
This is the error i'm getting in the failed test for the received value...
+ "payload": "Cannot read property 'forEach' of undefined",
+ "type": "FETCH_TOP_STREAMS_FAIL",
UPDATE: As #mgarcia suggested i changed the format of my responsePayload from [{ id: 1 }, { id: 2 }, { id: 3 }] to { data: [{ id: 1 }, { id: 2 }, { id: 3 }] } and now I'm not getting the initial error but now I'm receiving the following error:
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
What I still don't understand is does the test have to replicate the exact structure of the multiple API calls or that just mocking one response is enough? I'm still trying to figure out the cause of the Async callback... error.
You are mocking the axios request through moxios, but it seems that you are not returning the data in the expected format.
In your action creator you read the response data as:
const topStreams = response.data.data;
const users = userResponse.data.data;
But you are mocking the response so that it returns:
const responsePayload = [{ id: 1 }, { id: 2 }, { id: 3 }];
Instead, it seems that you should be returning:
const responsePayload = { data: [{ id: 1 }, { id: 2 }, { id: 3 }] };
Aside from the mock response, your code presents some further problems. First, as you have noticed yourself, you are only mocking the first request. You should mock the second request as well returning the desired data. Second, in your assertion you are expecting to have the actions created in:
const expectedActions = [
fetchTopStreamsStart(),
fetchTopStreamsSuccess(responsePayload)
];
This will not be true, as you are processing the responsePayload in the action creator, so that the payload with which you are calling fetchTopStreamsSuccess in the action creator will be different from responsePayload.
Taking all this into account, your test code could look like:
it('creates both fetchTopStreamsStart and fetchTopStreamsSuccess when api call succeeds', () => {
const streamsResponse = [
{ user_id: 1, thumbnail_url: 'thumbnail-1-{width}x{height}' },
{ user_id: 2, thumbnail_url: 'thumbnail-2-{width}x{height}' },
{ user_id: 3, thumbnail_url: 'thumbnail-3-{width}x{height}' }
];
const usersResponse = [
{ id: 1, profile_image_url: 'image-1' },
{ id: 2, profile_image_url: 'image-2' },
{ id: 3, profile_image_url: 'image-3' }
];
const store = mockStore();
// Mock the first request by URL.
moxios.stubRequest('https://api.twitch.tv/helix/streams?first=5', {
status: 200,
response: { data: streamsResponse }
});
// Mock the second request.
moxios.stubRequest('https://api.twitch.tv/helix/users?id=1&id=2&id=3', {
status: 200,
response: { data: usersResponse }
});
return store.dispatch(fetchTopStreamsStartAsync()).then(() => {
expect(store.getActions()).toEqual([
fetchTopStreamsStart(),
{
"type": "TOP_STREAMS_SUCCESS",
"payload": [
{ "avatar": "image-1", "thumbnail": "thumbnail-1-1280x720", "user_id": 1 },
{ "avatar": "image-2", "thumbnail": "thumbnail-2-1280x720", "user_id": 2 },
{ "avatar": "image-3", "thumbnail": "thumbnail-3-1280x720", "user_id": 3 },
]
}
]);
});
});
Note that I have made up the structure of the fetchTopStreamsSuccess action to have a type attribute equal to TOP_STREAMS_SUCCESS and to have an attribute payload with the completeStreams data. You will probably have to accommodate that to the real structure of the fetchTopStreamsSuccess action you are creating for the test to pass.

Redux store is not updated during integration test

I'm trying to do integration tests for the React-Redux application using Jest and Enzyme. The app itself works correctly and the Redux store is updated correctly after component dispatching an action.
But when I run an integration test, the store has an initial value after the action being dispatched.
Here's my test file:
import SendMessageSendsay from '../utils/sendMessage'
import {testStore} from '../utils'
import {sendMessage } from '../redux/actions/messageActions'
jest.mock('../utils/sendMessage');
describe('sendMessage action', ()=> {
const message = {
subject: 'Message subject',
toEmail: 'receiver#fakegmail.com'
}
test('Send message: Should update store correctly', () => {
const store = testStore();
const referenceState = {
messages: {
messages: [
{
date: '2019-08-30T11:36:14.813Z',
subject: 'Message subject',
trackId: '95'
}
],
isSent: true,
prevToEmail: 'receiver#fakegmail.com'
}
}
SendMessageSendsay.mockResolvedValue({'track.id' : '95' })
store.dispatch(sendMessage(message))
const newState = store.getState();
console.log(newState);
expect(newState).toBe(referenceState);
})
});
And I get these results:
● sendMessage action › Send message: Should update store correctly
expect(received).toBe(expected) // Object.is equality
- Expected
+ Received
Object {
"messages": Object {
- "isSent": true,
- "messages": Array [
- Object {
- "date": "2019-08-30T11:36:14.813Z",
- "subject": "Message subject",
- "trackId": "95",
- },
- ],
- "prevToEmail": "receiver#fakegmail.com",
+ "isSent": false,
+ "messages": Array [],
+ "prevToEmail": "",
},
}
where received is the initial state in my reducer.
Here's my action:
export const sendMessage = (message) => (dispatch) => {
sendMessageSendsay(message)
.then((res) => dispatch(
{
type: SEND_MESSAGE,
message: {
date: new Date(),
subject: message.subject,
trackId: res['track.id']
},
prevToEmail: message.toEmail
}
))
}
And here's the reducer:
export const initialState = {
messages : [],
isSent: false,
prevToEmail: ''
}
export default (state = initialState, action) => {
switch (action.type) {
case SEND_MESSAGE:
{
return {
...state,
messages: [
...state.messages,
action.message
],
isSent: true,
prevToEmail: action.prevToEmail
};
}
Based on #HMR comment I modified the action to this:
export const sendMessage = (message) => (dispatch) => {
return new Promise((resolve) => {
sendMessageSendsay(message)
.then((res) => resolve(dispatch(
{
type: SEND_MESSAGE,
message: {
date: new Date(),
subject: message.subject,
trackId: res['track.id']
},
prevToEmail: message.toEmail
}
)))
})
}
And test file to this:
import SendMessageSendsay from '../utils/sendMessage'
import {testStore} from '../utils'
import {sendMessage } from '../redux/actions/messageActions'
jest.mock('../utils/sendMessage');
describe('sendMessage action', ()=> {
const message = {
subject: 'Message subject',
toEmail: 'receiver#fakegmail.com'
}
test('Send message: Should update store correctly', () => {
const store = testStore();
const referenceState = {
messages: {
messages: [
{
subject: 'Message subject',
trackId: '95'
}
],
isSent: true,
prevToEmail: 'receiver#fakegmail.com'
}
}
SendMessageSendsay.mockResolvedValue({'track.id' : '95' })
return store.dispatch(sendMessage(message))
.then(() => {
const newState = store.getState();
expect(newState).toMatchObject(referenceState);
})
})
});
I used toMatchObject() istead of toEqual() to work around timestamp.

Resources