Safari In-App (aka WebView) Detection with JavaScript on iOS

Date: May 10, 2024

View on github.

The problem

There's no method to detect whether your webpage is opened in Safari or in iOS internal browser (In-App aka WebView). Ofcourse, you can detect Instagram or Facebook WebViews because they clearly change userAgent string. But, for example, Telegram or Slack doesn't. This could affect some features that your app would want to use. For example, propose to install to homescreen. You just can't do this inside In-App.

The solution

Implement detection algorithm based on checking window.innerHeight property, that actually represents the vertical height in pixels available to you webapp. This height is slightly greater for In-App compared to Safari (approx. 6 to 9 px). It also depends on device height, i.e. iPhone model. Knowing the height of all models we can solve the task. The combination of three components could give you the answer:
— iOS version (affects Safari height)
— screen.height (depend on iPhone model)
— window.innerHeight (depend on the app used - Safari or In-App)

The script

Apparently, it's more easier and reliable to detect if we're inside In-App instead of detecting if we're in Safari, here's why:
— In case we can't be 100% sure whether we're in Safari or In-App, we need to consider that we're in Safari. That's because it's better to show the user incorrect information than not to show anything;
— Safari height depend on iOS version as well as Safari settings (address bar on top/bottom). In-App height is equal through all devices at least in iOS 17, 16, 15;
— There's much more probability that Apple will change something in Safari interface than in In-App interface.

So, let's stick to the following logic:
— check iOS version, screen.height, window.innerHeight;
— if the values matches In-App values from the table, we're inside In-App;
— else we're inside Safari;
— that's why we need only one function detect_inapp(): boolean;
— but! Let's add one more return value (NULL) for the case a) we're not on iOS at all or b) not on iPhone or c) not on Safari or d) iOS version < 15.

    function detect_inapp() {
        const inapp_data = {"932":[746],"852":[666],"926":[752],"844":[670],"812":[635,641],"667":[559],"896":[725,721],"736":[628],"568":[460]};
        const is_ios_supported = !!navigator.userAgent.match(/iPhone OS 15_|iPhone OS 16_|iPhone OS 17_/i);
        const is_ios17 = !!navigator.userAgent.match(/iOS 17/i);
        const is_safari = !!navigator.userAgent.match(/Safari/i) && !!navigator.userAgent.match(/Mobile/i) && !!navigator.userAgent.match(/Version/i);
        const screen_h = screen.height;
        const window_h = window.innerHeight;
        if(is_ios_supported && is_safari && inapp_data[screen_h].length) {
            if(screen_h == 812 && window_h == 635) return is_ios17; // ambiguity case
            return inapp_data[screen_h].indexOf(window_h) !== -1;
        }
        return null;
    }

The data

iPhoneiOSPhysical heightLogical heightSafari innerHeightIn-App innerHeight
iPhone 15 Pro MaxiOS 17.42796 px932739746
iPhone 15 ProiOS 17.12556 px852659666
iPhone 15 PlusiOS 17.42796 px932739746
iPhone 15iOS 17.42556 px852659666
iPhone 14 Pro MaxiOS 17.42778 px932739746
iPhone 14 ProiOS 17.42556 px852659666
iPhone 14 PlusiOS 17.42778 px926745752
iPhone 14iOS 17.42532 px844663670
iPhone 14iOS 16.42532 px844664670
iPhone 13 Pro MaxiOS 17.42778 px932739746
iPhone 13 ProiOS 17.42532 px844663670
iPhone 13 ProiOS 15.52532 px844664670
iPhone 13iOS 17.42532 px844663670
iPhone 13 miniiOS 17.42340 px812628635
iPhone SE (3-gen)iOS 17.41334px667547559
iPhone 12 ProiOS 17.42532 px844663670
iPhone 12 Pro MaxiOS 17.42778 px926745752
iPhone 12 Pro MaxiOS 16.02778 px926746752
iPhone 12 miniiOS 17.42340 px812628635
iPhone 12iOS 17.42532 px844663670
iPhone SE (2-gen)iOS 17.41334px667547559
iPhone 11 Pro MaxiOS 17.42688 px896718725
iPhone 11 ProiOS 17.42436 px812634641
iPhone 11iOS 17.41792 px896714721
iPhone XSiOS 17.42436 px812634641
iPhone XSiOS 16.62436 px812635641
iPhone XSiOS 15.52436 px812635641
iPhone XS MaxiOS 17.42688 px896718725
iPhone XRiOS 17.3 top1792 px896714721
iPhone XiOS 17.42436 px812634641
iPhone 8 PlusiOS 16.7 top1920 px736620628
iPhone 8iOS 16.41334 px667548559
iPhone 7 PlusiOS 15.51920 px736617628
iPhone 7iOS 15.51334 px667548559
iPhone 6s PlusiOS 15.51920 px736617628
iPhone 6siOS 15.81334 px667548559
iPhone 6 PlusiOS 15.51920 px736617628
iPhone 6iOS 141334 px667
iPhone SE (1-gen)iOS 15.51136 px568449460

