aladinnetwork-blockstack
Version:
The Aladin Javascript library for authentication, identity, and storage.
154 lines • 8.48 kB
JavaScript
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 { localStorage, document, setTimeout, clearTimeout, addEventListener, removeEventListener } = utils_1.getGlobalObjects(['localStorage', 'document', 'setTimeout', 'clearTimeout', 'addEventListener', 'removeEventListener'], { throwIfUnavailable: true, usageDesc: 'detectProtocolLaunch' });
// 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.
localStorage.setItem(echoReplyKey, Date.now().toString());
const cleanUpLocalStorage = () => {
try {
localStorage.removeItem(echoReplyKey);
// Also clear out any stale echo-reply keys older than 1 hour.
for (let i = 0; i < localStorage.length; i++) {
const storageKey = localStorage.key(i);
if (storageKey && storageKey.startsWith(echoReplyKeyPrefix)) {
const storageValue = localStorage.getItem(storageKey);
if (storageValue === 'success' || (Date.now() - parseInt(storageValue, 10)) > 3600000) {
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) {
clearTimeout(redirectToWebAuthTimer);
redirectToWebAuthTimer = 0;
}
};
const startWebAuthRedirectTimer = (timeout = detectionTimeout) => {
cancelWebAuthRedirectTimer();
redirectToWebAuthTimer = setTimeout(() => {
if (redirectToWebAuthTimer) {
cancelWebAuthRedirectTimer();
let nextFunc;
if (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 = document.createElement('input');
inputPromptTracker.type = 'text';
// Setting display:none on an element prevents them from being focused/blurred.
// So we hide using 0 width/height/opacity, and set position:fixed so that the
// page does not scroll when the element is focused.
const hiddenCssStyle = 'all: initial; position: fixed; top: 0; height: 0; width: 0; opacity: 0;';
inputPromptTracker.style.cssText = hiddenCssStyle;
// 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.');
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);
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 (document.hidden && redirectToWebAuthTimer) {
logger_1.Logger.info('Detected immediate page visibility change (protocol handler probably working).');
startWebAuthRedirectTimer(3000);
}
};
document.addEventListener('visibilitychange', pageVisibilityChanged, { once: true, capture: true });
setTimeout(() => document.removeEventListener('visibilitychange', pageVisibilityChanged), 500);
// Listen for the custom protocol echo reply via localStorage update event.
addEventListener('storage', function replyEventListener(event) {
if (event.key === echoReplyKey && 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.
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.ALADIN_HANDLER}:${authRequest}&echo=${echoReplyID}`;
const iframe = document.createElement('iframe');
const iframeStyle = 'all: initial; display: none; position: fixed; top: 0; height: 0; width: 0; opacity: 0;';
iframe.style.cssText = iframeStyle;
iframe.src = locationSrc;
document.body.appendChild(iframe);
}
exports.launchCustomProtocol = launchCustomProtocol;
//# sourceMappingURL=protocolLaunch.js.map
;