HOWTO: Escape In-App Browsers on Android & iOS

Published

I recently wrote about in-app browsers as more of a piece on the history and state-of-the-state so to speak while also covering some options for “escape hatches” in Android & iOS. However, since posting that last week and working through some feedback, particularly from Lobsters, I want to create a standalone post on specifically escaping in-app browsers as of writing (July 2024).

Detection

As I mention in the original article, inapp-spy and bowser are still the go-to options for detecting in-app and OS/browser metadata, respectively.

Here’s a boilerplate example of how you could use them together:

import Bowser from "bowser"
import InAppSpy from "inapp-spy"
const { isInApp } = InAppSpy()
// Get the OS name, to lowercase e.g. "ios", "android"
const os = Bowser.getParser(window.navigator.userAgent).getOSName(true)

// Detect in-app
if (isInApp) {
  if (os === "ios") {
    // Handle iOS
  } else if (os === "android") {
    // Handle Android
  } else {
    // How did you get here?
  }
}

Android

Android has a simple and reliable method: intent: links. In practice, these look like intent:https://example.com#Intent;end and in code something like:

const url = "https://example.com"
const link = `intent:${url}#Intent;end`

Here’s a more complete example, assuming inapp-spy and bowser exist and are instantiated like the example above:

if (isInApp) {

  // 1. Attempt to auto-redirect
  window.location.replace(link)
    
  // 2. Append a native <a> with the same intent link
  const $div = document.createElement("div")
  $div.innerHTML = `
    <p>Tap the button to open in your default browser</p>
    <a href="${link}" target="_blank">Open</a>
  `
  document.body.appendChild($div)
}

If you want to try this out in a live example for yourself, Shalanah Dawson’s excellent inappdebugger.com demos this under the “Android In-App Escape Links” section.

iOS

Until a few days ago, I didn’t know of any reliable ways to escape in-app browsers on iOS. The Lobsters discussion led me to finding out about the “Shortcuts fallback” strategy where you attempt to run a Shortcut that doesn’t exist, specify your URL as an error query parameter callback, and your URL gets opened in the device’s default browser.

In code, this looks like:

const url = encodeURIComponent("https://example.com")
const id = crypto.randomUUID()
const link = `shortcuts://x-callback-url/run-shortcut?name=${id}&x-error=${url}`

We use the browser builtin crypto.randomUUID() to generate a random UUIDv4 on the fly which we can be confident will be an invalid Shortcut name.

This is very much “hacky” and an unintended use of this “feature”, which you can read more about from Apple’s docs.

It comes with two main side effect caveats:

  1. It opens the Shortcuts app on the user’s device.
  2. Some query parameters will be appended to your URL e.g. https://example.com becomes https://example.com/?errorDomain=NSCocoaErrorDomain&errorMessage=Could%20not%20find%20the%20shortcut%20%E2%80%9C5316b3b5-150b-40d9-b979-21d65ba482d7.%E2%80%9D&errorCode=4.

I got this merged into inappdebugger.com so you can test it out there under “iOS In-App Escape Links” as the button labelled “Shortcuts fallback.” As an aside, this isn’t limited to iOS only: it should also work on iPadOS and macOS.

All Together Now

We can combine all these strategies into one. Please note, this code snippet is not fully tested.

import Bowser from "bowser"
import InAppSpy from "inapp-spy"
const { isInApp } = InAppSpy()
// Get the OS name, to lowercase e.g. "ios", "android"
const os = Bowser.getParser(window.navigator.userAgent).getOSName(true)

// Perhaps defined build-time as a environment variable
const url = "https://example.com"

// Detect in-app
if (isInApp) do {
  let link
  if (os === "android") {
    link = `intent:${url}#Intent;end`
  } else if (os === "ios") {
    link = `shortcuts://x-callback-url/run-shortcut?name=${crypto.randomUUID()}&x-error=${encodeURIComponent(url)}`
  } else {
    // You probably shouldn't be here, maybe raise an error and tell somebody
    break
  }

  // Attempt to auto-redirect
  window.location.replace(link)

  // Append a native <a> with the same link
  const $div = document.createElement("div")
  $div.innerHTML = `
    <p>Tap the button to open in your default browser</p>
    <a href="${link}" target="_blank">Open</a>
  `
  document.body.appendChild($div)
} while (false)

I’ll leave you with a nice escape meme.

Someone mashing the Esc keyboard key. Via Giphy, of course.Someone mashing the Esc keyboard key. Via Giphy, of course.


I love hearing from readers so please feel free to reach out.

Reply via emailSubscribe via RSS or email

Last modified  #howto   #hack   #security   #privacy   #frontend   #web 


← Newer post  •  Older post →