From 859f8d0667d17d39e92ffd7b045285f0cfad77b8 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 27 Aug 2026 19:21:46 +0530 Subject: [PATCH] fix: run the migration without a dialog and bound the retries Three changes to how the incoming side behaves. No confirmation step. The status bar task already says what is happening and the copy is additive rather than destructive, so stopping to ask only added a click to something the user did not choose to start. Completion is a notification carrying the reload action, since copied extensions load on the next boot. A dialog now appears for one reason only, an error. The dialog could previously reappear on every boot. Declining wrote nothing and the done flag was only written on success, so saying no or losing the connection part way meant being asked again next time, indefinitely. Attempts are now counted the moment a transfer starts rather than when it finishes, so an attempt that dies hard still burns its slot: one attempt, one retry, then the automatic path stops for good and the error names the exact Help menu entry to use. That entry is built from the same format call the menu itself uses, so the two cannot drift apart. A failure before the transfer starts, meaning the handshake or scan, still costs nothing and stays silent. Being offline for two boots must not permanently cost someone their migration, and those retries are invisible anyway, bounded by the sunset date. An interrupted run also used to fail silently unless it had been started from the menu, so someone who saw a task appear on boot and then nothing had no way to know. Failures are now reported whichever path started them. Metrics were count only, which told us nothing about volume or cost. Adds file and byte counts recorded before the run, so the size of what is out there is known even from failures, plus duration, per file cost, failure counts, and the skip reasons. Safari is counted too: without it there is no way to tell "nobody there needed this" from "we never offered it to them". Sunset date is 2026-09-01. --- docs/API-Reference/command/Commands.md | 6 + .../MigrateAssist/constants.js | 18 +- .../MigrateAssist/main.js | 6 + .../MigrateAssist/migrator.js | 176 ++++++++++++------ src/nls/root/strings.js | 6 +- src/styles/Extn-MigrateAssist.less | 15 ++ 6 files changed, 164 insertions(+), 63 deletions(-) diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index 37a71de251..08e2feb9ff 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -854,6 +854,12 @@ Checks for updates ## HELP\_AUTO\_UPDATE Toggles auto update +**Kind**: global variable + + +## HELP\_MIGRATE\_DATA +Migrates browser data from the legacy web origin + **Kind**: global variable diff --git a/src/extensionsIntegrated/MigrateAssist/constants.js b/src/extensionsIntegrated/MigrateAssist/constants.js index 1db7f0e090..5b3073eef3 100644 --- a/src/extensionsIntegrated/MigrateAssist/constants.js +++ b/src/extensionsIntegrated/MigrateAssist/constants.js @@ -55,7 +55,7 @@ define(function (require, exports, module) { * The day the legacy origin stops serving. Month is 0 based, so 8 is September. * @type {number} */ - const SUNSET_DATE = Date.UTC(2026, 8, 10); + const SUNSET_DATE = Date.UTC(2026, 8, 1); /** * Android/ChromeOS Trusted Web Activity that wraps the legacy origin. Users launched from this @@ -72,6 +72,20 @@ define(function (require, exports, module) { */ const MIGRATION_DONE_KEY = "migrateAssist.v1.done"; + /** + * PhStore key holding how many times the transfer has actually been started. The automatic path + * gets MAX_AUTO_ATTEMPTS of them, so a single bad network moment does not cost the user their + * migration, while a persistently broken setup stops nagging and points at the Help menu. + * @type {string} + */ + const MIGRATION_ATTEMPTS_KEY = "migrateAssist.v1.attempts"; + + /** + * One attempt, and one retry. + * @type {number} + */ + const MAX_AUTO_ATTEMPTS = 2; + /** * Dev only override, so the whole cross origin flow can be exercised on one dev server. * http://localhost:8000 and http://127.0.0.1:8000 are different origins with separate IndexedDB @@ -203,6 +217,8 @@ define(function (require, exports, module) { exports.SUNSET_DATE = SUNSET_DATE; exports.TWA_STORE_URL = TWA_STORE_URL; exports.MIGRATION_DONE_KEY = MIGRATION_DONE_KEY; + exports.MIGRATION_ATTEMPTS_KEY = MIGRATION_ATTEMPTS_KEY; + exports.MAX_AUTO_ATTEMPTS = MAX_AUTO_ATTEMPTS; exports.getLegacyOrigin = getLegacyOrigin; exports.getMigrateAssistURL = getMigrateAssistURL; exports.getLegacyDomainName = getLegacyDomainName; diff --git a/src/extensionsIntegrated/MigrateAssist/main.js b/src/extensionsIntegrated/MigrateAssist/main.js index 76969e5363..49eb4cb1e8 100644 --- a/src/extensionsIntegrated/MigrateAssist/main.js +++ b/src/extensionsIntegrated/MigrateAssist/main.js @@ -39,6 +39,7 @@ define(function (require, exports, module) { Menus = require("command/Menus"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), + Metrics = require("utils/Metrics"), Constants = require("./constants"), SunsetDialog = require("./sunset-dialog"), Migrator = require("./migrator"); @@ -51,6 +52,11 @@ define(function (require, exports, module) { } function _initNewOrigin() { + if (!Constants.isMigrationSupportedBrowser()) { + // Deliberately excluded, but worth counting: it is the difference between "nobody on + // Safari needed this" and "we never offered it to them". + Metrics.countEvent(Metrics.EVENT_TYPE.PLATFORM, "migrateAssist", "unsupportedBrowser"); + } // The menu entry is only registered here, so it can never show up on the legacy origin or on // desktop. It is also skipped on Safari/iOS, where the migration is deliberately not // implemented: offering an action we do not honour would be worse than not offering it. diff --git a/src/extensionsIntegrated/MigrateAssist/migrator.js b/src/extensionsIntegrated/MigrateAssist/migrator.js index b7276146bd..565fc33fab 100644 --- a/src/extensionsIntegrated/MigrateAssist/migrator.js +++ b/src/extensionsIntegrated/MigrateAssist/migrator.js @@ -35,6 +35,7 @@ define(function (require, exports, module) { const Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), + NotificationUI = require("widgets/NotificationUI"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), Metrics = require("utils/Metrics"), @@ -50,7 +51,7 @@ define(function (require, exports, module) { IFRAME_ID = "migrate-assist-frame"; const RESULT_MIGRATED = "migrated", - RESULT_DECLINED = "declined", + RESULT_INTERRUPTED = "interrupted", RESULT_NOTHING = "nothing", RESULT_UNREACHABLE = "unreachable"; @@ -213,54 +214,55 @@ define(function (require, exports, module) { } /** - * Shown once, before anything is copied, so the user knows why their machine is busy. + * Everything after the single up front question is reported at the bottom of the window rather + * than in another modal. The user opted in and went back to work; interrupting them again to say + * it finished would undo the point of moving progress out of a dialog in the first place. */ - function _confirmStart(fileCount) { - return Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_INFO, - Strings.MIGRATE_PROGRESS_TITLE, - StringUtils.format(Strings.MIGRATE_START_MESSAGE, fileCount, Constants.getLegacyDomainName()), - [ - { - className: Dialogs.DIALOG_BTN_CLASS_NORMAL, - id: Dialogs.DIALOG_BTN_CANCEL, - text: Strings.CANCEL - }, - { - className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, - id: Dialogs.DIALOG_BTN_OK, - text: Strings.MIGRATE_START_CONFIRM - } - ] - ).getPromise(); + /** + * Errors are the one thing that earns an interruption here. Everything else rides on the status + * bar task: if that is already telling the user what is happening, a dialog repeating it is just + * another click for them. + */ + function _errorDialog(title, message) { + Dialogs.showModalDialog(DefaultDialogs.DIALOG_ID_ERROR, title, message); + } + + function _toast(title, message, style, $extra) { + const $content = $("
").append($("
").text(message)); + if ($extra) { + $content.append($extra); + } + return NotificationUI.createToastFromTemplate(title, $content, { + dismissOnClick: false, // there is a close button, and a stray click must not eat the action + toastStyle: style + }); + } + + /** + * The Help menu path, spelled exactly as the menu itself spells it. + */ + function _menuPath() { + return `${Strings.HELP_MENU} > ` + + StringUtils.format(Strings.CMD_MIGRATE_DATA, Constants.getLegacyDomainName()); } function _showCompletion(migratedFiles, failed) { - const message = failed.length - ? StringUtils.format(Strings.MIGRATE_DONE_MESSAGE, migratedFiles) + "

" - + StringUtils.format(Strings.MIGRATE_DONE_PARTIAL, failed.length) - : StringUtils.format(Strings.MIGRATE_DONE_MESSAGE, migratedFiles); - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_INFO, - Strings.MIGRATE_DONE_TITLE, - message, - [ - { - className: Dialogs.DIALOG_BTN_CLASS_NORMAL, - id: Dialogs.DIALOG_BTN_CANCEL, - text: Strings.MIGRATE_RELOAD_LATER - }, - { - className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, - id: Dialogs.DIALOG_BTN_OK, - text: Strings.MIGRATE_RELOAD_NOW - } - ] - ).done(function (buttonId) { - if (buttonId === Dialogs.DIALOG_BTN_OK) { - CommandManager.execute(Commands.APP_RELOAD); - } + const $actions = $("
").addClass("migrate-assist-toast-actions"); + const $reload = $("