I'm not sure how to write this question so please let me know if you need any more information.
I'm building an online shop web application. I'm coding the login functionality using Google's Firebase authorisation API.
It logs in fine and inside the: user.getToken().then function, I can console.log the displayName just fine. If it was a static webpage, i could append it to the DOM too.
Inside react/webpack though, I want to set the displayName string as my usernameState state. It doesn't work, when I run this.setState inside the user.getToken().then function, it fires an error, see below.
I would like to be able to use that value on the webpage/react states.
constructor(){
super();
this.state = {
usernameState: ""
};
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
var displayName = user.displayName;
var email = user.email;
var emailVerified = user.emailVerified;
var photoURL = user.photoURL;
var uid = user.uid;
var providerData = user.providerData;
user.getToken().then(function(accessToken) {
console.log(displayName);
this.setState({
usernameState:JSON.stringify(displayName) //cannot read property 'setState' of undefined
});
});
} else {
// User is signed out.
console.log("signed out");
}
}, function(error) {
console.log(error);
});
};
make a bind or save real this
constructor(){
super();
this.state = {
usernameState: ""
};
const self=this;
...
self.setState(...)
...
}
Related
I am kind of new to angular 2 so ill try to explain the requirement in details.
The app I build has a login page (/login) and has the settings page (/settings).
when the user access the login page the gapi var is initialized properly then the user logs in to the app.
Once the user is in he has the settings page. the issue starts when the user refresh the page, when that happens the gapi var is no longer recognized and become undefined. My though is that the gapi library is not getting loaded and therefore it fails.
I placed the following code in the app index.html file
<script type="text/javascript">
// Client ID and API key from the Developer Console
var CLIENT_ID = '***.apps.googleusercontent.com';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = 'https://www.googleapis.com/auth/gmail.readonly';
/**
* On load, called to load the auth2 library and API client library.
*/
function handleClientLoad() {
console.log("handleClientLoad")
gapi.load('client:auth2', initClient);
}
/**
* Initializes the API client library and sets up sign-in state
* listeners.
*/
function initClient() {
gapi.client.init({
discoveryDocs: DISCOVERY_DOCS,
clientId: CLIENT_ID,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
console.log("client init");
gapi.auth2.getAuthInstance().isSignedIn.listen();
// Handle the initial sign-in state.
//updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
});
}
</script>
<script async defer src="https://apis.google.com/js/api.js"
onload=this.onload=function(){};handleClientLoad();
onreadystatechange="if (this.readyState === 'complete') this.onload()";>
</script>
To conclude how can I properly load the gapi module to handle the above refresh scenario?
I tried to work with the solution from Best way to wait for 3rd-party JS library to finish initializing within Angular 2 service? however it didnt work, gapi is still undefined.
What I've done is create a custom GoogleService that is responsible for initializing the GAPI client in Angular apps. Instead of interacting with the GAPI client directly, my app interacts with the GoogleService.
For example (using Angular 9.x)
import { Injectable } from '#angular/core';
import { environment } from '../environments/environment';
#Injectable({
providedIn: 'root',
})
export class GoogleService {
private gapiAuth?: Promise<gapi.auth2.GoogleAuth>;
constructor() {
// Chrome lets us load the SDK on demand, but firefox will block the popup
// when loaded on demand. If we preload in the constructor,
// then firefox won't block the popup.
this.googleSDK();
}
async signinGoogle() {
const authClient = (await this.googleSDK()) as gapi.auth2.GoogleAuth;
const googleUser = await authClient.signIn();
const profile = googleUser.getBasicProfile();
return {
type: 'GOOGLE',
token: googleUser.getAuthResponse().id_token as string,
uid: profile.getId() as string,
firstName: profile.getGivenName() as string,
lastName: profile.getFamilyName() as string,
photoUrl: profile.getImageUrl() as string,
emailAddress: profile.getEmail() as string,
};
}
async grantOfflineAccess() {
const authClient: gapi.auth2.GoogleAuth = (await this.googleSDK()) as any;
try {
const { code } = await authClient.grantOfflineAccess();
return code;
} catch (e) {
// access was denied
return null;
}
}
// annoyingly there is some sort of bug with typescript or the `gapi.auth2`
// typings that seems to prohibit awaiting a promise of type `Promise<gapi.auth2.GoogleAuth>`
// https://stackoverflow.com/questions/54299128/type-is-referenced-directly-or-indirectly-in-the-fulfillment-callback-of-its-own
private googleSDK(): Promise<unknown> {
if (this.gapiAuth) return this.gapiAuth;
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.defer = true;
script.src = 'https://apis.google.com/js/api.js?onload=gapiClientLoaded';
this.gapiAuth = new Promise<void>((res, rej) => {
(window as any)['gapiClientLoaded'] = res;
script.onerror = rej;
})
.then(() => new Promise(res => gapi.load('client:auth2', res)))
.then(() =>
gapi.client.init({
apiKey: environment.google.apiKey,
clientId: environment.google.clientId,
discoveryDocs: environment.google.discoveryDocs,
scope: environment.google.scopes.join(' '),
}),
)
.catch(err => {
console.error('there was an error initializing the client', err);
return Promise.reject(err);
})
.then(() => gapi.auth2.getAuthInstance());
document.body.appendChild(script);
return this.gapiAuth;
}
}
Logic: users select a few items on the homepage, then click a 'confirm' button which starts a Firebase $signInAnonymously() auth flow. Their selection is stored under their users/{uid} branch in the database tree and they are redirected to a checkout page that retrieves their selection and asks for more information to proceed.
Issue: when the user lands on the checkout page for the first time their auth state cannot be retrieved (so their selection doesn't appear). However when they refresh the page, everything works as expected for all subsequent attempts (their user info is stored and now retrievable)
Code:
User auth and selection setter/getter factory userService
var auth = $firebaseAuth();
var usersRef = $firebaseRef.users; // custom ref in app config
// Authenticate anonymously to create user session
function startSession() {
return auth.$signInAnonymously()
.then(function(user) {
return user;
})
.catch(function(error) {
console.log(error);
});
}
// Check authentication state before everything loads
function checkAuthState() {
return $q(function(resolve) {
var unsubscribe = auth.$onAuthStateChanged(function(user) {
if (user) {
unsubscribe();
resolve(user);
}
else {
console.log('User unidentified');
}
});
});
}
// Save user's menu selection into selection node of firebase
function saveSelection(items, user) {
var selectionRef = usersRef.child(user.uid).child('selection');
for (var i = 0, item; !!(item = items[i]); i++) {
var id = item.id;
if (item.selected) {
selectionRef.child(id).update(item);
}
else if (typeof(selectionRef.child(id)) !== 'undefined') {
selectionRef.child(id).remove();
}
}
}
// Get user's selection from firebase
function getSelection(user) {
var selectionRef = usersRef.child(user.uid).child('selection');
return $q(function(resolve) {
var selection = $firebaseArray(selectionRef);
resolve(selection);
});
}
Menu controller:
var menu = this;
menu.saveMenu = saveMenu;
// Save menu selection and create anonymous user on firebase
function saveMenu() {
var items = menu.items;
return userService.startSession()
.then(function(user) {
return userService.saveSelection(items, user);
});
}
Checkout controller:
// Get selected menu items from user data
function getCheckoutItems() {
return userService.checkAuthState()
.then(function(user) {
return userService.getSelection(user);
})
.then(function(selection) {
checkout.items = selection;
return checkout.items;
})
.catch(function(error) {
console.log(error);
});
}
I've looked through dozens of posts on SO before asking this. Here are a couple of the similar ones I've found:
Apparent race condition getting firebase.User from controller in Firebase 3.x
Handle asynchronous authentication in Firebase on page reload to get list that needs user's uid
I've also looked through the reference on GitHub to set it up:
https://github.com/firebase/angularfire/blob/master/docs/reference.md#firebaseauth
NB: I'm using the new version of Firebase with Angular 1.5.8:
"firebase": "^3.4.1",
"angularfire": "^2.0.2"
UPDATE
Got it. I had to add a resolve to my /checkout route to wait for authentication before loading elements on the page... Figured it out thanks to this answer by David East from the Firebase team.
resolve: {
// controller will not be loaded until $waitForSignIn resolves
"firebaseUser": function($firebaseAuthService) {
return $firebaseAuthService.$waitForSignIn();
}
}
I have been following a course on Firebase, but quickly discovered a lot of things have changed. https://www.firebase.com/docs/web/libraries/angular/api.html#angularfire-users-and-authentication-createusercredentials
these docs are completely outdated.
Here I declare my firebase variables
var ref = firebase.database().ref();
var firebasedata = $firebaseObject(ref);
var auth = $firebaseAuth();
Then I try to call this function
this.register = function() {
auth.$createUserWithEmailAndPassword({
email: this.user.email,
password: this.user.password
}).then(function(regUser) {
$log.log(regUser);
}).catch(function(error) {
$log.log(error);
});
};
But it throws this error:
code
:
"auth/argument-error"
message
:
"createUserWithEmailAndPassword failed: First argument "email" must be a valid string."
I have searched for answers to this problem but all I can find is updating my angularfire or firebase (which are both the latest version) or that this.user.email is not a string. When I do a
console.log(this.user.email);
I can see it is a string containing a valid e-mail.
Any help into the right direction would be welcome!
For anyone experiencing this issue:
the old
auth.$createUser()
function took an user-object as parameter.
While:
auth.$createUserWithEmailAndPassword()
takes 2 strings as parameters, not an object.
Change your code from
auth.$createUserWithEmailAndPassword({
email: this.user.email,
password: this.user.password
})
to
auth.createUserWithEmailAndPassword(user.email, user.password);
Change the function parameters as follows:
this.register = function() {
auth.$createUserWithEmailAndPassword(this.user.email,this.user.password).then(function(regUser) {
$log.log(regUser);
}).catch(function(error) {
$log.log(error);
});
};
$scope.register = function() {
firebase.auth().createUserWithEmailAndPassword($scope.email, $scope.password)
.then(function(user) {
console.log(user);
})
.catch(function(error) {
console.log(error);
});
//}
}
here, you can use it this way.
I am not able to figure out the sequence of code execution in react native. The following code logs test, test 2 and then test 1. I want to change value of submittedURI.
constructor(props) {
super(props);
this.state = {
enterURI: ''
};
}
onUriTextChanged(event) {
this.setState({ enterURI: event.nativeEvent.text });
}
onSubmitPress() {
var submittedURI = this.state.enterURI;
console.log("test "+submittedURI);
var url = encodeURIComponent(submittedURI), data = null;
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.urlmeta.org/?url=" + url);
xhr.send(data);
xhr.addEventListener("readystatechange", function () {
if (this.readyState === this.DONE) {
// console.log(this.responseText);
var responseJSON = JSON.parse(this.responseText);
submittedURI = responseJSON.meta.image;
console.log("test 1 "+submittedURI);
}
});
this.props.onURISubmit(submittedURI);
console.log("test 2 "+submittedURI);
}
The function in the xhr.addEventListner("readystatechange", function() { ... }) is a callback and get's called after the request is DONE. Therefore console.log("test 2 "+submittedURI); is called before the request is completed.
See the docs here (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/onreadystatechange)
If you want to change submittedURI you are going to have to call this.props.onURISubmit(submittedURI); function within the readystatechange callback. Make sure the callback has the proper context to access this.props.
I'm using Firebase 3.x and the auth state change event to assign the current user in my service -
.service('FireService', function ($q, $http,$state) {
var accessToken;
var user;
firebase.auth().onAuthStateChanged(function (usr) {
if (usr) {
// User signed in!
user = usr;
} else {
user = null;
$state.go('login');
}
});
return {
currentUser: function () {
return user;
}
}
But I need the current user to a reference to my child objects from the database in the controller
var getLog = function () {
var logRange = currentDate.format('MMMYYYY').toUpperCase();
var userId = FireService.currentUser().uid;
var ref = LogService.logs(userid,logRange);
// download the data into a local object
ref.once('value').then(
function (result) {
logObj = result;
})
};
When I set breakpoints, I see that the currentUser() is always empty because the state change hasn't fired yet. I've also tried to assign my userid directly from firebase.User but this is also not yet initialized by the time my controllers are ready. How do I get the currently logged in user so that it's available before I start routing around in my app?
I would recommend using the observer directly within the getLog method, so it waits until the current user is set:
var getLog = function () {
var unsubscribe = firebase.auth().onAuthStateChanged(function(user) {
if (user) {
var logRange = currentDate.format('MMMYYYY').toUpperCase();
var userId = user.uid;
var ref = LogService.logs(userid,logRange);
// download the data into a local object
ref.once('value').then(
function (result) {
logObj = result;
});
}
unsubscribe(); // So this observer is only triggered once.
});
};
As the ref.once('value') resolves asynchronously anyway, it will just add a bit of latency to the method but won't change its behavior.
Using this "one off" observer pattern (an observer that calls its unsubscribe function the first time it is called) allows you to make sure Firebase Auth is fully initialized before executing the code. If Firebase Auth is already initialized by the time you create the observer, then its resolution will be triggered immediately. Remember that observers are always called asynchronously.
If you end up using this pattern often, you could add a method on FireService:
...
ready: function() {
return $q(function(resolve, reject) {
var unsubscribe = firebase.auth().onAuthStateChanged(function(user) {
unsubscribe();
resolve(user);
});
});
}
...
And use it within your getLog method:
var getLog = function () {
FireService.ready().then(function(user) {
if (!user) { return; }
var logRange = currentDate.format('MMMYYYY').toUpperCase();
var userId = user.uid;
var ref = LogService.logs(userid,logRange);
// download the data into a local object
ref.once('value').then(
function (result) {
logObj = result;
});
});
};