diff --git a/CLAUDE.md b/CLAUDE.md index 5cd2e44..861b007 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -446,6 +446,8 @@ tests/ translate-card.test.ts # popup translate input flow login-password.test.ts # popup password form (ENABLE_PASSWORD_AUTH=true) login-password-disabled.test.ts # popup password form hidden when flag is unset + login-analytics.test.ts # user_logged-in source, login_failed source + reason (E-03) + save-login-prompt.test.ts # "Login to save" prompt viewed once per word view, clicked (E-03) language-state.test.ts # round-1 rules: dark gate, own language, save row, plan limit, deep link language-catalog.test.ts # one RTL list, picker names, bundled Vazirmatn own-language.test.ts # translate request with / without a server profile; language store diff --git a/src/background.ts b/src/background.ts index 20a3a2f..6cbf214 100644 --- a/src/background.ts +++ b/src/background.ts @@ -106,8 +106,13 @@ chrome.runtime.onMessage.addListener(function (request, _sender, sendResponse) { const left = screen.width / 2 - width / 2; const top = screen.height / 2 - height / 2; + // The login source rides on the hash route's query (E-03). + const query = request.source + ? `?source=${encodeURIComponent(request.source)}` + : ""; + chrome.windows.create({ - url: chrome.runtime.getURL("popup.html") + "#/login", + url: chrome.runtime.getURL("popup.html") + "#/login" + query, type: "popup", width: width, height: height, diff --git a/src/common/types/messaging.ts b/src/common/types/messaging.ts index c80cd62..8ae1fa1 100644 --- a/src/common/types/messaging.ts +++ b/src/common/types/messaging.ts @@ -74,11 +74,21 @@ export class StoreUserTokenMessage implements BaseMessage { } } +/** + * Where a login started (E-03). The background puts it on the login window's + * URL (`popup.html#/login?source=…`) so LoginView can send it with + * `user_logged-in` and `login_failed`. A login opened from the toolbar popup + * itself has no source on the URL and counts as "popup". + */ +export type LoginSource = "save_prompt" | "practice" | "popup"; + export class OpenLoginWindowMessage implements BaseMessage { type: string; + source?: LoginSource; - constructor() { + constructor(source?: LoginSource) { this.type = MESSAGE_TYPE.OPEN_LOGIN_WINDOW; + if (source) this.source = source; } static is(message: any): message is OpenLoginWindowMessage { diff --git a/src/console-crane/modules/practice-config/index.vue b/src/console-crane/modules/practice-config/index.vue index 395e9b9..18b48ce 100644 --- a/src/console-crane/modules/practice-config/index.vue +++ b/src/console-crane/modules/practice-config/index.vue @@ -324,7 +324,7 @@ function upgrade() { /** Logged out — open the login window; the isLogin watch advances the page. */ function handleLoginRequest() { analytic.track("practice-now_login-clicked"); - sendMessage(new OpenLoginWindowMessage()); + sendMessage(new OpenLoginWindowMessage("practice")); } function start() { diff --git a/src/console-crane/modules/word-detail/index.vue b/src/console-crane/modules/word-detail/index.vue index e4e45a8..d3a849c 100644 --- a/src/console-crane/modules/word-detail/index.vue +++ b/src/console-crane/modules/word-detail/index.vue @@ -249,7 +249,7 @@