Search code examples
progressive-web-appswindow.open

PWA: force window.open to open browser instead of PWA


I have built an ASP.NET Core application with an angular frontend. The angular app has the @angular/pwa node package setup, so it's a progressive web app that can be installed on android/windows behaving like a native app.

I have setup external logins (Microsoft, Google, Facebook, Twitter) with Microsoft.AspNetCore.Identity. From my angular app I'm opening a popup to an external login page:

  this.authWindow = window.open(`${this.baseUrl}/web/v2/Account/${this.action}/${medium}/${this.platform}`, null, 'width=600,height=400');

The url for the popup routes to an ASP.NET Core endpoint where I have the return Challenge() call, which returns the login page for the specific external provider (Microsoft, Google, Facebook, Twitter).

In Chrome on Windows, you click a button which triggers the window.open() in order to open a window with the external login page. On successfull login you're being redirected to the callback page, which is a razor page which sends a message to the main window containing the angular app. The message is being handled and the popup is being closed.

Problem

When I use the website on Chrome for Android, I can install the PWA as app, which adds an icon on my android homepage. When I open the PWA and click the button to open the popup, the popup is being opened in a popup window for my PWA, so no problem there.

When I open Chrome on android and visit the website, while the PWA is installed, the window.open() call does not open a popup window for the Chrome browser, but instead tries to open the popup window for the Progressive Web App. Since this is the case, the popup window inside the PWA cannot notify the website in Chrome about the successful login (duh...).

But when the PWA is not installed, the window.open() works fine and opens the popup in Chrome itself.

So the bottom line is, the PWA is installed on android. And I want to be able to call window.open() from my website inside Chrome, and have it open the popup in Chrome browser instead of the PWA.

Things I've tried

  1. Modify ngsw-config.json

    { ..., "navigationUrls": [ "/", "!//.", "!//__", "!//__/", "!/web/v2/Account/connect//", "!/web/v2/Account/add//**" ] }

  2. Open the window with target='_system'

    this.authWindow = window.open(${this.baseUrl}/web/v2/Account/${this.action}/${medium}/${this.platform}, '_system', 'width=600,height=400');

  3. Open the window with target='_blank'

    this.authWindow = window.open(${this.baseUrl}/web/v2/Account/${this.action}/${medium}/${this.platform}, '_blank', 'width=600,height=400');

  4. Open the window with target='_blank' and without baseUrl, just an absolute path.

    this.authWindow = window.open(/web/v2/Account/${this.action}/${medium}/${this.platform}, '_blank', 'width=600,height=400');

  5. Use ngsw-bypass

    this.authWindow = window.open(/web/v2/Account/${this.action}/${medium}/${this.platform}?ngsw-bypass=true, '_blank', 'width=600,height=400');

But all tricks seem to behave the same and still open the window in the PWA.


Solution

  • I ended up creating a subdomain hosting my endpoints for external login (ExternalLogin, ExternalLoginCallback, AddExternalLogin, AddExternalLoginCallback):

    [Controller]
    [Route("web/v2/[controller]")]
    public class AccountController : Controller
    {
        private IAccountService accountService;
        public AccountController(IAccountService accountService)
        {
            this.accountService = accountService;
        }
        
        ...
    
        // GET: web/Account/providers
        [AllowAnonymous]
        [HttpGet("providers", Name = "web-v2-account-external-providers")]
        public async Task<ActionResult<IEnumerable<string>>> Providers()
        {
            var result = await accountService.GetProviders();
            return Ok(result);
        }
    
        // GET: web/Account/connect/{provider}
        [AllowAnonymous]
        [HttpGet("connect/{medium}/{provider}", Name = "web-v2-account-external-connect-challenge")]
    #if RELEASE
        [Host("external.mintplayer.com")]
    #endif
        public async Task<ActionResult> ExternalLogin([FromRoute]string medium, [FromRoute]string provider)
        {
            var redirectUrl = Url.RouteUrl("web-v2-account-external-connect-callback", new { medium, provider });
            var properties = await accountService.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
            return Challenge(properties, provider);
        }
    
        // GET: web/Account/connect/{provider}/callback
        [HttpGet("connect/{medium}/{provider}/callback", Name = "web-v2-account-external-connect-callback")]
    #if RELEASE
        [Host("external.mintplayer.com")]
    #endif
        public async Task<ActionResult> ExternalLoginCallback([FromRoute]string medium, [FromRoute]string provider)
        {
            try
            {
                var login_result = await accountService.PerfromExternalLogin();
                if (login_result.Status)
                {
                    var model = new LoginResultVM
                    {
                        Status = true,
                        Medium = medium,
                        Platform = login_result.Platform
                    };
                    return View(model);
                }
                else
                {
                    var model = new LoginResultVM
                    {
                        Status = false,
                        Medium = medium,
                        Platform = login_result.Platform,
    
                        Error = login_result.Error,
                        ErrorDescription = login_result.ErrorDescription
                    };
                    return View(model);
                }
            }
            catch (OtherAccountException otherAccountEx)
            {
                var model = new LoginResultVM
                {
                    Status = false,
                    Medium = medium,
                    Platform = provider,
    
                    Error = "Could not login",
                    ErrorDescription = otherAccountEx.Message
                };
                return View(model);
            }
            catch (Exception ex)
            {
                var model = new LoginResultVM
                {
                    Status = false,
                    Medium = medium,
                    Platform = provider,
    
                    Error = "Could not login",
                    ErrorDescription = "There was an error with your social login"
                };
                return View(model);
            }
        }
    
        // GET: web/Account/logins
        [Authorize]
        [HttpGet("logins", Name = "web-v2-account-external-logins")]
        public async Task<ActionResult<IEnumerable<string>>> GetExternalLogins()
        {
            var logins = await accountService.GetExternalLogins(User);
            return Ok(logins.Select(l => l.ProviderDisplayName));
        }
    
        // GET: web/Account/add/{provider}
        [Authorize]
        [HttpGet("add/{medium}/{provider}", Name = "web-v2-account-external-add-challenge")]
    #if RELEASE
        [Host("external.mintplayer.com")]
    #endif
        public async Task<ActionResult> AddExternalLogin([FromRoute]string medium, [FromRoute]string provider)
        {
            var redirectUrl = Url.RouteUrl("web-v2-account-external-add-callback", new { medium, provider });
            var properties = await accountService.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
            return Challenge(properties, provider);
        }
    
        // GET: web/Account/add/{provider}/callback
        [Authorize]
        [HttpGet("add/{medium}/{provider}/callback", Name = "web-v2-account-external-add-callback")]
    #if RELEASE
        [Host("external.mintplayer.com")]
    #endif
        public async Task<ActionResult> AddExternalLoginCallback([FromRoute]string medium, [FromRoute]string provider)
        {
            try
            {
                await accountService.AddExternalLogin(User);
                var model = new LoginResultVM
                {
                    Status = true,
                    Medium = medium,
                    Platform = provider
                };
                return View(model);
            }
            catch (Exception)
            {
                var model = new LoginResultVM
                {
                    Status = false,
                    Medium = medium,
                    Platform = provider,
    
                    Error = "Could not login",
                    ErrorDescription = "There was an error with your social login"
                };
                return View(model);
            }
        }
    }
    

    When running in the PWA, the window.open will still open the link inside an embedded browser within your PWA, and when running from the browser window.open will still open the link in a new browser window (not in your PWA). In both cases I'm still able to access the opener to send messages (window.opener.postMessage).