{"id":3008,"date":"2024-07-17T10:07:47","date_gmt":"2024-07-17T16:07:47","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=3008"},"modified":"2024-07-23T17:37:42","modified_gmt":"2024-07-23T22:37:42","slug":"the-pitfalls-of-in-app-browsers","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/the-pitfalls-of-in-app-browsers\/","title":{"rendered":"The Pitfalls of In-App Browsers"},"content":{"rendered":"\n<p>Developing websites for modern mobile devices has a pitfall you may not be aware of: in-app browsers. These are web browsers embedded directly within native mobile apps. So if a link is clicked within a native app (e.g. Instagram or TikTok), it uses the in-app browser<em> instead<\/em> of switching apps to a dedicated browser app.<\/p>\n\n\n\n<p>While potentially convenient for mobile developers (i.e. <em>users will never leave our app! the businessmen squeal<\/em>), we\u2019ll discuss the drawbacks for web developers like yourself <em>and<\/em> your users. <\/p>\n\n\n\n<p class=\"learn-more\">In-app browsers are also referred to as <em>embedded browsers<\/em> or <em>WebView<\/em>. These are interchangeable terms.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Drawbacks<\/h2>\n\n\n\n<p>The drawbacks of in-app browsers can be broadly categorized:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"#limited-functionality\">Limited functionality<\/a><\/li>\n\n\n\n<li><a href=\"#privacy-security-concerns\">Privacy &amp; security concerns<\/a><\/li>\n\n\n\n<li><a href=\"#inconsistent-ui-ux\">Inconsistent UI\/UX<\/a><\/li>\n\n\n\n<li><a href=\"#worse-performance\">Worse performance<\/a><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"drawback-limited\">Limited functionality<\/h3>\n\n\n\n<p>In-app browsers are considerably stripped down when compared to their fully-featured counterparts and typically lack features like bookmarking, UI controls, settings, extensions, and downloads. For for instance a browser extension that a user depends on or help protect their privacy will not work in an in-app browser.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"drawback-privacy\">Privacy &amp; security concerns<\/h3>\n\n\n\n<p>Because in-app browsers are embedded within a native mobile app, the app developer has control <em>and<\/em> visibility into the users\u2019 in-app browsing activity. This even extends into being able to <em>inject<\/em> code into the in-app browser which is a major privacy and security concern. Users are largely unaware and aren\u2019t able to opt-out even if they are.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"drawback-inconsistency\">Inconsistent UI\/UX<\/h3>\n\n\n\n<p>Because in-app browser implementations are all different, the UI is inconsistent. Further, browsing data like history and bookmarks aren\u2019t shared so users typically need to sign into services they may already be securely signed into in their devices actual browser. This leads to a fragmented and frustrating user experience.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"drawback-performance\">Worse performance<\/h3>\n\n\n\n<p>In-app browsers tend to be running outdated browser internals which can cause slower loading times and compatibility issues. Users on slower Internet connections may have the problem exacerbated.<\/p>\n\n\n\n<p><strong>Author Update:<\/strong> Since Apple doesn&#8217;t allow apps, even browsers, to use their own rendering engine&nbsp;<a href=\"https:\/\/open-web-advocacy.org\/files\/OWA%20-%20DMA%20Interventions%20-%20In-App%20Browsers%20v1.2.pdf#page=13\">only Android<\/a>&nbsp;has the problem of bundling in a custom in-app browser instead of using the system WebView, which may be outdated and have worse performance. On iOS, the built-in WebView is&nbsp;<a href=\"https:\/\/open-web-advocacy.org\/files\/OWA%20-%20DMA%20Interventions%20-%20In-App%20Browsers%20v1.2.pdf#page=7\">bundled as part of the iOS WebKit<\/a>. On Android, the default built-in WebView is based on the Blink version and is&nbsp;<a href=\"https:\/\/open-web-advocacy.org\/files\/OWA%20-%20DMA%20Interventions%20-%20In-App%20Browsers%20v1.2.pdf#page=7\">updated independently of the OS<\/a>&nbsp;as part of the Chrome update process via Google Play.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Bad Behavior in In-App Browsers<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">History<\/h3>\n\n\n\n<p>In-app browsers have existed since circa 2016, but it wasn\u2019t until 2019 when Thomas Steiner, a Google engineer, published a <a href=\"https:\/\/blog.tomayac.com\/2019\/12\/09\/inspecting-facebooks-webview\/\" target=\"_blank\" rel=\"noreferrer noopener\">blog post<\/a> that dove into Facebook\u2019s iOS and Android apps that a wider audience was made aware of the privacy and security concerns. Thomas discussed the technical details behind how the apps implemented their in-app browsers and stated how in-app browsers can perform man-in-the-middle (MITM) attacks by injecting arbitrary JavaScript code and intercepting network traffic.<\/p>\n\n\n\n<p>Three years later, in 2022, Felix Krause published two blog posts (<a href=\"https:\/\/krausefx.com\/blog\/ios-privacy-instagram-and-facebook-can-track-anything-you-do-on-any-website-in-their-in-app-browser\" target=\"_blank\" rel=\"noreferrer noopener\">1<\/a>, <a href=\"https:\/\/krausefx.com\/blog\/announcing-inappbrowsercom-see-what-javascript-commands-get-executed-in-an-in-app-browser\" target=\"_blank\" rel=\"noreferrer noopener\">2<\/a>) and a tool, <a href=\"https:\/\/inappbrowser.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">inappbrowser.com<\/a>, which focused on the privacy concerns of iOS apps. Initially this covered apps by Meta (Facebook, Messenger, Instagram) and then followed up with Android and other social media apps including TikTok. Felix\u2019s findings supported Thomas\u2019 from 3 years earlier and <em>showed<\/em> concerning findings from iOS Instagram: the arbitrary injection of a <a href=\"https:\/\/connect.facebook.net\/en_US\/pcm.js\" target=\"_blank\" rel=\"noreferrer noopener\">pcm.js<\/a> script which Meta claimed to be an \u201cevent aggregator\u201d but was <em>also<\/em> monitoring user interactions in the form of taps and selections. Further cause for concern was TikTok injecting JavaScript that monitors <em>all<\/em> keyboard inputs along with taps, which is effectively the functionality of a keylogger on third-party sites. TikTok acknowledged the existence of this code but claimed it\u2019s only used for debugging, troubleshooting, and performance monitoring.<\/p>\n\n\n\n<p>Felix\u2019s findings led to a <a href=\"https:\/\/regmedia.co.uk\/2022\/09\/22\/willis_v_meta.pdf\" target=\"_blank\" rel=\"noreferrer noopener\">lawsuit<\/a> being filed against Meta in September 2022. The case was <a href=\"https:\/\/storage.courtlistener.com\/recap\/gov.uscourts.cand.400497\/gov.uscourts.cand.400497.95.0.pdf\" target=\"_blank\" rel=\"noreferrer noopener\">dismissed<\/a> in October 2023.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Nothing Has Changed<\/h3>\n\n\n\n<p>Let\u2019s revisit the behavior of iOS &amp; Android Instagram\u2019s in-app browser at the time of this writing (July 2024). This is done by sharing the two testing links, <a href=\"https:\/\/inappbrowser.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">inappbrowser.com<\/a> and <a href=\"https:\/\/inappdebugger.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">inappdebugger.com<\/a> (we\u2019ll discuss this one more shortly), in the app as a direct message or URL in your profile bio. This is so you can actually click on them, as Instagram prevents clickable URLs in places like the descriptions of posts.<\/p>\n\n\n\n<p>Let\u2019s start with iOS. Below is iOS Instagram opening <code>inappbrowser.com<\/code> and <code>inappdebugger.com<\/code> in July 2024:<\/p>\n\n\n\n<div class=\"wp-block-jetpack-tiled-gallery aligncenter is-style-rectangular\"><div class=\"\"><div class=\"tiled-gallery__gallery\"><div class=\"tiled-gallery__row\"><div class=\"tiled-gallery__col\" style=\"flex-basis:33.33934%\"><figure class=\"tiled-gallery__item\"><img decoding=\"async\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/1-472x1024.jpg?strip=info&#038;w=600&#038;ssl=1 600w,https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/1-472x1024.jpg?strip=info&#038;w=800&#038;ssl=1 800w\" alt=\"\" data-height=\"1734\" data-id=\"3012\" data-link=\"https:\/\/frontendmasters.com\/blog\/?attachment_id=3012\" data-url=\"https:\/\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/1-472x1024.jpg\" data-width=\"800\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/1-472x1024.jpg?ssl=1\" data-amp-layout=\"responsive\" tabindex=\"0\" role=\"button\" aria-label=\"Open image 1 of 3 in full-screen\"\/><\/figure><\/div><div class=\"tiled-gallery__col\" style=\"flex-basis:33.33934%\"><figure class=\"tiled-gallery__item\"><img decoding=\"async\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/2-472x1024.jpg?strip=info&#038;w=600&#038;ssl=1 600w,https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/2-472x1024.jpg?strip=info&#038;w=800&#038;ssl=1 800w\" alt=\"\" data-height=\"1734\" data-id=\"3013\" data-link=\"https:\/\/frontendmasters.com\/blog\/?attachment_id=3013\" data-url=\"https:\/\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/2-472x1024.jpg\" data-width=\"800\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/2-472x1024.jpg?ssl=1\" data-amp-layout=\"responsive\" tabindex=\"0\" role=\"button\" aria-label=\"Open image 2 of 3 in full-screen\"\/><\/figure><\/div><div class=\"tiled-gallery__col\" style=\"flex-basis:33.32132%\"><figure class=\"tiled-gallery__item\"><img decoding=\"async\" srcset=\"https:\/\/i1.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/lIdOSskS-472x1024.png?strip=info&#038;w=600&#038;ssl=1 600w,https:\/\/i1.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/lIdOSskS-472x1024.png?strip=info&#038;w=900&#038;ssl=1 900w,https:\/\/i1.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/lIdOSskS-472x1024.png?strip=info&#038;w=996&#038;ssl=1 996w\" alt=\"\" data-height=\"2160\" data-id=\"3014\" data-link=\"https:\/\/frontendmasters.com\/blog\/?attachment_id=3014\" data-url=\"https:\/\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/lIdOSskS-472x1024.png\" data-width=\"996\" src=\"https:\/\/i1.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/lIdOSskS-472x1024.png?ssl=1\" data-amp-layout=\"responsive\" tabindex=\"0\" role=\"button\" aria-label=\"Open image 3 of 3 in full-screen\"\/><\/figure><\/div><\/div><\/div><\/div><\/div>\n\n\n\n<p>This shows that iOS Instagram is still injecting arbitrary JavaScript code which listens to user clicks along with JavaScript messages.<\/p>\n\n\n\n<p>(Editor note: when testing this I noted that <a href=\"https:\/\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/IMG_9190.png\">Instagram also appends URL parameters<\/a> on outgoing links, which may be used to communicate additional information to this injected JavaScript). <\/p>\n\n\n\n<p>Next, Android.<\/p>\n\n\n\n<div class=\"wp-block-jetpack-tiled-gallery aligncenter is-style-rectangular\"><div class=\"\"><div class=\"tiled-gallery__gallery\"><div class=\"tiled-gallery__row\"><div class=\"tiled-gallery__col\" style=\"flex-basis:33.33628%\"><figure class=\"tiled-gallery__item\"><img decoding=\"async\" srcset=\"https:\/\/i2.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/1-android-465x1024.jpg?strip=info&#038;w=600&#038;ssl=1 600w,https:\/\/i2.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/1-android-465x1024.jpg?strip=info&#038;w=800&#038;ssl=1 800w\" alt=\"\" data-height=\"1761\" data-id=\"3035\" data-link=\"https:\/\/frontendmasters.com\/blog\/?attachment_id=3035\" data-url=\"https:\/\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/1-android-465x1024.jpg\" data-width=\"800\" src=\"https:\/\/i2.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/1-android-465x1024.jpg?ssl=1\" data-amp-layout=\"responsive\" tabindex=\"0\" role=\"button\" aria-label=\"Open image 1 of 3 in full-screen\"\/><\/figure><\/div><div class=\"tiled-gallery__col\" style=\"flex-basis:33.33628%\"><figure class=\"tiled-gallery__item\"><img decoding=\"async\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/2-android-465x1024.jpg?strip=info&#038;w=600&#038;ssl=1 600w,https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/2-android-465x1024.jpg?strip=info&#038;w=800&#038;ssl=1 800w\" alt=\"\" data-height=\"1761\" data-id=\"3036\" data-link=\"https:\/\/frontendmasters.com\/blog\/?attachment_id=3036\" data-url=\"https:\/\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/2-android-465x1024.jpg\" data-width=\"800\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/2-android-465x1024.jpg?ssl=1\" data-amp-layout=\"responsive\" tabindex=\"0\" role=\"button\" aria-label=\"Open image 2 of 3 in full-screen\"\/><\/figure><\/div><div class=\"tiled-gallery__col\" style=\"flex-basis:33.32744%\"><figure class=\"tiled-gallery__item\"><img decoding=\"async\" srcset=\"https:\/\/i2.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/3-android-465x1024.jpg?strip=info&#038;w=600&#038;ssl=1 600w,https:\/\/i2.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/3-android-465x1024.jpg?strip=info&#038;w=900&#038;ssl=1 900w,https:\/\/i2.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/3-android-465x1024.jpg?strip=info&#038;w=981&#038;ssl=1 981w\" alt=\"\" data-height=\"2160\" data-id=\"3037\" data-link=\"https:\/\/frontendmasters.com\/blog\/?attachment_id=3037\" data-url=\"https:\/\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/3-android-465x1024.jpg\" data-width=\"981\" src=\"https:\/\/i2.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/3-android-465x1024.jpg?ssl=1\" data-amp-layout=\"responsive\" tabindex=\"0\" role=\"button\" aria-label=\"Open image 3 of 3 in full-screen\"\/><\/figure><\/div><\/div><\/div><\/div><\/div>\n\n\n\n<p>The story on Android is slightly different: there\u2019s still arbitrary JavaScript being injected but it isn\u2019t necessarily listening to events tightly coupled with user interactions.<\/p>\n\n\n\n<p>Unfortunately, not much has changed since Felix\u2019s findings nearly 3 years ago. <\/p>\n\n\n\n<p>Open Web Advocacy <a href=\"https:\/\/open-web-advocacy.org\/blog\/in-app-browsers-the-worst-erosion-of-user-choice-you-havent-heard-of\/\" target=\"_blank\" rel=\"noreferrer noopener\">wrote a piece<\/a> earlier this year following the events of <a href=\"https:\/\/letter.open-web-advocacy.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">Apple threatening to kill web apps<\/a>. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging &amp; Detecting In-App Browsers<\/h2>\n\n\n\n<p>Leveraging the existing excellent work of <a href=\"https:\/\/krausefx.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">Felix Krause<\/a> and <a href=\"https:\/\/github.com\/shalanah\" target=\"_blank\" rel=\"noreferrer noopener\">Shalanah Dawson<\/a> we have strategies for debugging and detecting when our websites are being viewed by in-app browsers.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/inappbrowser.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/inappbrowser.com\/<\/a>\n<ul class=\"wp-block-list\">\n<li>Attempts to detect if there\u2019s any injected JavaScript code running.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><a href=\"https:\/\/inappdebugger.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/inappdebugger.com\/<\/a>\n<ul class=\"wp-block-list\">\n<li>Attempts to detect you&#8217;re in an in-app browser and, if so, which app it is inside of.<\/li>\n\n\n\n<li>Additionally provides some debugging tests for if downloads are possible and escape-hatches for getting to an actual device browser.<\/li>\n\n\n\n<li>Leverages both inapp-spy and bowser.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><a href=\"https:\/\/github.com\/bowser-js\/bowser\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/github.com\/bowser-js\/bowser<\/a>\n<ul class=\"wp-block-list\">\n<li>A browser detection library providing metadata and filtering based on browser version.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><a href=\"https:\/\/github.com\/shalanah\/inapp-spy\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/github.com\/shalanah\/inapp-spy<\/a>\n<ul class=\"wp-block-list\">\n<li>A TypeScript library written by Shalanah Dawson that aids in detecting in-app browsers.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Escaping<\/h3>\n\n\n\n<p>Now that we have some tools, let\u2019s look at a example in JavaScript for detecting and redirecting in Android using an <code>intent:<\/code> link. You&#8217;d do this if you simply do not want your website being opened in an in-app browser, and offer a link to users to open it in their default browser instead. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> InAppSpy <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"inapp-spy\"<\/span>\n<span class=\"hljs-keyword\">const<\/span> { isInApp } = InAppSpy()\n\n<span class=\"hljs-comment\">\/\/ Your app's full URL, maybe defined build-time for different environments<\/span>\n<span class=\"hljs-keyword\">const<\/span> url = <span class=\"hljs-string\">`https:\/\/example.com`<\/span>\n<span class=\"hljs-keyword\">const<\/span> intentLink = <span class=\"hljs-string\">`intent:<span class=\"hljs-subst\">${url}<\/span>#Intent;end`<\/span>\n\n<span class=\"hljs-comment\">\/\/ 1. Detect in-app<\/span>\n<span class=\"hljs-keyword\">if<\/span> (isInApp) {\n\n  <span class=\"hljs-comment\">\/\/ 2. Attempt to auto-redirect<\/span>\n  <span class=\"hljs-built_in\">window<\/span>.location.replace(intentLink)\n    \n  <span class=\"hljs-comment\">\/\/ 3. Append a native &lt;a&gt; with the same intent link<\/span>\n  <span class=\"hljs-keyword\">const<\/span> $div = <span class=\"hljs-built_in\">document<\/span>.createElement(<span class=\"hljs-string\">\"div\"<\/span>)\n  $div.innerHTML = <span class=\"hljs-string\">`\n    &lt;p&gt;Tap the button to open in your default browser&lt;\/p&gt;\n    &lt;a href=\"<span class=\"hljs-subst\">${intentLink}<\/span>\" target=\"_blank\"&gt;Open&lt;\/a&gt;\n  `<\/span>\n  <span class=\"hljs-built_in\">document<\/span>.body.appendChild($div)\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It\u2019s not ideal to have to load extra JavaScript for this, but it is reliable. This may be heavy handed, but for those of you working on particularly sensitive sites, it might be worth doing.<\/p>\n\n\n\n<p>To get an idea of a way this can be implemented, Shalanah\u2019s <a href=\"https:\/\/inappdebugger.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">inappdebugger.com<\/a> provides this functionality under the \u201cAndroid In-App Escape Links\u201d section.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"465\" height=\"1024\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/escape-links.jpeg?resize=465%2C1024&#038;ssl=1\" alt=\"\" class=\"wp-image-3038\" style=\"width:247px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/escape-links.jpeg?resize=465%2C1024&amp;ssl=1 465w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/escape-links.jpeg?resize=136%2C300&amp;ssl=1 136w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/escape-links.jpeg?resize=768%2C1691&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/escape-links.jpeg?resize=698%2C1536&amp;ssl=1 698w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/escape-links.jpeg?resize=930%2C2048&amp;ssl=1 930w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/escape-links.jpeg?w=981&amp;ssl=1 981w\" sizes=\"auto, (max-width: 465px) 100vw, 465px\" \/><figcaption class=\"wp-element-caption\">Test out the Android escape hatch strategy on inappdebugger.com.<\/figcaption><\/figure>\n<\/div>\n\n\n<p>Unfortunately, there\u2019s currently no reliable way of handling iOS in-app browsers in terms of a proper escape hatch. Similar to Android, there\u2019s a handful of device-specific URI schemes (that\u2019s technically what the <code>intent:<\/code> prefix is called), but none of them are reliable to the default browser on a specific URL. A not-so-great workaround is using the <code>x-web-search:\/\/?<\/code> scheme, but the best-case is using the <code>site:<\/code> search prefix to get close to your actual URL e.g. <code>x-web-search:\/\/?site:example.com<\/code>.<\/p>\n\n\n\n<p><strong>Author Update:<\/strong> a somewhat reliable iOS workaround has been documented and tested by trying to run a Shortcut that doesn&#8217;t exist, specifying your URL in an error callback, and opening that in the user&#8217;s default browser. In practice, this looks like:<\/p>\n\n\n\n<p><code>shortcuts:\/\/x-callback-url\/run-shortcut?name=${crypto.randomUUID()}&amp;x-error=${encodeURIComponent('<a href=\"https:\/\/example.com\/\">https:\/\/example.com<\/a>')}<\/code><\/p>\n\n\n\n<p>This comes with some side effect caveats: the Shortcuts app is opened on the user&#8217;s device and some query parameters are appended to your URL.\u00a0<a href=\"https:\/\/github.com\/shalanah\/inapp-debugger\/pull\/2\">Read more on GitHub<\/a>.<\/p>\n\n\n\n<p>A last-ditch effort on iOS would be creating a UI element <em>in your web app<\/em> that gives the user manual instructions for bailing:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Tapping the \u201c\u2026\u201d menu<\/li>\n\n\n\n<li>Tapping on \u201cOpen in browser\u201d<\/li>\n<\/ol>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"354\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/e12rGCUs.png?resize=1024%2C354&#038;ssl=1\" alt=\"Screenshot of a UI that points to the upper right of the screen saying:\n\n1. Click on the overflow menu (\u2022\u2022\u2022)\n2. Then click Open in browser\" class=\"wp-image-3044\" style=\"width:412px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/e12rGCUs.png?resize=1024%2C354&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/e12rGCUs.png?resize=300%2C104&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/e12rGCUs.png?resize=768%2C265&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/e12rGCUs.png?w=1250&amp;ssl=1 1250w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n<\/div>\n\n\n<p>This is considerably more fragile and error-prone, but if you have the metrics to where your user traffic is coming from and <em>which<\/em> in-app browser is preventing them from converting to your <a href=\"https:\/\/frontendmasters.com\/workshops\/pwas-v2\/?utm_source=boost&amp;utm_medium=blog&amp;utm_campaign=boost\" target=\"_blank\" rel=\"noreferrer noopener\">feature-rich PWA<\/a> then it could be worth considering.<\/p>\n\n\n\n<p>Hopefully, with time, we\u2019ll see the fall of in-app browsers. The privacy and security concerns alone are unacceptable. Couple that with the limited functionality and poor user experience, it&#8217;s probably best they just went away. Thanks to groups like the Open Web Advocacy and individuals like Shalanah Dawson and Felix Krause for their work and support for this cause.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Recommended Reading<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Shalanah Dawson\n<ul class=\"wp-block-list\">\n<li>Talk: <a href=\"https:\/\/github.com\/shalanah\/talks\/blob\/master\/2024-02_Inapp_JSMN.md\">Solving the In-App Escape Room<\/a> (This talk at JavaScript MN is what got me curious about all this.)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Felix Krause\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/krausefx.com\/blog\/ios-privacy-instagram-and-facebook-can-track-anything-you-do-on-any-website-in-their-in-app-browser\">iOS Privacy: Instagram and Facebook can track anything you do on any website in their in-app browser<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/krausefx.com\/blog\/announcing-inappbrowsercom-see-what-javascript-commands-get-executed-in-an-in-app-browser\">iOS Privacy: Announcing InAppBrowser.com &#8211; see what JavaScript commands get injected through an in-app browser<\/a><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Open Web Advocacy\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/open-web-advocacy.org\/blog\/in-app-browsers-the-worst-erosion-of-user-choice-you-havent-heard-of\/\">In-App Browsers: The worst erosion of user choice you haven&#8217;t heard of<\/a><\/li>\n\n\n\n<li>(PDF): <a href=\"https:\/\/open-web-advocacy.org\/files\/OWA%20-%20DMA%20Interventions%20-%20In-App%20Browsers%20v1.2.pdf\">Digital Markets Act &#8211; Interventions In-App Browsers<\/a><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>The Meta in-app browser lawsuit\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/regmedia.co.uk\/2022\/09\/22\/willis_v_meta.pdf\">Initial lawsuit<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/storage.courtlistener.com\/recap\/gov.uscourts.cand.400497\/gov.uscourts.cand.400497.79.0.pdf\">Reply in support of dismissal<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/storage.courtlistener.com\/recap\/gov.uscourts.cand.400497\/gov.uscourts.cand.400497.95.0.pdf\">Dismissal<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>You should know that in-app browser can and do literally inject JavaScript into the websites you visit with them with tracking scripts from the app you&#8217;re inside of. And that&#8217;s just one thing that sucks about them.<\/p>\n","protected":false},"author":28,"featured_media":3048,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[88,210,13,70,197],"class_list":["post-3008","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-browsers","tag-in-app-browsers","tag-mobile-development","tag-performance","tag-security"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2024\/07\/cHJpdmF0ZS9zdGF0aWMvaW1hZ2Uvd2Vic2l0ZS8yMDIyLTA0L2xyL2Zyd2FybmluZ19zaWduX3NpZ25fd2FybmluZy1pbWFnZS1reWJhYjFtaC5qcGc.webp?fit=1024%2C678&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3008","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/28"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=3008"}],"version-history":[{"count":17,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3008\/revisions"}],"predecessor-version":[{"id":3117,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/3008\/revisions\/3117"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/3048"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=3008"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=3008"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=3008"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}