1(function (root, factory) { 2 if (typeof module === 'object' && module.exports) module.exports = factory(); 3 else root.InAppEscape = factory(); 4})(typeof self !== 'undefined' ? self : this, function () { 5 'use strict'; 6 7 // Webview signatures. FBAN/FBAV = Facebook iOS/Android, GSA = Google 8 // Search app (Google app / Discover feed in-app browser), "wv" = Android 9 // WebView. 10 var IN_APP = /(Instagram|FBAN|FBAV|FB_IAB|Messenger|Line|Snapchat|TikTok|musical_ly|Pinterest|WeChat|MicroMessenger|LinkedInApp|Twitter|GSA|GoogleSearch|InApp|WebView|; wv)/i; 11 var INSTAGRAM = /instagram/i; 12 var IOS = /(iPhone|iPod|iPad)/i; 13 var ANDROID = /Android/i; 14 15 var STORAGE_KEY = 'inapp-escape:attempted'; 16 17 function ua() { 18 return (navigator && navigator.userAgent) || ''; 19 } 20 21 function isInApp() { 22 return IN_APP.test(ua()); 23 } 24 25 function isIOS() { 26 // iPadOS 13+ reports a desktop UA, so fall back to the touch-point check. 27 return IOS.test(ua()) || 28 (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); 29 } 30 31 function isAndroid() { 32 return ANDROID.test(ua()); 33 } 34 35 function isInstagram() { 36 return INSTAGRAM.test(ua()); 37 } 38 39 /** 40 * Build the platform-specific URL that asks the OS to hand off to the 41 * native browser. Returns null when there is no known handoff for this 42 * platform (desktop, or an OS we have no scheme for). 43 * 44 * None of these are documented, supported APIs â they are observed app 45 * behaviour and can stop working after any OS or app update. Always keep a 46 * fallback path for users 47 */ 48 function nativeUrlFor(target) { 49 var bare = target.replace(/^https?:\/\//, ''); 50 51 if (isIOS()) { 52 // Instagram's app-specific handoff. Everything else gets Safari's 53 // private scheme, which more in-app browsers honour than you'd expect. 54 return isInstagram() 55 ? 'instagram://extbrowser/?url=' + encodeURIComponent(target) 56 : 'x-safari-https://' + bare; 57 } 58 59 if (isAndroid()) { 60 // Documented Android mechanism: hand the URL to the default browser. 61 return 'intent://' + bare + '#Intent;scheme=https;end'; 62 } 63 64 return null; 65 } 66 67 function sameAsCurrentPage(target) { 68 try { 69 return new URL(target, location.href).href === location.href; 70 } catch (e) { 71 return target === location.href; 72 } 73 } 74 75 // sessionStorage throws in some webviews (private mode, blocked storage), 76 // so every access is guarded and failure degrades to "not yet attempted". 77 function alreadyAttempted() { 78 try { 79 return sessionStorage.getItem(STORAGE_KEY) === '1'; 80 } catch (e) { 81 return false; 82 } 83 } 84 85 function markAttempted() { 86 try { 87 sessionStorage.setItem(STORAGE_KEY, '1'); 88 } catch (e) {} 89 } 90 91 /** 92 * Attempt the escape. 93 * 94 * @param {Object} [opts] 95 * @param {string} [opts.url] Where to send them. Default: current URL. 96 * @param {boolean} [opts.fallback] If the handoff fails, navigate to url 97 * anyway inside the webview. Ignored when 98 * url is the current page (that would just 99 * reload and re-trigger). Default true. 100 * @param {number} [opts.timeout] ms to wait before deciding it failed. 101 * @param {boolean} [opts.once] Only ever attempt once per tab. Default true. 102 * @returns {boolean} whether a handoff was attempted. 103 */ 104 function escape(opts) { 105 opts = opts || {}; 106 107 var target = opts.url || location.href; 108 var useFallback = opts.fallback !== false; 109 var timeout = typeof opts.timeout === 'number' ? opts.timeout : 500; 110 var once = opts.once !== false; 111 112 if (!isInApp()) return false; 113 if (once && alreadyAttempted()) return false; 114 115 var nativeUrl = nativeUrlFor(target); 116 if (!nativeUrl) return false; 117 118 if (once) markAttempted(); 119 120 // Redirecting to the page we're already on would reload the webview and 121 // run this script again â so only ever fall back to a *different* URL. 122 if (useFallback) { 123 var timer = setTimeout(function () { 124 location.href = target; 125 }, timeout); 126 127 // If the native browser really opened, this tab gets backgrounded. 128 // That's our signal the handoff worked, so cancel the fallback. 129 var cancel = function () { 130 clearTimeout(timer); 131 }; 132 document.addEventListener('visibilitychange', function () { 133 if (document.hidden) cancel(); 134 }); 135 window.addEventListener('pagehide', cancel); 136 window.addEventListener('blur', cancel); 137 } 138 139 location.href = nativeUrl; 140 return true; 141 } 142 143 var api = { 144 escape: escape, 145 isInApp: isInApp, 146 isIOS: isIOS, 147 isAndroid: isAndroid, 148 isInstagram: isInstagram, 149 nativeUrlFor: nativeUrlFor, 150 }; 151 152 // Auto-run unless the tag opts out with data-auto="false". 153 if (typeof document !== 'undefined') { 154 var self = document.currentScript; 155 if (!self || self.getAttribute('data-auto') !== 'false') { 156 escape(); 157 } 158 } 159 160 return api; 161});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.