Window.open with ios for an async function not working

In my app so far all the window.open calls work fine on iOS but this one is using an async function.

Is there a solution for this?
I have spent several hours trying various things from stackoverflow.

I have seen that installing the inAppBrowser may help which I have not tried yet.

I also saw that putting the window.open in the click event may allow it but this would require the user to make another click which is not ideal.

<ion-button type="button" color="primary" expand="block" (click)="onCreateStripePortalSession()">
        Manage my cards
</ion-button>

async onCreateStripePortalSession() {
    const returnUrl = `https://myapp.app/goto/${this.tenant.slug}/account`;
    try {
      const session = await this.stripeService.createStripePortalSession(this.member.stripeCustomerId, returnUrl);
      if (session.url) {
        // Go to Stripe Customer Portal
        window.open(session.url, '_blank');
      }
    } catch (err) {
      console.log(err);
    }
  }

I am not a big fan of async and await because they obscure what is actually happening, and this may be easier to track down if you get rid of them.

That being said, I have a hunch.

Hopefully you control the code to stripeService. If you don’t, I’m sure you can modify this advice accordingly, though.

Replace your createStripePortalSession with a call to createSafeStripePortalSession instead, and make it look like this:

createSafeStripePortalSession(id: string, returnUrl: string): Promise<PortalSession> {
  return Promise.resolve(this.createStripePortalSession(id, returnUrl));
}

See if it fixes your problem. If so, the reason it works is that the Promise coming from Stripe’s API isn’t zone-aware. I ran into this problem with Promises that come from WebCrypto, and wrapping them with another Promise.resolve is the cleanest way I came up with to zonify them. You could do something similar with NgZone.run, but I hate that because it needlessly exposes implementation details that should be subject to change.

That didn’t work.

Here is what I tried.

const session = await this.stripeService.createSafeStripePortalSession(this.member.stripeCustomerId, returnUrl);

// Customer Portal
  createStripePortalSession(stripeCustomerId: string, returnUrl: string) {
    const fun = this.aff.httpsCallable('stripeCreatePortalSession');
    return fun({ connectedStripeAccountId: this.tenant.connectedStripeAccountId, stripeCustomerId, returnUrl}).toPromise();
  }

  createSafeStripePortalSession(stripeCustomerId: string, returnUrl: string): Promise<any> {
    return Promise.resolve(this.createStripePortalSession(stripeCustomerId, returnUrl));
  }

It is actually calling my own firebase cloud function which is then calling Stripe from there so I have control of the cloud function too.

Amongst the “many things tried from stackoverflow”, did you try:

let wref = window.open();
this.stripeService.createStripePortalSession(...).then(ps => {
  wref.location = ps.url;
});

Yeah I did try that.
The window.open works (although I need to give it a url) but it seems to lose the reference so the wref.location does not work. I think this probably only works for web.

OK, I think we’re at the point where this topic could be renamed “help me make Stripe play nicely with Safari’s popup blocker”.

There seem to be many different APIs and assorted plugins out there for interacting with Stripe. Perhaps an overview of which you are using under the hood here might help suggest further lines of inquiry.

Safari apparently really doesn’t want windows spawned from code that is not a direct response to a user input, which I guess is sort of understandable but probably sort of irritating in this precise situation. I’m sure you’re not the only one who’s hit this, but I think it’s important to focus on Stripe-related options, because generic attempts at solutions seem to be hitting a brick wall.

Yeah ios can be a real pain.

I’m going to search for a few more things to try.
The next thing I will try is the in app browser as I saw mention that it resolved the problem, even if you open the window in the system browser.

If none of that works I will resort to having two buttons or creating the portal session if the user goes to that page. Not great solutions but either should work.

I have no idea why the inappbrowser solution would work though.

I fixed it easily with the below but it is not a great solution as the user has click twice.
I may just leave it like this for a while rather than expending any more time on it.

const session = await this.stripeService.createStripePortalSession(this.member.stripeCustomerId, returnUrl);
if (session.url) {
  // Go to Stripe Customer Portal
  this.notificationService.presentAlertWithExternalUrlOption('Manage my Cards', 'Go to Stripe to manage your cards?', 'Lets go', session.url);
}

 presentAlertWithExternalUrlOption(header: string, message: string, routeLabel: string, url: string) {
    this.alertCtrl.create({
      header,
      cssClass: 'ion-alert-custom',
      message,
      buttons: [
        {
          text: 'Cancel',
          role: 'cancel'
        },
        {
          text: routeLabel,
          handler: () => {
            window.open(url, '_blank');
          }
        },
    ]
    }).then(alertEl => {
      alertEl.present();
    });
  }