blob: d9fdc08171a03b4cdb69c244a628018e9174fc06 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './strings.m.js';
import {assertNotReached} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {getRequiredElement} from 'chrome://resources/js/util.js';
import type {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';
import type {Origin} from 'chrome://resources/mojo/url/mojom/origin.mojom-webui.js';
import type {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';
import type {IwaDevModeLocation} from './web_app_internals.mojom-webui.js';
import {WebAppInternalsHandler} from './web_app_internals.mojom-webui.js';
const webAppInternalsHandler = WebAppInternalsHandler.getRemote();
const debugInfoAsJsonString: Promise<string> =
webAppInternalsHandler.getDebugInfoAsJsonString().then(
response => response.result);
const iwaInstallButton =
getRequiredElement('iwa-install-button') as HTMLButtonElement;
const iwaInstallUrl = getRequiredElement('iwa-install-url') as HTMLInputElement;
const iwaSelectFileButton =
getRequiredElement('iwa-select-bundle') as HTMLButtonElement;
const iwaSearchForUpdatesButton = getRequiredElement('iwa-search-for-updates');
/**
* Converts a mojo origin into a user-readable string, omitting default ports.
* @param origin Origin to convert
*
* TODO(b/304717391): Extract origin serialization logic from here and use it
* everywhere `url.mojom.Origin` is serialized in JS/TS.
*/
function originToText(origin: Origin): string {
if (origin.host.length === 0) {
return 'null';
}
let result = origin.scheme + '://' + origin.host;
if (!(origin.scheme === 'https' && origin.port === 443) &&
!(origin.scheme === 'http' && origin.port === 80)) {
result += ':' + origin.port;
}
return result;
}
/**
* Converts a mojo representation of `base::FilePath` into a user-readable
* string.
* @param filePath File path to convert
*/
function filePathToText(filePath: FilePath): string {
if (typeof filePath.path === 'string') {
return filePath.path;
}
const decoder = new TextDecoder('utf-16');
const buffer = new Uint16Array(filePath.path);
return decoder.decode(buffer);
}
getRequiredElement('copy-button').addEventListener('click', async () => {
navigator.clipboard.writeText(await debugInfoAsJsonString);
});
getRequiredElement('download-button').addEventListener('click', async () => {
const url = URL.createObjectURL(new Blob([await debugInfoAsJsonString], {
type: 'application/json',
}));
const a = document.createElement('a');
a.href = url;
a.download = 'web_app_internals.json';
a.click();
// Downloading succeeds even if the URL was revoked during downloading. See
// the spec for details (https://w3c.github.io/FileAPI/#dfn-revokeObjectURL):
//
// "Note: ... Requests that were started before the url was revoked should
// still succeed."
URL.revokeObjectURL(url);
});
function iwaInstallStateUpdate() {
iwaInstallButton.disabled = (iwaInstallUrl.value.length === 0);
}
async function iwaInstallSubmit() {
iwaInstallButton.disabled = true;
const iwaInstallMessageDiv = getRequiredElement('iwa-install-message-div');
// Validate the provided URL.
let valid = false;
try {
// We don't need the result of this, only to verify it doesn't throw an
// exception.
new URL(iwaInstallUrl.value);
valid =
(iwaInstallUrl.value.startsWith('http:') ||
iwaInstallUrl.value.startsWith('https:'));
} catch (_) {
// Fall-through.
}
if (!valid) {
iwaInstallMessageDiv.innerText =
`Installing IWA: ${iwaInstallUrl.value} is not a valid URL`;
iwaInstallStateUpdate();
return;
}
iwaInstallMessageDiv.innerText = `Installing IWA: ${iwaInstallUrl.value}...`;
const location: Url = {url: iwaInstallUrl.value};
const installFromDevProxy =
await webAppInternalsHandler.installIsolatedWebAppFromDevProxy(location);
if (installFromDevProxy.result.success) {
iwaInstallMessageDiv.innerText =
`Installing IWA: ${iwaInstallUrl.value} successfully installed.`;
iwaInstallUrl.value = '';
iwaInstallStateUpdate();
return;
}
iwaInstallMessageDiv.innerText =
`Installing IWA: ${iwaInstallUrl.value} failed to install: ${
installFromDevProxy.result.error}`;
iwaInstallStateUpdate();
}
iwaInstallUrl.addEventListener('enter', iwaInstallSubmit);
iwaInstallButton.addEventListener('click', iwaInstallSubmit);
function updateIwaInstallButtonState(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault();
iwaInstallSubmit();
return;
}
iwaInstallStateUpdate();
}
iwaInstallUrl.addEventListener('keyup', updateIwaInstallButtonState);
iwaInstallStateUpdate();
async function iwaSelectFile() {
const iwaInstallMessageDiv = getRequiredElement('iwa-install-message-div');
iwaInstallMessageDiv.innerText = `Installing IWA from bundle...`;
const installFromDevBundle =
await webAppInternalsHandler
.selectFileAndInstallIsolatedWebAppFromDevBundle();
if (installFromDevBundle.result.success) {
iwaInstallMessageDiv.innerText = `Installing IWA: successfully installed.`;
return;
}
iwaInstallMessageDiv.innerText =
`Installing IWA: failed to install: ${installFromDevBundle.result.error}`;
}
iwaSelectFileButton.addEventListener('click', iwaSelectFile);
async function iwaSearchForUpdates() {
const messageDiv = getRequiredElement('iwa-update-discovery-message-div');
messageDiv.innerText = `Queueing update discovery tasks...`;
const {result} =
await webAppInternalsHandler.searchForIsolatedWebAppUpdates();
messageDiv.innerText = result;
}
iwaSearchForUpdatesButton.addEventListener('click', iwaSearchForUpdates);
function formatDevModeLocation(location: IwaDevModeLocation): string {
if (location.proxyOrigin) {
return originToText(location.proxyOrigin);
}
if (location.bundlePath) {
return filePathToText(location.bundlePath);
}
assertNotReached();
}
document.addEventListener('DOMContentLoaded', async () => {
getRequiredElement('json').innerText = await debugInfoAsJsonString;
if (loadTimeData.getBoolean('experimentalAreIwasEnabled')) {
// Unhide the IWA div.
getRequiredElement('iwa-div').style.display = '';
if (loadTimeData.getBoolean('experimentalIsIwaDevModeEnabled')) {
// Unhide the IWA install div.
getRequiredElement('iwa-install-div').style.display = '';
const devModeAppList = getRequiredElement('iwa-dev-mode-app-list');
const {apps: devModeApps} =
await webAppInternalsHandler.getIsolatedWebAppDevModeAppInfo();
for (const devModeApp of devModeApps) {
const li = document.createElement('li');
li.innerText = `${devModeApp.name} (${devModeApp.installedVersion}) → ${
formatDevModeLocation(devModeApp.location)}`;
const updateMsg = document.createElement('p');
const updateBtn = document.createElement('button');
updateBtn.className = 'iwa-update-btn';
updateBtn.innerText = 'Perform update now';
updateBtn.onclick = async () => {
const oldText = updateBtn.innerText;
try {
updateBtn.disabled = true;
updateBtn.innerText =
'Performing update... (close the IWA if it is currently open!)';
if (devModeApp.location.bundlePath) {
const {result} =
await webAppInternalsHandler
.selectFileAndUpdateIsolatedWebAppFromDevBundle(
devModeApp.appId);
updateMsg.innerText = result;
} else if (devModeApp.location.proxyOrigin) {
const {result} =
await webAppInternalsHandler.updateDevProxyIsolatedWebApp(
devModeApp.appId);
updateMsg.innerText = result;
} else {
assertNotReached();
}
} finally {
updateBtn.innerText = oldText;
updateBtn.disabled = false;
}
};
li.appendChild(updateBtn);
li.appendChild(updateMsg);
devModeAppList.appendChild(li);
}
// Unhide the div that hides the list of dev mode apps.
getRequiredElement('iwa-dev-mode-updates').style.display = '';
}
}
});