UNPKG

blockstack

Version:

The Blockstack Javascript library for authentication, identity, and storage.

160 lines 8.51 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../utils"); const logger_1 = require("../logger"); /** * Detects if the native auth-browser is installed and is successfully * launched via a custom protocol URI. * @param {String} authRequest * The encoded authRequest to be used as a query param in the custom URI. * @param {String} successCallback * The callback that is invoked when the protocol handler was detected. * @param {String} failCallback * The callback that is invoked when the protocol handler was not detected. * @return {void} */ function launchCustomProtocol(authRequest, successCallback, failCallback) { // Create a unique ID used for this protocol detection attempt. const echoReplyID = Math.random().toString(36).substr(2, 9); const echoReplyKeyPrefix = 'echo-reply-'; const echoReplyKey = `${echoReplyKeyPrefix}${echoReplyID}`; const apis = ['localStorage', 'document', 'setTimeout', 'clearTimeout', 'addEventListener', 'removeEventListener']; apis.forEach((windowAPI) => utils_1.checkWindowAPI('detectProtocolLaunch', windowAPI)); // Use localStorage as a reliable cross-window communication method. // Create the storage entry to signal a protocol detection attempt for the // next browser window to check. window.localStorage.setItem(echoReplyKey, Date.now().toString()); const cleanUpLocalStorage = () => { try { window.localStorage.removeItem(echoReplyKey); // Also clear out any stale echo-reply keys older than 1 hour. for (let i = 0; i < window.localStorage.length; i++) { const storageKey = window.localStorage.key(i); if (storageKey && storageKey.startsWith(echoReplyKeyPrefix)) { const storageValue = window.localStorage.getItem(storageKey); if (storageValue === 'success' || (Date.now() - parseInt(storageValue, 10)) > 3600000) { window.localStorage.removeItem(storageKey); } } } } catch (err) { logger_1.Logger.error('Exception cleaning up echo-reply entries in localStorage'); logger_1.Logger.error(err); } }; const detectionTimeout = 1000; let redirectToWebAuthTimer = 0; const cancelWebAuthRedirectTimer = () => { if (redirectToWebAuthTimer) { window.clearTimeout(redirectToWebAuthTimer); redirectToWebAuthTimer = 0; } }; const startWebAuthRedirectTimer = (timeout = detectionTimeout) => { cancelWebAuthRedirectTimer(); redirectToWebAuthTimer = window.setTimeout(() => { if (redirectToWebAuthTimer) { cancelWebAuthRedirectTimer(); let nextFunc; if (window.localStorage.getItem(echoReplyKey) === 'success') { logger_1.Logger.info('Protocol echo reply detected.'); nextFunc = successCallback; } else { logger_1.Logger.info('Protocol handler not detected.'); nextFunc = failCallback; } failCallback = () => { }; successCallback = () => { }; cleanUpLocalStorage(); // Briefly wait since localStorage changes can // sometimes be ignored when immediately redirected. setTimeout(() => nextFunc(), 100); } }, timeout); }; startWebAuthRedirectTimer(); const inputPromptTracker = window.document.createElement('input'); inputPromptTracker.type = 'text'; const inputStyle = inputPromptTracker.style; // Prevent this element from inherited any css. inputStyle.all = 'initial'; // Setting display=none on an element prevents them from being focused/blurred. // So hide the element using other properties.. inputStyle.opacity = '0'; inputStyle.filter = 'alpha(opacity=0)'; inputStyle.height = '0'; inputStyle.width = '0'; // If the the focus of a page element is immediately changed then this likely indicates // the protocol handler is installed, and the browser is prompting the user if they want // to open the application. const inputBlurredFunc = () => { // Use a timeout of 100ms to ignore instant toggles between blur and focus. // Browsers often perform an instant blur & focus when the protocol handler is working // but not showing any browser prompts, so we want to ignore those instances. let isRefocused = false; inputPromptTracker.addEventListener('focus', () => { isRefocused = true; }, { once: true, capture: true }); setTimeout(() => { if (redirectToWebAuthTimer && !isRefocused) { logger_1.Logger.info('Detected possible browser prompt for opening the protocol handler app.'); window.clearTimeout(redirectToWebAuthTimer); inputPromptTracker.addEventListener('focus', () => { if (redirectToWebAuthTimer) { logger_1.Logger.info('Possible browser prompt closed, restarting auth redirect timeout.'); startWebAuthRedirectTimer(); } }, { once: true, capture: true }); } }, 100); }; inputPromptTracker.addEventListener('blur', inputBlurredFunc, { once: true, capture: true }); setTimeout(() => inputPromptTracker.removeEventListener('blur', inputBlurredFunc), 200); window.document.body.appendChild(inputPromptTracker); inputPromptTracker.focus(); // Detect if document.visibility is immediately changed which is a strong // indication that the protocol handler is working. We don't know for sure and // can't predict future browser changes, so only increase the redirect timeout. // This reduces the probability of a false-negative (where local auth works, but // the original page was redirect to web auth because something took too long), const pageVisibilityChanged = () => { if (window.document.hidden && redirectToWebAuthTimer) { logger_1.Logger.info('Detected immediate page visibility change (protocol handler probably working).'); startWebAuthRedirectTimer(3000); } }; window.document.addEventListener('visibilitychange', pageVisibilityChanged, { once: true, capture: true }); setTimeout(() => window.document.removeEventListener('visibilitychange', pageVisibilityChanged), 500); // Listen for the custom protocol echo reply via localStorage update event. window.addEventListener('storage', function replyEventListener(event) { if (event.key === echoReplyKey && window.localStorage.getItem(echoReplyKey) === 'success') { // Custom protocol worked, cancel the web auth redirect timer. cancelWebAuthRedirectTimer(); inputPromptTracker.removeEventListener('blur', inputBlurredFunc); logger_1.Logger.info('Protocol echo reply detected from localStorage event.'); // Clean up event listener and localStorage. window.removeEventListener('storage', replyEventListener); const nextFunc = successCallback; successCallback = () => { }; failCallback = () => { }; cleanUpLocalStorage(); // Briefly wait since localStorage changes can sometimes // be ignored when immediately redirected. setTimeout(() => nextFunc(), 100); } }, false); // Use iframe technique for launching the protocol URI rather than setting `window.location`. // This method prevents browsers like Safari, Opera, Firefox from showing error prompts // about unknown protocol handler when app is not installed, and avoids an empty // browser tab when the app is installed. logger_1.Logger.info('Attempting protocol launch via iframe injection.'); const locationSrc = `${utils_1.BLOCKSTACK_HANDLER}:${authRequest}&echo=${echoReplyID}`; const iframe = window.document.createElement('iframe'); const iframeStyle = iframe.style; iframeStyle.all = 'initial'; iframeStyle.display = 'none'; iframe.src = locationSrc; window.document.body.appendChild(iframe); } exports.launchCustomProtocol = launchCustomProtocol; //# sourceMappingURL=protocolLaunch.js.map