Ask for a passcode with Ionic and RxJS

Issue

I want to implement an alert dialog which asks the user for a passcode.
If the passcode is equal to a saved password, the dialog should dismiss.
Otherwise, the dialog should present itself again until the user enter the correct passcode.
For this task I’m using Ionic and in particular ion-alert.

The following works as expected.

askForPasscode() {
  const alertOpts = {
    backdropDismiss: false,
    header: this.transloco.translate('passcodeEnter'),
    message: this.failedAttempts ? this.transloco.translate('passcodeFailedAttempts', { failedAttempts: this.failedAttempts }) : '',
    inputs: [ {
      name: 'passcode',
      placeholder: this.transloco.translate('passcodeEnter'),
      attributes: { maxlength: passcodeLength, type: 'password' },
    } ],
    buttons: [ {
      text: this.transloco.translate('confirm'),
      handler: this.verifyPasscode.bind(this),
    } ],
  };
  return fromPromise(this.alertCtrl.create(alertOpts)).pipe(
    map(alert => fromPromise(alert.present())));
}

verifyPasscode({ passcode }: any ) {
  const digest = SHA1(passcode).toString();
  this.storage.get('passcode').subscribe(stored => {
    const isFailedAttempt = digest !== stored;
    this.failedAttempts = isFailedAttempt ? this.failedAttempts + 1 : 0;
    if (isFailedAttempt)
      this.askForPasscode().subscribe();
  });
}

this.askForPasscode().subscribe();

As refinement, I’d like to move the retry portion outside of the askForPasscode method, and manage it with the RxJS operators.
So I have refactored the code in the following way.

askForPasscode() {
  const alertOpts = {
    backdropDismiss: false,
    header: this.transloco.translate('passcodeEnter'),
    message: this.failedAttempts ? this.transloco.translate('passcodeFailedAttempts', { failedAttempts: this.failedAttempts }) : '',
    inputs: [ {
      name: 'passcode',
      placeholder: this.transloco.translate('passcodeEnter'),
      attributes: { maxlength: PasscodeLength, type: 'password' },
    } ],
    buttons: [ {
      text: this.transloco.translate('confirm'),
      // handler: this.verifyPasscode.bind(this),
    } ],
  };
  return fromPromise(this.alertCtrl.create(alertOpts)).pipe(
    // map(alert => fromPromise(alert.present())));
    switchMap(alert => {
      alert.present();
      return fromPromise(alert.onDidDismiss());
    }));
}

verifyPasscode(passcode: string) {
  const digest = SHA1(passcode).toString();
  // this.storage.get('passcode').subscribe(stored => {
  return this.storage.get('passcode').pipe(map(stored => {
    const isFailedAttempt = digest !== stored;
    this.failedAttempts = isFailedAttempt ? this.failedAttempts + 1 : 0;
    // if (isFailedAttempt)
    //   this.askForPasscode().subscribe();
    return !isFailedAttempt;
  }));
}

// this.askForPasscode().subscribe();
this.askForPasscode().pipe(
  map(result => result.data.values.passcode),
  switchMap(passcode => this.passcode.verifyPasscode(passcode)),
  map(isSuccessful => { if (!isSuccessful) throw 'error' }),
  retry({count: 5}),
).subscribe();

verifyPasscode now just verify the correctness of the passcode, and returns a flag.
askForPasscode now have a bunch of pipeable operators which (1) read the flag, (2) throw an error if the verification failed, and (3) resubscribe if an error was thrown.

However, the dialog is not presented again if the passcode is wrong.
It looks like the askForPasscode() is not invoked again no matter what.
What am I missing?

Solution

I refactored your stackblitz-example to make it work as intended:

Stackblitz-Demo

What were the issues with your code?

  • When retry is triggered, fromPromise(this.alertCtrl.create(alertOpts)) is not executed, while the subsequent switchMap actually is. Therefore alert.present() refers to an alert that was already dismissed.
  • Because you placed the alertOpts inside the function, its properties would not be updated on failed login-attempts and therefore the ${this.failedAttempts} failed attempts message would not have been displayed ever.

How did I resolve the issues?

  • In the askForPasscode() method, I wrapped all alert promises in a defer operator which creates a new observable each time it is subscribed to.
  • I’ve moved the alert options out into a separate function so that they are reassembled from scratch each time a login fails, thus showing the correct number of failed login attempts.
failedAttempts = 0;

constructor(private alertCtrl: AlertController) {}

ngOnInit() {
  this.askForPasscode()
    .pipe(
      map((result: any) => result.data.values.passcode),
      switchMap((passcode) => this.verifyPasscode(passcode)),
      map((isSuccessful) => {
        if (!isSuccessful) throw 'error';
      }),
      retry({ count: 5 })
    )
    .subscribe();
}

askForPasscode(): Observable<any> {
  return defer(async () => {
    const alert = await this.alertCtrl.create(this.getLoginModal());
    await alert.present();
    return alert.onDidDismiss();
  });
}

verifyPasscode(passcode: string) {
  return of('passcode').pipe(
    map((stored) => {
      const isFailedAttempt = passcode !== stored;
      this.failedAttempts = isFailedAttempt ? this.failedAttempts + 1 : 0;
      return !isFailedAttempt;
    })
  );
}

getLoginModal() {
  return {
    backdropDismiss: false,
    header: 'passcodeEnter',
    message: this.failedAttempts
      ? `${this.failedAttempts} failed attempts`
      : '',
    inputs: [
      {
        name: 'passcode',
        placeholder: 'enter passcode',
        attributes: { type: 'password' },
      },
    ],
    buttons: [
      {
        text: 'confirm',
      },
    ],
  };
}

Answered By – kellermat

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published