Height pairs analytics

Safari height pairs: 568-449, 667-547, 667-548, 736-617, 736-620, 812-628, 812-634, 812-635, 844-663, 844-664, 852-659, 896-714, 896-718, 926-745, 926-746, 932-739
In-App height pairs: 568-460, 667-559, 736-628, 812-635, 812-641, 844-670, 852-666, 896-721, 896-725, 926-752, 932-746
Intersection: 812-635

In-App data as json:

{"932":[746],"852":[666],"926":[752],"844":[670],"812":[635,641],"667":[559],"896":[725,721],"736":[628],"568":[460]}

Amiguity case

As you see from pair analytics, there's one amiguity case (intersection) between iPhone XS @ iOS 16/15 and iPhone 12/13 mini (812 screen height). For these devices window.innerHeight = 635 both in Safari and in In-App. We'll follow this simple logic to increase the chance of right detection:
— If a user has more latest model of iPhone (12 mini or 13 mini), there are more chances of latest iOS (17) installed;
— Safari height = 635 only on iPhone XS @ iOS 16/15. In case iOS is 17, the height will be 634, which is perfect (no ambiguity in this case);
— So, with height = 635, there are more chances that this is In-App, unless iOS version is strictly lower than 17.

One more thing you might want to know before you go

There are acutally two types of "In-App":
1) Safari native In-App — the one we've explored here;
2) Non-native In-App — the one you can find in Instagram, Facebook, Viber and some other apps. It still uses Safari as an engine but the UI looks differently.
According to my research, you can detect second type by userAgent string in all cases (see below). So, there's no need to use the above script for second case, only for the first one.

Appendix 1. Device height vs window height

Safari
Array
(
    [932] => Array
        (
            [739] => iPhone 15 Pro Max @ iOS 17.4  iPhone 15 Plus @ iOS 17.4  iPhone 14 Pro Max @ iOS 17.4  iPhone 13 Pro Max @ iOS 17.4  
        )

    [852] => Array
        (
            [659] => iPhone 15 Pro @ iOS 17.1  iPhone 15 @ iOS 17.4  iPhone 14 Pro @ iOS 17.4  
        )

    [926] => Array
        (
            [745] => iPhone 14 Plus @ iOS 17.4  iPhone 12 Pro Max @ iOS 17.4  
            [746] => iPhone 12 Pro Max @ iOS 16.0  
        )

    [844] => Array
        (
            [663] => iPhone 14 @ iOS 17.4  iPhone 13 Pro @ iOS 17.4  iPhone 13 @ iOS 17.4  iPhone 12 Pro @ iOS 17.4  iPhone 12 @ iOS 17.4  
            [664] => iPhone 14 @ iOS 16.4  iPhone 13 Pro @ iOS 15.5  
        )

    [812] => Array
        (
            [628] => iPhone 13 mini @ iOS 17.4  iPhone 12 mini @ iOS 17.4  
            [634] => iPhone 11 Pro @ iOS 17.4  iPhone XS @ iOS 17.4  iPhone X @ iOS 17.4  
            [635] => iPhone XS @ iOS 16.6  iPhone XS @ iOS 15.5  
        )

    [667] => Array
        (
            [547] => iPhone SE (3-gen) @ iOS 17.4  iPhone SE (2-gen) @ iOS 17.4  
            [548] => iPhone 8 @ iOS 16.4  iPhone 7 @ iOS 15.5  iPhone 6s @ iOS 15.8  
        )

    [896] => Array
        (
            [718] => iPhone 11 Pro Max @ iOS 17.4  iPhone XS Max @ iOS 17.4  
            [714] => iPhone 11 @ iOS 17.4  iPhone XR @ iOS 17.3 top  
        )

    [736] => Array
        (
            [620] => iPhone 8 Plus @ iOS 16.7 top  
            [617] => iPhone 7 Plus @ iOS 15.5  iPhone 6s Plus @ iOS 15.5  iPhone 6 Plus @ iOS 15.5  
        )

    [568] => Array
        (
            [449] => iPhone SE (1-gen) @ iOS 15.5  
        )

)

