diff --git a/assets/js/app.js b/assets/js/app.js index 5695e3fa7..6fb8dcc0c 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -18,43 +18,97 @@ import { loadAppAuthToken } from "./lib/app"; import { settingsStore } from "./lib/settings"; import { registerTopbar, registerGlobalEventHandlers } from "./events"; -const csrfToken = document - .querySelector("meta[name='csrf-token']") - .getAttribute("content"); +function connect() { + const csrfToken = document + .querySelector("meta[name='csrf-token']") + .getAttribute("content"); -const liveSocket = new LiveSocket( - window.LIVEBOOK_BASE_URL_PATH + "/live", - Socket, - { - params: (liveViewName) => { - return { - _csrf_token: csrfToken, - // Pass the most recent user data to the LiveView in `connect_params` - user_data: loadUserData(), - app_auth_token: loadAppAuthToken(), - }; - }, - hooks: hooks, - dom: morphdomOptions, - } -); + const liveSocket = new LiveSocket( + window.LIVEBOOK_BASE_URL_PATH + "/live", + Socket, + { + params: (liveViewName) => { + return { + _csrf_token: csrfToken, + // Pass the most recent user data to the LiveView in `connect_params` + user_data: loadUserData(), + app_auth_token: loadAppAuthToken(), + }; + }, + hooks: hooks, + dom: morphdomOptions, + } + ); -// Show progress bar on live navigation and form submits -registerTopbar(); + // Show progress bar on live navigation and form submits + registerTopbar(); -// Handle custom events dispatched with JS.dispatch/3 -registerGlobalEventHandlers(); + // Handle custom events dispatched with JS.dispatch/3 + registerGlobalEventHandlers(); -// Reflect global configuration in attributes to enable CSS rules -settingsStore.getAndSubscribe((settings) => { - document.body.setAttribute("data-editor-theme", settings.editor_theme); -}); + // Reflect global configuration in attributes to enable CSS rules + settingsStore.getAndSubscribe((settings) => { + document.body.setAttribute("data-editor-theme", settings.editor_theme); + }); -// Connect if there are any LiveViews on the page -liveSocket.connect(); + // Connect if there are any LiveViews on the page + liveSocket.connect(); -// Expose liveSocket on window for web console debug logs and latency simulation: -// >> liveSocket.enableDebug() -// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session -// >> liveSocket.disableLatencySim() -window.liveSocket = liveSocket; + // Expose liveSocket on window for web console debug logs and latency simulation: + // >> liveSocket.enableDebug() + // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session + // >> liveSocket.disableLatencySim() + window.liveSocket = liveSocket; +} + +// When Livebook runs in a cross-origin iframe the browser may restrict access +// to cookies. This is the case in Safari with the "Prevent cross-site tracking" +// option enabled, which is the default. Without cookies access, the session +// is not stored, so CSRF tokens are invalid. Consequently, LV keeps reloading +// the page, as we try to connect the socket with invalid token. To work around +// this we need to ask to explicitly grant access to cookies, as outlined in (1). +// +// (1): https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API + +if (document.hasStorageAccess) { + document.hasStorageAccess().then((hasStorageAccess) => { + if (hasStorageAccess) { + connect(); + } else { + const overlayEl = document.createElement("div"); + + overlayEl.innerHTML = ` +
+
+
+ Action required +
+
+ It looks like Livebook does not have access to cookies. This usually happens when + it runs in an iframe. To make the app functional you need to grant Livebook access + to its cookies explicitly. +
+
+ +
+
+
+ `; + + document.body.appendChild(overlayEl); + + const grantAccessButtonEl = overlayEl.querySelector("#grant-access"); + + grantAccessButtonEl.addEventListener("click", (event) => { + document + .requestStorageAccess() + .then(() => window.location.reload()) + .catch(() => console.log("Access to storage denied")); + }); + } + }); +} else { + connect(); +}