0

I'm getting a weird bug in my program that is making the *ngIf stop working.

My method:

stripeSubmit(){
    this.stripeCheckout = false;   // removes the form 
    this.isLoading = true;         // shows a loading spinner
       ...
        this.api.chargeCard(charge).subscribe(res =>{ 
          if(res === "Success"){
            this.api.submitOrder(this.orderObj).subscribe(res =>{
              this.isLoading = false;              // not working
              sessionStorage.clear();
              this.router.navigate(['/thank-you']) // is working but still displays /cart on top
            });
          }else {
            console.log("ELSE condition"); // works
            alert(res);                    // works
            this.stripeFailBoolean = true; // doesn't work
            this.stripeCheckout = true;    // doesn't work
            this.isLoading = false;        // doesn't work
            console.log(this.isLoading);   // logs "false"
          }
        })
      }
    });
 }

HTML:

...
    <h1 class="title">Your Cart</h1>
    <img *ngIf="isLoading"id="loading" src="../../../assets/infinitySpinner.svg" alt="">
 ...
<div *ngIf="stripeFailBoolean" class="alert alert-danger"> Something went wrong, please try again</div>

    <div *ngIf="stripeCheckout" class="stripe">
        <form class="form mt-4">
            <mat-form-field>
                <mat-label>Credit Card</mat-label>
                <input type="text" matInput [(ngModel)]="cardNumber" name="cardNumber">
            </mat-form-field>
            <br>
            <mat-form-field>
                <mat-label>Expiration Month</mat-label>
                <input type="number" matInput [(ngModel)]="expMonth" name="expMonth">
            </mat-form-field>
            <br>
            <mat-form-field>
                <mat-label>Expiration Year</mat-label>
                <input type="number" matInput [(ngModel)]="expYear" name="expYear">
            </mat-form-field>
            <br>
            <mat-form-field>
                <mat-label>CVC</mat-label>
                <input type="number" matInput [(ngModel)]="cvc" name="cvc">
            </mat-form-field>
            <br>
           
            <button class="btn btn-secondary submit" (click)="stripeSubmit()" >Submit</button>
          </form>
    </div>

As is labeled in the commented-out sections above, when I click submit the form goes away and the spinner starts up. if the method chargeCard is a success, the h1 and the loading spinner will stay and the contents of "/thank-you" display underneath them (the URL shows "/thank-you").

if the method does not pass the if statement the alert and the log work but none of the 3 booleans work and the spinner goes indefinitely.

I tried putting all of my booleans in an ngDoCheck, but that didn't help at all.

Anyone know why my booleans work unless inside this one method? Thanks!

*****EDIT: You can replicate the error for yourself if you would like:

www.howlingwolfe.com/rentals

rent a boat on the bottom and then go to cart

for a successful transaction use credit card: 4242 4242 4242 4242 12/2024 123

for insufficient funds use credit card: 4000 0000 0000 9995 12/2024 123

The code is at www.github.com/andrethetallguy/howlingwolfefe

***********EDIT 2

Here is the code on Stackblitz: https://angular-ivy-gyhsya.stackblitz.io

It is the Cart Component that is giving me troubles

AylaWinters
  • 1,121
  • 2
  • 7
  • 24
  • 1
    Can you create an [mcve] that better illustrates the issue? You can also try to recreate it using https://stackblitz.com – Igor May 11 '21 at 19:35
  • I added the live site so that the error can be reproduced. Can't seem to get stackblitz to work, but I'll keep trying – AylaWinters May 11 '21 at 20:52

2 Answers2

1

As Stripe is a 3ed party tool and seems to not have any integration with Angular (judging on this line in Your repo when You are casting window to any before getting access to Stripe object) I would say this unexpected behavior is caused by running code outside of NgZone.

In a very simple terms - if Your code is calling some code as a callback of some external code, Angular might be not aware that the code was run. That means Angular is not aware of the fact some values changed -> it does not trigger change detection -> it does not refresh the page properly.

Why navigation is working? Because code in createToken's callback navigates to https://www.howlingwolfe.com/thank-you. And as URL changes, the matching component is used. Angular won't update the view but it will load a new component as URL changed.

