I have two files contact.js and functions.js. I am using firestore realtime functionality.
Here is my functions.js file code:
export const getUserContacts = () => {
const contactDetailsArr = [];
return db.collection("users").doc(userId)
.onSnapshot(docs => {
const contactsObject = docs.data().contacts;
for (let contact in contactsObject) {
db.collection("users").doc(contact).get()
.then(userDetail => {
contactDetailsArr.push({
userId: contact,
lastMessage: contactsObject[contact].lastMsg,
time: contactsObject[contact].lastMsgTime,
userName:userDetail.data().userName,
email: userDetail.data().emailId,
active: userDetail.data().active,
img: userDetail.data().imageUrl,
unreadMsg:contactsObject[contact].unreadMsg
})
})
}
console.log(contactDetailsArr);
return contactDetailsArr;
})
}
in contact.js when I do:
useEffect(() => {
let temp = getUserContacts();
console.log(temp);
}, [])
I want to extract data of contactDetailsArr in contacts.js but I get the value of temp consoled as:
ƒ () {
i.Zl(), r.cs.ws(function () {
return Pr(r.q_, o);
});
}
How do I extract the array data in my case?
The onSnapshot() returns a function that can be used to detach the Firestore listener. When using a listener, it's best to set the data directly into state rather than returning something from that function. Try refactoring the code as shown below:
const [contacts, setContacts] = useState([]);
useEffect(() => {
const getUserContacts = () => {
const contactDetailsArr = [];
const detach = db.collection("users").doc(userId)
.onSnapshot(docs => {
const contactsObject = docs.data().contacts;
const contactsSnap = await Promise.all(contactsObject.map((c) => db.collection("users").doc(c).get()))
const contactDetails = contactsSnap.map((d) => ({
id: d.id,
...d.data()
// other fields like unreadMsg, time
}))
// Update in state
setContacts(contactDetails);
})
}
getUserContacts();
}, [])
Then use contacts array to map data in to UI directly.
Assumptions
This answer assumes a user's data looks like this in your Firestore:
// Document at /users/someUserId
{
"active": true,
"contacts": {
"someOtherUserId": {
"lastMsg": "This is a message",
"lastMsgTime": /* Timestamp */,
"unreadMsg": true // unclear if this is a boolean or a count of messages
},
"anotherUserId": {
"lastMsg": "Hi some user! How are you?",
"lastMsgTime": /* Timestamp */,
"unreadMsg": false
}
},
"emailId": "someuser#example.com",
"imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
"userName": "Some User"
}
Note: When asking questions in the future, please add examples of your data structure similar to the above
Attaching Listeners with Current Structure
The structure as shown above has a number of flaws. The "contacts" object in the user's data should be moved to a sub-collection of the user's main document. The reasons for this include:
Any user can read another user's (latest) messages (which can't be blocked with security rules)
Any user can read another user's contacts list (which can't be blocked with security rules)
As an individual user messages more users, their user data will grow rapidly in size
Each time you want to read a user's data, you have to download their entire message map even if not using it
As you fill out a user's contacts array, you are fetching their entire user data document even though you only need their active, email, imageUrl, and userName properties
Higher chance of encountering document write conflicts when two users are editing the contact list of the same user (such as when sending a message)
Hard to (efficiently) detect changes to a user's contact list (e.g. new addition, deletion)
Hard to (efficiently) listen to changes to another user's active status, email, profile image and display name as the listeners would be fired for every message update they receive
To fetch a user's contacts once in your functions.js library, you would use:
// Utility function: Used to hydrate an entry in a user's "contacts" map
const getContactFromContactMapEntry = (db, [contactId, msgInfo]) => {
return db.collection("users")
.doc(contactId)
.get()
.then((contactDocSnapshot) => {
const { lastMsg, lastMsgTime, unreadMsg, userName } = msgInfo;
const baseContactData = {
lastMessage: lastMsg,
time: lastMsgTime,
unreadMsg,
userId: contactId
}
if (!contactDocSnapshot.exists) {
// TODO: Decide how to handle unknown/deleted users
return {
...baseContactData,
active: false, // deleted users are inactive, nor do they
email: null, // have an email, image or display name
img: null,
userName: "Deleted user"
};
}
const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
return {
...baseContactData,
active,
email: emailId,
img: imageUrl,
userName
};
});
};
export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
return db.collection("users")
.doc(userId)
.get()
.then((userDataSnapshot) => {
const contactsMetadataMap = userDataSnapshot.get("contacts");
return Promise.all( // <-- waits for each Promise to complete
Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
.map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
});
}
Example Usage:
getUserContacts(db, userId)
.then((contacts) => console.log("Contacts data:", contacts))
.catch((err) => console.error("Failed to get contacts:", err))
// OR
try {
const contacts = await getUserContacts(db, userId);
console.log("Contacts data:", contacts);
} catch (err) {
console.error("Failed to get contacts:", err)
}
To fetch a user's contacts, and keep the list updated, using a function in your functions.js library, you would use:
// reuse getContactFromContactMapEntry as above
export const useUserContacts = (db, userId) => {
if (!db) throw new TypeError("Parameter 'db' is required");
const [userContactsData, setUserContactsData] = useState({ loading: true, contacts: [], error: null });
useEffect(() => {
// no user signed in?
if (!userId) {
setUserContactsData({ loading: false, contacts: [], error: "No user signed in" });
return;
}
// update loading status (as needed)
if (!userContactsData.loading) {
setUserContactsData({ loading: true, contacts: [], error: null });
}
let detached = false;
const detachListener = db.collection("users")
.doc(userId)
.onSnapshot({
next: (userDataSnapshot) => {
const contactsMetadataMap = userDataSnapshot.get("contacts");
const hydrateContactsPromise = Promise.all( // <-- waits for each Promise to complete
Object.entries(contactsMetadataMap) // <-- used to get an array of id-value pairs that we can iterate over
.map(getContactFromContactMapEntry.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
hydrateContactsPromise
.then((contacts) => {
if (detached) return; // detached already, do nothing.
setUserContactsData({ loading: false, contacts, error: null });
})
.catch((err) => {
if (detached) return; // detached already, do nothing.
setUserContactsData({ loading: false, contacts: [], error: err });
});
},
error: (err) => {
setUserContactsData({ loading: false, contacts: [], error: err });
}
});
return () => {
detached = true;
detachListener();
}
}, [db, userId])
}
Note: The above code will not (due to complexity):
react to changes in another user's active status, email or profile image
properly handle when the setUserContactsData method is called out of order due to network issues
handle when db instance is changed on every render
Example Usage:
const { loading, contacts, error } = useUserContacts(db, userId);
Attaching Listeners with Sub-collection Structure
To restructure your data for efficiency, your structure would be updated to the following:
// Document at /users/someUserId
{
"active": true,
"emailId": "someuser#example.com",
"imageUrl": "https://firebasestorage.googleapis.com/b/bucket/o/images%20stars.jpg",
"userName": "Some User"
}
// Document at /users/someUserId/contacts/someOtherUserId
{
"lastMsg": "This is a message",
"lastMsgTime": /* Timestamp */,
"unreadMsg": true // unclear if this is a boolean or a count of messages
}
// Document at /users/someUserId/contacts/anotherUserId
{
"lastMsg": "Hi some user! How are you?",
"lastMsgTime": /* Timestamp */,
"unreadMsg": false
}
Using the above structure provides the following benefits:
Significantly better network performance when hydrating the contacts list
Security rules can be used to ensure users can't read each others contacts lists
Security rules can be used to ensure a message stays private between the two users
Listening to another user's profile updates can be done without reading or being notified of any changes to their other private messages
You can partially fetch a user's message inbox rather than the whole list
The contacts list is easy to update as two users updating the same contact entry is unlikely
Easy to detect when a user's contact entry has been added, deleted or modified (such as receiving a new message or marking a message read)
To fetch a user's contacts once in your functions.js library, you would use:
// Utility function: Merges the data from an entry in a user's "contacts" collection with that user's data
const mergeContactEntrySnapshotWithUserSnapshot = (contactEntryDocSnapshot, contactDocSnapshot) => {
const { lastMsg, lastMsgTime, unreadMsg } = contactEntryDocSnapshot.data();
const baseContactData = {
lastMessage: lastMsg,
time: lastMsgTime,
unreadMsg,
userId: contactEntryDocSnapshot.id
}
if (!contactDocSnapshot.exists) {
// TODO: Handle unknown/deleted users
return {
...baseContactData,
active: false, // deleted users are inactive, nor do they
email: null, // have an email, image or display name
img: null,
userName: "Deleted user"
};
}
const { active, emailId, imageUrl, userName } = contactDocSnapshot.data();
return {
...baseContactData,
active,
email: emailId,
img: imageUrl,
userName
};
}
// Utility function: Used to hydrate an entry in a user's "contacts" collection
const getContactFromContactsEntrySnapshot = (db, contactEntryDocSnapshot) => {
return db.collection("users")
.doc(contactEntry.userId)
.get()
.then((contactDocSnapshot) => mergeContactEntrySnapshotWithUserSnapshot(contactEntryDocSnapshot, contactDocSnapshot));
};
export const getUserContacts = (db, userId) => { // <-- note that db and userId are passed in
return db.collection("users")
.doc(userId)
.collection("contacts")
.get()
.then((userContactsQuerySnapshot) => {
return Promise.all( // <-- waits for each Promise to complete
userContactsQuerySnapshot.docs // <-- used to get an array of entry snapshots that we can iterate over
.map(getContactFromContactsEntrySnapshot.bind(null, db)); // for each contact, call the function (reusing db), returning a Promise with the data
);
});
}
Example Usage:
getUserContacts(db, userId)
.then((contacts) => console.log("Contacts data:", contacts))
.catch((err) => console.error("Failed to get contacts:", err))
// OR
try {
const contacts = await getUserContacts(db, userId);
console.log("Contacts data:", contacts);
} catch (err) {
console.error("Failed to get contacts:", err)
}
To fetch a user's contacts in a way where it's kept up to date, we first need to introduce a couple of utility useEffect wrappers (there are libraries for more robust implementations):
export const useFirestoreDocument = ({ db, path }) => {
if (!db) throw new TypeError("Property 'db' is required");
const [documentInfo, setDocumentInfo] = useState({ loading: true, snapshot: null, error: null });
useEffect(() => {
if (!path) {
setDocumentInfo({ loading: false, snapshot: null, error: "Invalid path" });
return;
}
// update loading status (as needed)
if (!documentInfo.loading) {
setDocumentInfo({ loading: true, snapshot: null, error: null });
}
return db.doc(path)
.onSnapshot({
next: (docSnapshot) => {
setDocumentInfo({ loading: false, snapshot, error: null });
},
error: (err) => {
setDocumentInfo({ loading: false, snapshot: null, error: err });
}
});
}, [db, path]);
return documentInfo;
}
export const useFirestoreCollection = ({ db, path }) => {
if (!db) throw new TypeError("Property 'db' is required");
const [collectionInfo, setCollectionInfo] = useState({ loading: true, docs: null, error: null });
useEffect(() => {
if (!path) {
setCollectionInfo({ loading: false, docs: null, error: "Invalid path" });
return;
}
// update loading status (as needed)
if (!collectionInfo.loading) {
setCollectionInfo({ loading: true, docs: null, error: null });
}
return db.collection(path)
.onSnapshot({
next: (querySnapshot) => {
setCollectionInfo({ loading: false, docs: querySnapshot.docs, error: null });
},
error: (err) => {
setCollectionInfo({ loading: false, docs: null, error: err });
}
});
}, [db, path]);
return collectionInfo;
}
To use that method to hydrate a contact, you would call it from a ContactEntry component:
// mergeContactEntrySnapshotWithUserSnapshot is the same as above
const ContactEntry = ({ db, userId, key: contactId }) => {
if (!db) throw new TypeError("Property 'db' is required");
if (!userId) throw new TypeError("Property 'userId' is required");
if (!contactId) throw new TypeError("Property 'key' (the contact's user ID) is required");
const contactEntryInfo = useFirestoreDocument(db, `/users/${userId}/contacts/${contactId}`);
const contactUserInfo = useFirestoreDocument(db, `/users/${contactId}`);
if ((contactEntryInfo.loading && !contactEntryInfo.error) && (contactUserInfo.loading && !contactUserInfo.error)) {
return (<div>Loading...</div>);
}
const error = contactEntryInfo.error || contactUserInfo.error;
if (error) {
return (<div>Contact unavailable: {error.message}</div>);
}
const contact = mergeContactEntrySnapshotWithUserSnapshot(contactEntryInfo.snapshot, contactUserInfo.snapshot);
return (<!-- contact content here -->);
}
Those ContactEntry components would be populated from a Contacts component:
const Contacts = ({db}) => {
if (!db) throw new TypeError("Property 'db' is required");
const { user } = useFirebaseAuth();
const contactsCollectionInfo = useFirestoreCollection(db, user ? `/users/${user.uid}/contacts` : null);
if (!user) {
return (<div>Not signed in!</div>);
}
if (contactsCollectionInfo.loading) {
return (<div>Loading contacts...</div>);
}
if (contactsCollectionInfo.error) {
return (<div>Contacts list unavailable: {contactsCollectionInfo.error.message}</div>);
}
const contactEntrySnapshots = contactsCollectionInfo.docs;
return (
<>{
contactEntrySnapshots.map(snapshot => {
return (<ContactEntry {...{ db, key: snapshot.id, userId: user.uid }} />);
})
}</>
);
}
Example Usage:
const db = firebase.firestore();
return (<Contacts db={db} />);
Your code seems to be not written with async/await or promise like style
e.g. contactDetailsArr will be returned as empty array
also onSnapshot creates long term subscription to Firestore collection and could be replaced with simple get()
See example on firestore https://firebase.google.com/docs/firestore/query-data/get-data#web-version-9_1
I'm attempting to allow each user read and write their own data using firestore, but I'm getting an insufficient permissions error. I'm not sure why.
I have these rules in place for my firestore...
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
allow create: if request.auth != null;
allow read, write, update, delete: if request.auth != null && request.auth.uid == uid;
}
}
}
In my project, I have my service that uses the following function to push the data to angular firebase (yes, it's pretty lengthy)...
constructor(private afs: AngularFirestore){}
addToOrders(artist: string, formInput: AlbumInput) {
const currentUser = this.authService.currentUser; // uses a getter function to obtain the current user
const trackingUrl = 'https://tools.usps.com/go/TrackConfirmAction_input?strOrigTrackNum=';
const newOrder: Order = {
artistName: artist,
album: formInput.selectedAlbum.name,
image: formInput.selectedAlbum.images[0].url,
orderType: formInput.orderType,
trackingUrl: trackingUrl,
variant: formInput.variant
}
if (formInput.orderType === "shipped") {
newOrder.trackingNum = formInput.trackingNum;
return of(this.afs.doc(`users/${currentUser.uid}`).collection('shipped').add(newOrder))
.subscribe({
next: (() => {
this.albumAdded$.next(true);
}),
error: (() => {
this.albumAdded$.next(false);
})
});
} else {
newOrder.date = formInput.date;
return of(this.afs.doc(`users/${currentUser.uid}`).collection('preordered').add(newOrder))
.subscribe({
next: (() => {
this.albumAdded$.next(true);
}),
error: (() => {
this.albumAdded$.next(false);
})
});
}
}
Is there anything I'm missing in this pattern that would cause such an error?
If I change the rules to users/${user=**}, it does successfully store the data into the users subcollections, but now I can't sign in normally (for some reason, I can sign up despite the methods being nearly identical). Here is my sign in...
signIn(signInForm: SignInForm) {
return this.afAuth.signInWithEmailAndPassword(signInForm.email, signInForm.password)
.then((result) => {
this.isUserData.next(true);
this.setUserData(result.user!)
.then(() => {
this.router.navigateByUrl("/home");
});
}).catch(error => {
this.errorModal(error); // Modal Generic launches to inform the user
});
}
set user data...
setUserData(user: User) {
const userRef: AngularFirestoreDocument<any> = this.afs.doc(`users/${user.uid}`);
const userData: User = {
uid: user.uid,
email: user.email,
displayName: user.displayName
}
return userRef.set(userData, {
merge: true
});
}
This rule:
match /users/{uid} {
Allows a user to read their own profile document. It does not allow them to read subcollections under there, which is what you do in this code:
of(this.afs.doc(`users/${currentUser.uid}`).collection('shipped').add(newOrder))
to allow a user to also read all subcollections of their profile document, use a recursive wildcard (**):
match /users/{uid=**} {
Using TypeScript and Angular, I am trying to create an empty array within my testConnection function that allows the addition of objects every time the function is called, without clearing the array.
testConnection function:
testConnection(system) {
var systemList = [];
this.service.testConnection(system)
.subscribe(conn => {
if (conn == 'Success') {
this.snackBarHandler.open('Connection Found', 'success');
system.isClicked = false;
system.connection = true;
systemList.push(system);
}
else {
this.snackBarHandler.open('Connection Failed', 'failure');
system.isClicked = false;
system.connection = false;
systemList.push(system);
}
}, err => console.log(err));
}
Currently, with the logic I have, it is adding the system object to the array, but since the empty array declaration is within the function, it is clearing and restarting this process every time the function is called. I have tried to declare the systemList at the top of the class (systemList = any[]), but when I try to reference it within the function, it is showing up as undefined.
How am I able to add system objects to the array any time the function is called, without clearing the existing objects out of the array?
You should be able to set a component variable outside of the function, then the list will persist.
export class SystemComponent implements OnInit {
constructor(private service: TestConnectionService) {}
systemList = []
testConnection(system) {
this.service.testConnection(system)
.subscribe(conn => {
this.systemList.push(system);
if (conn === 'Success') {
this.snackBarHandler.open('Connection Found', 'success');
system.isClicked = false;
system.connection = true;
}
else {
this.snackBarHandler.open('Connection Failed', 'failure');
system.isClicked = false;
system.connection = false;
}
}, err => console.log(err));
}
ngOnInit(): void {}
}
Another option is to create a service and use the service to hold the state of the system list which is more useful if you want multiple components to access the systemList.
interface System {
some: string
props: string
}
#Injectable({ providedIn: 'root' })
export class SystemService {
private _systemList$$ = new BehaviorSubject<System[]>([])
get systemList(): System[] {
return this._systemList$$.value
}
addToSystemList(system: System) {
this._systemList$$.next([...this.systemList, system])
}
constructor() {}
}
then your testConnection function would use it like this.
testConnection(system) {
this.service.testConnection(system).subscribe(
conn => {
this.systemService.addToSystemList(system)
if (conn === 'Success') {
this.snackBarHandler.open('Connection Found', 'success')
system.isClicked = false
system.connection = true
} else {
this.snackBarHandler.open('Connection Failed', 'failure')
system.isClicked = false
system.connection = false
}
},
err => console.log(err)
)
}
Excuse my ignorance, I am fairly new to the reactive concepts.
My issue is with not knowing how to deal loading a Ionic 2 loader or an Ionic 2 alert based on the stores current state.
I have been able to achieve the loader behaviour I need by subscribing to the store slice it is reacting to. Although when it comes to an alert (thrown on a catched error), it never fires in the subscription block.
Any help pointing out a better direction, or what I have missed would be greatly appreciated.
This code is from the signin modals view.
signin(user) {
this.submitAttempt = true;
if (this.signinForm.valid) {
let loader = this.loadingCtrl.create({
content: "Signing In..."
});
let auth;
let signinSub = this.store.select(s => auth = s.auth).subscribe(() => {
if (auth.state) {
loader.dismiss();
} else if (auth.error) {
let alert = this.alertCtrl.create({
title: "Error",
subTitle: auth.error,
buttons: ['OK']
});
loader.dismiss();
alert.present();
}
});
loader.present();
this.store.dispatch(UserActions.UserActions.signinUser(user));
}
}
Effect
#Effect() signinUser$ = this.actions$
.ofType(UserActions.ActionTypes.SIGNIN_USER)
.map(toPayload)
.switchMap(user => {
return Observable.fromPromise(this.userService.signinUser(user))
.map(result => {
return ({ type: "GET_USER", payload: user});
})
.catch(err => {
return Observable.of({ type: "SIGNIN_USER_FAILED", payload: err });
});
});
Service
signinUser(user): Promise<any> {
return <Promise<any>>firebase.auth()
.signInWithEmailAndPassword(user.email, user.password);
}
Reducer
export const UserReducer: ActionReducer<Auth> = (state: Auth = initialState, action: Action) => {
switch(action.type) {
case UserActions.ActionTypes.SIGNIN_USER:
return state;
case UserActions.ActionTypes.SIGNIN_USER_FAILED:
return Object.assign(state, { apiState: "Failed", error: action.payload.message });
case UserActions.ActionTypes.STARTED_SIGNIN:
return Object.assign(state, { requested: true });
case UserActions.ActionTypes.GET_USER:
return Object.assign(state, { apiState: "Success", error: ""});
case UserActions.ActionTypes.GET_USER_SUCCESS:
return Object.assign({ user: action.payload.val() }, state, { state: true });
default:
return state;
};
}
store
export interface Auth {
state: boolean,
requested: boolean,
apiState: string,
error: {},
user?: {}
}
export interface AppState {
auth: Auth;
}
I just have a loadingState in my store and then I load and unload the spinner/loading UI based on that state.
I have a complete project here showing how I manage the state and the UI
https://github.com/aaronksaunders/ngrx-simple-auth
/**
* Keeping Track of the AuthenticationState
*/
export interface AuthenticationState {
inProgress: boolean; // are we taking some network action
isLoggedIn: boolean; // is the user logged in or not
tokenCheckComplete: boolean; // have we checked for a persisted user token
user: Object; // current user | null
error?: Object; // if an error occurred | null
}
and then in the different states, AuthActions.LOGIN
case AuthActions.LOGIN: {
return Object.assign({}, state, {inProgress: true, isLoggedIn: false, error: null})
}
and then, AuthActions.LOGIN_SUCCESS
case AuthActions.LOGIN_SUCCESS: {
return Object.assign({}, state, {inProgress: false, user: action.payload, isLoggedIn: true})
}
here is how we handle it in the LoginPage
var dispose = this.store.select('authReducer').subscribe(
(currentState: AuthenticationState) => {
console.log("auth store changed - ", currentState);
if (currentState.user) {
dispose.unsubscribe();
this.nav.setRoot(HomePage, {});
}
// this is where the magic happens...
this.handleProgressDialog(currentState);
this.error = currentState.error
},
error => {
console.log(error)
}
);
}
how we handle loading
/**
*
* #param _currentState
*/
handleProgressDialog(_currentState) {
if (_currentState.inProgress && this.loading === null) {
this.loading = this.loadingCtrl.create({
content: "Logging In User..."
});
this.loading.present()
}
if (!_currentState.inProgress && this.loading !== null) {
this.loading && this.loading.dismiss();
this.loading = null;
}
}
I use Ionic 2 with ngrx too and so far as I know, LoadingController and AlertController don't provide any observable or promise. So I think the best you can do is what you're doing now by subscribing its state and do some condition based on its state.
OR you can get rid LoadingController replace it with ion-spinner:
<ion-spinner [disabled]="isLoading$ | async"></ion-spinner>
And replace AlertController with some label :
<span>{{errorMessage$ | async}}</span>
Below is my code, I want login() and authenticated() functions to wait for getProfile() function to finish its execution. I tried several ways like promise etc. but I couldn't implement it. Please suggest me the solution.
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
// Avoid name not found warnings
declare var Auth0Lock: any;
#Injectable()
export class Auth {
// Configure Auth0
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, {
additionalSignUpFields: [{
name: "address", // required
placeholder: "enter your address", // required
icon: "https://example.com/address_icon.png", // optional
validator: function(value) { // optional
// only accept addresses with more than 10 chars
return value.length > 10;
}
}]
});
//Store profile object in auth class
userProfile: any;
constructor() {
this.getProfile(); //I want here this function to finish its work
}
getProfile() {
// Set userProfile attribute if already saved profile
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on("authenticated", (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
// Fetch profile information
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
// Handle error
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
});
});
};
public login() {
this.lock.show();
this.getProfile(); //I want here this function to finish its work
};
public authenticated() {
this.getProfile(); //I want here this function to finish its work
return tokenNotExpired();
};
public logout() {
// Remove token and profile from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
};
}
Like you saw in the comments, you have to use Promise or Observable to achieve this, since your behaviour is pretty simple, you should use Promise because Observable will have a lot of features you don't need in this case.
Here is the Promise version of your service:
import { Injectable } from '#angular/core';
import { tokenNotExpired } from 'angular2-jwt';
import { myConfig } from './auth.config';
// Avoid name not found warnings
declare var Auth0Lock: any;
#Injectable()
export class Auth {
// Configure Auth0
lock = new Auth0Lock(myConfig.clientID, myConfig.domain, {
additionalSignUpFields: [{
name: "address", // required
placeholder: "enter your address", // required
icon: "https://example.com/address_icon.png", // optional
validator: function(value) { // optional
// only accept addresses with more than 10 chars
return value.length > 10;
}
}]
});
//Store profile object in auth class
userProfile: any;
constructor() {
this.getProfile(); //I want here this function to finish its work
}
getProfile():Promise<void> {
return new Promise<void>(resolve => {
// Set userProfile attribute if already saved profile
this.userProfile = JSON.parse(localStorage.getItem('profile'));
// Add callback for lock `authenticated` event
this.lock.on("authenticated", (authResult) => {
localStorage.setItem('id_token', authResult.idToken);
// Fetch profile information
this.lock.getProfile(authResult.idToken, (error, profile) => {
if (error) {
// Handle error
alert(error);
return;
}
profile.user_metadata = profile.user_metadata || {};
localStorage.setItem('profile', JSON.stringify(profile));
this.userProfile = profile;
resolve()
});
});
})
};
public login(): Promise<void>{
this.lock.show();
return this.getProfile(); //I want here this function to finish its work
};
public authenticated():void{
this.getProfile().then( () => {
return tokenNotExpired();
});
};
public logout():void {
// Remove token and profile from localStorage
localStorage.removeItem('id_token');
localStorage.removeItem('profile');
this.userProfile = undefined;
};
}
More on Promise here
I would recommend that you set up getProfile to return an observable. Then your other functions can subscribe to that function and do their actions in the subscribe function. The Angular 2 HTTP tutorial gives an example of this