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();
    });
  }

Hello everyone.
Does someone have any news regarding this issue?

I use Capacitor, and my users log in using OpenID Connect (with angular-auth-oidc-client), because of company policies, we can’t use the inAppBrowser, and have to rely on the system browser.

This code works on Android, but not on iOS:

const urlHandler = (authUrl) => {
    window.open(authUrl, '_system');
}
this.oidcSecurityService.authorize(this.clientConfiguration, { urlHandler });

So I managed to make it work using the solution provided by @wekas with a popup, but it’s definitely annoying for the users to clic on “Login”, and then have a popup to confirm that he, indeed, wants to login…

login() {
   const urlHandler = (authUrl) => {
       this.zone.run(async () => await this.askToOpenBrowser(authUrl));
   }
   this.oidcSecurityService.authorize(this.clientConfiguration, { urlHandler });
}

    async askToOpenBrowser(url: string) {
        const alert = await this.alertCtrl.create({
            message: this.translate.instant('open_browser_to_login.message'),
            buttons: [
                {
                    text: this.translate.instant('open_browser_to_login.accept'),
                    handler: () => {
                        window.open(url, '_system');
                    }
                },
                {
                    text: this.translate.instant('open_browser_to_login.cancel'),
                    role: "cancel"
                }
            ]
        });

        await alert.present();
    }

Window.open function not work on safari ios and safari
For more information you can have a look on the documentation Window.open() - Web APIs | MDN

This is false, and MDN docs says that window.open is compatible since Safari iOS version 1.

The issue here is that it doesn’t work in an async callback in Ionic.

If there is another way to open the native browser, I would be very interested in that.

window.open work on safari browser version 1 but not with latest version of safari browser.

There are some plugin you can use for native browser it may help you.

For capacitor

For cordova

For web
window.location.href

Thanks for your reply, but I feel like we’re going in circle here.

Opening safari works well on iOS 16.2 with window.open, as long as it’s not in an async function.
In the code I provided, opening safari works, it just does so on user action.

The plugins you provided uses inApp browser, and I can’t use that.

I really feel like it’s an issue with Ionic itself.