Why console.log(this.isLoading); is working? It is not a part of Angular to worry about updating values of the variables. Angular will not have any idea You changed the value but in the code You definitely changed it, so console.log will be printing the proper value. It will be changed but Angular won't do anything about it if done outside of zone.

Now how to solve it: inject NgZone to constructor and wrap entire of the code (so starting from line 109) in this._ngZone.run(). You can follow the example provided in the docs.

Other issue might be that You are subscribing in the subscription. I do not have documentation here to back me up. The main reason would be that it gets tricky to unsubscribe from the inner subscription in a subscription (to be fair You should at least use takeUntil(unsubscribe$) pattern for each of Your subscriptions (so ...pipe(takeUntil(this.unsubscribe$)).subscribe(...)).

Next step would be to not having inner subscriptions - that's when higher observables come into play.

To give You an example, You should avoid doing it like this:

this.api.chargeCard(charge).subscribe(res1 => { 
    if (res1 === "Success") {
        this.api.submitOrder(this.orderObj).subscribe(res2 => {...})
    }
})

and do it like this:

this.api.chargeCard(charge).pipe(
    switchMap((res1: string) => res1 === "Success" 
        ? this.api.submitOrder(this.orderObj).pipe(tap(/* do Your code here, clear and navigate*/)) 
        : of(res1).pipe(tap(/* do Your code here on failure))),
    finalize(() => this.isLoading = false),
    takeUntil(this.unsubscribe$)
).subscribe()

switchMap tap

Eatos
  • 444
  • 4
  • 12
  • Thank you very much for this well thought out response! Unfortunately, I'm still getting the same behavior when I test the application. Perhaps I'm not using tap right? I have tried `tap( ()=> { /* my code */})` and `tap({ complete: ()=> { /* my code */ }})` but both are giving me the same results. What is the code supposed to look like inside of the `tap()` thanks! – AylaWinters May 13 '21 at 21:44
  • 1
    I didn't see how Your API's are defined but for starters I will try to comment out everything in aStripe's callback, put ngZone there and withit just try to change the flag/navigate to see if it is working. So without any subscriptions. I'll try to clone Your repo today and see if I can reproduce it. Or this is only tap part where Your code is not working? – Eatos May 14 '21 at 04:47
  • thank you for looking in to it. I'm not sure which part is not working, but adding in your code is still producing the same results as the original question. – AylaWinters May 14 '21 at 15:26
  • 1
    See the [Pull Request](https://github.com/AndreTheTallGuy/HowlingWolfeFE/pull/2/commits/1078db6dea9dd489ae80d2cfb278f1d8a5d2b4a1). Run the project, study the examples, apply changes to Your codebase. Pay attention to numbers that are changing on the UI for 3 and 4 example. – Eatos May 14 '21 at 18:04
0

Thank you soooo much to @Eatos!! The NgZone was definitely the way to go. Here is the new working method:

stripeSubmit(){
    this.stripeCheckout = false;
    this.isLoading = true;
    this.stripeFailBoolean = false;

    (<any>window).Stripe.card.createToken({
      number: this.cardNumber,
      exp_month: this.expMonth,
      exp_year: this.expYear,
      cvc: this.cvc
    }, (status: number, response: any) => {
      this.ngZone.run(() => {
      if (status === 200) {
        let charge: Charge = {
          token: response.id,
          price: this.total
        }
        // this.chargeCard(token);
        this.api.chargeCard(charge).pipe(takeUntil(this.unsubscibe), tap(()=>{
          console.log('inside of charge card tap');
          
          
        }), switchMap((res: any)=>{
          if(res === "Success"){
            return this.api.submitOrder(this.orderObj).pipe(tap(()=>{
              this.isLoading = false;
              sessionStorage.clear();
              this.router.navigate(['/thank-you'])              
            }))
          } else {
            return of(res).pipe(tap(()=>{
            this.stripeFailText = res;
            this.stripeFailBoolean = true;
            this.stripeCheckout = true;
            this.isLoading = false;
            }))
          }
          
        })).subscribe();
      }else {
        console.log(response.error.message);
        
      }
      console.log('ngZone leave');
    })
  }); 
  }   
 
  ngOnDestroy(){
    this.unsubscibe.next();
    this.unsubscibe.complete();
  }

AylaWinters
  • 1,121
  • 2
  • 7
  • 24