In-App
Array
(
    [932] => Array
        (
            [746] => iPhone 15 Pro Max @ iOS 17.4  iPhone 15 Plus @ iOS 17.4  iPhone 14 Pro Max @ iOS 17.4  iPhone 13 Pro Max @ iOS 17.4  
        )

    [852] => Array
        (
            [666] => iPhone 15 Pro @ iOS 17.1  iPhone 15 @ iOS 17.4  iPhone 14 Pro @ iOS 17.4  
        )

    [926] => Array
        (
            [752] => iPhone 14 Plus @ iOS 17.4  iPhone 12 Pro Max @ iOS 17.4  iPhone 12 Pro Max @ iOS 16.0  
        )

    [844] => Array
        (
            [670] => iPhone 14 @ iOS 17.4  iPhone 14 @ iOS 16.4  iPhone 13 Pro @ iOS 17.4  iPhone 13 Pro @ iOS 15.5  iPhone 13 @ iOS 17.4  iPhone 12 Pro @ iOS 17.4  iPhone 12 @ iOS 17.4  
        )

    [812] => Array
        (
            [635] => iPhone 13 mini @ iOS 17.4  iPhone 12 mini @ iOS 17.4  
            [641] => iPhone 11 Pro @ iOS 17.4  iPhone XS @ iOS 17.4  iPhone XS @ iOS 16.6  iPhone XS @ iOS 15.5  iPhone X @ iOS 17.4  
        )

    [667] => Array
        (
            [559] => iPhone SE (3-gen) @ iOS 17.4  iPhone SE (2-gen) @ iOS 17.4  iPhone 8 @ iOS 16.4  iPhone 7 @ iOS 15.5  iPhone 6s @ iOS 15.8  
        )

    [896] => Array
        (
            [725] => iPhone 11 Pro Max @ iOS 17.4  iPhone XS Max @ iOS 17.4  
            [721] => iPhone 11 @ iOS 17.4  iPhone XR @ iOS 17.3 top  
        )

    [736] => Array
        (
            [628] => iPhone 8 Plus @ iOS 16.7 top  iPhone 7 Plus @ iOS 15.5  iPhone 6s Plus @ iOS 15.5  iPhone 6 Plus @ iOS 15.5  
        )

    [568] => Array
        (
            [460] => iPhone SE (1-gen) @ iOS 15.5  
        )

)

Appendix 2. UserAgents for iPhone

// Home Screen
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 && navigator.standalone = true
// Safari
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1
// Telegram
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1
// Slack
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1
// Brave
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1
// Opera
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 OPT/4.6.3
// Chrome
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/123.0.6312.52 Mobile/15E148 Safari/604.1
// Firefox
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/125.1 Mobile/15E148 Safari/605.1.15
// Firefox Focus
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/125 Mobile/15E148 Version/15.0
// Edge
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/123.0.2420.104 Version/16.0 Mobile/15E148 Safari/604.1
// Instagram
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/20G81 Instagram 327.1.6.30.88 (iPhone11,2; iOS 16_6_1; en_UA; en; scale=3.00; 1125x2436; 588348860)
// Facebook
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBAV/453.1.0.40.112;FBBV/586716179;FBDV/iPhone11,2;FBMD/iPhone;FBSN/iOS;FBSV/16.6.1;FBSS/3;FBCR/;FBID/phone;FBLC/en;FBOP/80]
// LinkedIn
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [LinkedInApp]/9.29.5227
// Viber
Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)