AWS Amplify Authenication using pre-built UI components

Hi,

Having trouble setting up AWS Amplify Authenication using the pre-built UI component which automatically renders a sign in page in an Ionic / Angular app.

Having followed the guide here:

https://docs.amplify.aws/ui/auth/authenticator/q/framework/ionic

It tells you to replace the contents of app.component.html with the following:

<amplify-authenticator *ngIf="authState !== 'signedin'"></amplify-authenticator>

<div *ngIf="authState === 'signedin' && user" class="App">
    <amplify-sign-out></amplify-sign-out>
    <div>Hello, {{user.username}}</div>
    <!-- This is where you application template code goes -->  
</div>

Therefore I assumed that the previous contents:

<ion-app>
  <ion-router-outlet></ion-router-outlet>
</ion-app>

would be placed inside as below:

<amplify-authenticator *ngIf="authState !== 'signedin'"></amplify-authenticator>

<div *ngIf="authState === 'signedin' && user" class="App">
    <amplify-sign-out></amplify-sign-out>
    <div>Hello, {{user.username}}</div>
    <!-- This is where you application template code goes -->  
    <ion-app>
        <ion-router-outlet></ion-router-outlet>
    </ion-app>
</div>

Implementing the <amplify-authenticator> this way seems to interfere in how Ionic / Angular change detection works.

For example, a simple click function that changes some text in the home.page.html template no longer automatically updates the text.

home.page.ts

  someText: String = '';

  getSomeText(){
    this.someText = 'Some text to write out';
    console.log(this.someText);
  }

home.page.html

<button (click)="getSomeText()">Get some text</button>
<p>{{ someText }}</p>

If I switch on Chrome DevTools, the text change is detected as I guess the viewport size change triggers it???

If I move the <ion-app> back outside the <div> then the someText property will change on click of the button, as expected.

So where should the be placed when using the prebuilt UI from AWS Amplify?

Thanks.

here is how I handled it

home.page.html

<amplify-authenticator username-alias="email"> </amplify-authenticator>
<div style="height: 100%" *ngIf="user">
  <ion-header [translucent]="true">
    <ion-toolbar>
      <ion-title> Blank </ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-content class="ion-padding">
    <div>TASKS: {{ user?.attributes?.email }}</div>
    <ion-list>
      <div *ngFor="let t of ($tasks|async); let i = index">
        <ion-item>
          <ion-label>
            <p>{{ t?.title }}</p>
            <p>{{ t?.description }}</p>
            <p id="id">{{t?.id}};{{ t?.title }}</p>
          </ion-label>
        </ion-item>
      </div>
    </ion-list>
  </ion-content>
</div>
<ion-footer class="ion-padding">
  <amplify-sign-out></amplify-sign-out>
</ion-footer>

home.page.ts

import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { onAuthUIStateChange } from '@aws-amplify/ui-components';
import { DataStore, Auth } from 'aws-amplify';
import { Task } from '../../models';

import { defer } from 'rxjs';
@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnDestroy, OnInit {
  $tasks;

  private unsubscribeAuth;
  private curAuthState;
  private user;

  constructor(private ref: ChangeDetectorRef) {}
  ngOnInit(): void {

    this.unsubscribeAuth = onAuthUIStateChange((authState, authData) => {
      this.curAuthState = authState;
      this.user = authData || null;
      console.log(this.user);

      // to update the user state
      this.ref.detectChanges();

      if (this.user) {
        this.$tasks = defer(() => this.getData());
      }
    });
  }

  ngOnDestroy(): void {
    this.unsubscribeAuth();
  }

  async getData() {
    return await DataStore.query(Task);
  }
}

Thanks for the reply Aaron.

So far I’ve tried this after finding a post in the Github pages:

app.component.ts

import { Component, ChangeDetectorRef, NgZone } from '@angular/core';
import { onAuthUIStateChange, CognitoUserInterface, AuthState } from '@aws-amplify/ui-components';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {

  user: CognitoUserInterface | undefined;
  authState: AuthState;

  constructor(private ref: ChangeDetectorRef, private _ngZone: NgZone) {}

  ngOnInit() {
    onAuthUIStateChange((authState, authData) => {
      this.authState = authState;
      this.user = authData as CognitoUserInterface;
      if (!this.ref['destroyed']) {
        // Added the line below as bug in AWS Amplify documentation
        this._ngZone.run(() => this.ref.detectChanges());
        
        // Original
        // this.ref.detectChanges();
      }
    })
  }

  ngOnDestroy() {
    return onAuthUIStateChange;
  }
}

Solution in Github issues page:

Adding the ngZone line appears to fix the change detection issues caused by onAuthUIStateChange().

Then in app.component.html I’ve just added the ngIf to the <ion-app> itself. Therefore if a user isnt logged in, they’ll just see the Amplify pre-built UI sign in page. Then after sign in, they’ll see the contents of <ion-app>

app.component.html

<amplify-authenticator *ngIf="authState !== 'signedin'"></amplify-authenticator>
<ion-app *ngIf="authState === 'signedin' && user">
  <ion-router-outlet></ion-router-outlet>
</ion-app>

I’ve raised with AWS support and they are looking into it, so I’ll see what solution they come back with.

it doesn’t make any sense to wrap the ion-router-outlet ? Where did you even get the idea that was a valid pattern to use?

The code I posted is from a working example; not sure what the issue is you are having if you use the code I provided… or you could just wait to see what you get back from amazon.

I haven’t tried your code, but assume it would be a good approach.

The code I’ve posted above is what I currently have working based on the AWS Amplify documentation and the extra line that fixes the change detection issues it causes.

The ion-router-outlet is wrapped in the ion-app component by default when you create an ionic app with the blank template.

I am saying it doesn’t make sense to wrap the router with the amplify code? But if it works for you the good for you

Yeah I thought it seemed strange too.

This documentation has the template outside of the <amplify-authenticator>

<template>
  <amplify-authenticator>
    <div>
      My App
      <amplify-sign-out></amplify-sign-out>
    </div>
  </amplify-authenticator>
</template>

If using this approach, clicking the signout button doesnt remove / hide any of the app pages and therefore doesnt show the Amplify sign in UI in its place.

This documentation, however, shows it implemented differently:

https://docs.amplify.aws/ui/auth/authenticator/q/framework/ionic

<amplify-authenticator> is placed outside of the app ‘template’ code.

<amplify-authenticator *ngIf="authState !== 'signedin'"></amplify-authenticator>

<div *ngIf="authState === 'signedin' && user" class="App">
    <amplify-sign-out></amplify-sign-out>
    <div>Hello, {{user.username}}</div>
    <!-- This is where you application template code goes -->  
</div>

I therefore presumed that the <ion-app> code should be placed in the space where the comment is.

When using this approach, clicking the sign out button does remove / hide the app pages from view and brings up the Amplify sign in UI.

I’m not sure there is any negative implications for removing / including the <ion-app> component in this way?

@lrsupport
This is similar to how I have implemented it and it works fine… not sure why you are saying it doesn’t work. I have provided working source code, if you have code that shows it doesn’t work then you should provide it so that others arriving on this page are not confused.

@aaronksaunders - For some reason when implementing it as AWS Amplify suggest, I get issues with change detection not working. It only works after the ngZone line I mentioned earlier.