0

In an Angular 9 service class I'm chaining a couple of HTTP calls using RxJS's concatMap, using the output of the first call as input the second:

getUserDetails(authorisationCode: string): Observable<UserInfo> {

this.getAuthorisationTokens(authorisationCode)
  .pipe(
    concatMap(authorisationTokens =>  this.getUserInfo(authorisationTokens.access_token)))
    .subscribe(
      (data: UserInfo) => {
        this.userInfo = data;
        console.log(JSON.stringify(this.userInfo));
        console.log(JSON.stringify(data));
      },
      (err: any) => console.log('Error getting user info details : ' + err),
      () => {
        console.log('Got user information: ' + this.userInfo);
      }
    );

return of(this.userInfo);
}

I want to return this.userInfo to a caller and my first naive idea was to wrap it in an Observable (return of(this.userInfo)) and call it like this:

export class LandingComponent implements OnInit {

  username: string;
  userFirstName: string;
  email: string;


  constructor(private route: ActivatedRoute, private userService: UserDataService) {
    this.authorisationCode = route.snapshot.queryParamMap.get('code');
    console.log('Code was ' + this.authorisationCode);
  }

  ngOnInit(): void {

    this.userService.getUserDetails(this.authorisationCode)
      .subscribe((data: UserInfo) => {
        this.userFirstName = data.given_name;
        this.username = data.preferred_username;
        this.email = data.email;

        console.log('Got: ' + this.userFirstName + ', ' + this.username + ', ' + this.email);
      });

  }
}

I can see from the browser console that the calls to my services are succeeding and populating my object this.userInfo, but only after I've tried to use it and got an undefined error:

Code was 2ffa40f9-5e71-4f29-8ddd-318e8d0b99bc
main-es2015.8df8d853b157ca70b40a.js:1 Getting authorisation tokens in exchange for authorisation code 2ffa40f9-5e71-4f29-8ddd-318e8d0b99bc
main-es2015.8df8d853b157ca70b40a.js:1 Header: [object Object]
main-es2015.8df8d853b157ca70b40a.js:1 Body: grant_type=authorization_code&redirect_uri=https://xxx/landing/&client_id=xxx&code=2ffa40f9-5e71-4f29-8ddd-318e8d0b99bc&client_secret=xxx
main-es2015.8df8d853b157ca70b40a.js:1 TOKEN endpoint: https://xxx.amazoncognito.com/oauth2/token

TOKEN endpoint: https://xxx.amazoncognito.com/oauth2/token
main-es2015.8df8d853b157ca70b40a.js:1 ERROR TypeError: Cannot read property 'given_name' of undefined

    ...

USERINFO endpoint https://xxx.amazoncognito.com/oauth2/userInfo
main-es2015.8df8d853b157ca70b40a.js:1 USERINFO endpoint https://xxx.amazoncognito.com/oauth2/userInfo
main-es2015.8df8d853b157ca70b40a.js:1 {"sub":"4bfd88a4-5439-4ad6-a399-71b02034dfa1","email_verified":"true","given_name":"Craig","family_name":"Caulfield","email":"[email protected]","username":"4bfd88a4-5439-4ad6-a399-xxx"}
main-es2015.8df8d853b157ca70b40a.js:1 Got user information: [object Object]

I've tried to apply the following questions, but I can't find a way to apply them to my case:

So, my async thinking is wrong. Is there something obvious that I haven't done?

2 Answers 2

1

You could be returning undefined because this.userInfo is assigned value asynchronously. One correct way would be to return the HTTP observable and subscribe to it in the controller.

I've also moved extracting the authorisationCode from the constructor to the ngOnInit() hook. While usually the ngOnInit() hook will be triggered after the constructor is called, it can't be guaranteed that the variable this.authorisationCode would have been assigned the value by the time the hook is triggered.

Try the following

Service

getUserDetails(authorisationCode: string): Observable<any> {
  return this.getAuthorisationTokens(authorisationCode)
    .pipe(
      concatMap(authorisationTokens =>  this.getUserInfo(authorisationTokens.access_token))
    );
}

Controller

constructor(private route: ActivatedRoute, private userService: UserDataService) { }

ngOnInit(): void {
  const authorisationCode = route.snapshot.queryParamMap.get('code');
  this.userService.getUserDetails(authorisationCode).subscribe(
    (data: UserInfo) => {
      this.userFirstName = data.given_name;
      this.username = data.preferred_username;
      this.email = data.email;

      console.log('Got: ' + this.userFirstName + ', ' + this.username + ', ' + this.email);
    },
    (err: any) => { console.log('Error getting user info details : ' + err) },
    () => { console.log('Got user information: ' + data); }
  );
}
Sign up to request clarification or add additional context in comments.

2 Comments

It worked beautifully. So, the pattern seems to be: perform the subscribe close to where you'll be using the data?
Any data/function that directly depend on the asynchronous data should be assigned/called inside the subscription. You could refer here for more info on asynchronous data: stackoverflow.com/a/14220323/6513921
0

When you access this.UserInfo via the observable for the first time it is undefined. You need to wait until the API returns or you have to ignore the first value emitted by observable creted by of(this.userUnfo) (e.g.: apply skip(1) operator). A better solution wohl be to fire the this.user observable only after the API returns. You can create a subject, which fires once the API completes, and instead of returning of(this.userInfo) yiu simply create the observable from zhe above mentiined subject (e.g.: user ToObservable() operation)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.