Skip to content

Commit df8086b

Browse files
authored
Merge pull request #169 from AstrBotDevs/codex/fix-auth-startup-white-screen
fix: prevent blank screen after desktop auth
2 parents 58468fc + a49015f commit df8086b

2 files changed

Lines changed: 99 additions & 21 deletions

File tree

scripts/prepare-resources/bridge-bootstrap-updater-contract.test.mjs

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,8 @@ const chatTransportContractPath = new URL(
1111

1212
const flushAsyncWork = () => new Promise((resolve) => setImmediate(resolve));
1313

14-
function runBootstrap(source, authResults) {
15-
const values = new Map();
16-
const invocations = [];
17-
const intervals = [];
18-
const localStorage = {
14+
function createStorage(values) {
15+
return {
1916
getItem(key) {
2017
return values.has(key) ? values.get(key) : null;
2118
},
@@ -29,12 +26,28 @@ function runBootstrap(source, authResults) {
2926
values.clear();
3027
},
3128
};
29+
}
30+
31+
function runBootstrap(source, authResults, sharedState = {}) {
32+
const localValues = sharedState.localValues || new Map();
33+
const sessionValues = sharedState.sessionValues || new Map();
34+
const navigation = sharedState.navigation || { reloads: 0 };
35+
sharedState.localValues = localValues;
36+
sharedState.sessionValues = sessionValues;
37+
sharedState.navigation = navigation;
38+
const invocations = [];
39+
const intervals = [];
40+
const localStorage = createStorage(localValues);
41+
const sessionStorage = createStorage(sessionValues);
3242
const location = {
3343
href: 'http://127.0.0.1:6185/#/auth/login',
3444
origin: 'http://127.0.0.1:6185',
3545
hash: '#/auth/login',
3646
assign() {},
3747
replace() {},
48+
reload() {
49+
navigation.reloads += 1;
50+
},
3851
toString() {
3952
return this.href;
4053
},
@@ -60,6 +73,7 @@ function runBootstrap(source, authResults) {
6073
},
6174
},
6275
localStorage,
76+
sessionStorage,
6377
location,
6478
open: () => null,
6579
setInterval(handler, delay) {
@@ -69,7 +83,17 @@ function runBootstrap(source, authResults) {
6983
};
7084
class MockElement {}
7185
class MockAnchor extends MockElement {}
72-
const document = { addEventListener() {} };
86+
const document = {
87+
addEventListener() {},
88+
getElementById(id) {
89+
if (id !== 'app') return null;
90+
return {
91+
hasChildNodes() {
92+
return sharedState.appMounted === true;
93+
},
94+
};
95+
},
96+
};
7397
const quietConsole = { warn() {}, error() {}, log() {} };
7498

7599
runInNewContext(
@@ -88,7 +112,14 @@ function runBootstrap(source, authResults) {
88112
},
89113
);
90114

91-
return { window, localStorage, invocations, intervals };
115+
return {
116+
window,
117+
localStorage,
118+
sessionStorage,
119+
invocations,
120+
intervals,
121+
navigation,
122+
};
92123
}
93124

94125
test('bridge bootstrap defines astrbotAppUpdater methods', async () => {
@@ -115,27 +146,47 @@ test('bridge bootstrap owns desktop passwordless authentication lifecycle', asyn
115146
);
116147
});
117148

118-
test('bridge bootstrap automatically authenticates and reacquires a removed token', async () => {
149+
test('bridge bootstrap reloads once after initial auth and reacquires a removed token', async () => {
119150
const source = await readFile(bootstrapPath, 'utf8');
120-
const runtime = runBootstrap(source, [
121-
{ ok: true, token: 'first-jwt', username: 'astrbot' },
122-
{ ok: true, token: 'second-jwt', username: 'astrbot' },
123-
]);
151+
const sharedState = {};
152+
const firstRuntime = runBootstrap(
153+
source,
154+
[{ ok: true, token: 'first-jwt', username: 'astrbot' }],
155+
sharedState,
156+
);
124157

125158
await flushAsyncWork();
126159
await flushAsyncWork();
127-
assert.equal(runtime.localStorage.getItem('token'), 'first-jwt');
128-
assert.equal(runtime.localStorage.getItem('user'), 'astrbot');
129-
assert.equal(runtime.window.location.hash, '/welcome');
130-
assert.equal(runtime.intervals.length, 1);
131-
assert.equal(runtime.intervals[0].delay, 6 * 60 * 60 * 1000);
160+
assert.equal(firstRuntime.localStorage.getItem('token'), 'first-jwt');
161+
assert.equal(firstRuntime.localStorage.getItem('user'), 'astrbot');
162+
assert.equal(firstRuntime.navigation.reloads, 1);
163+
assert.equal(firstRuntime.window.location.hash, '#/auth/login');
164+
assert.equal(firstRuntime.intervals.length, 1);
165+
assert.equal(firstRuntime.intervals[0].delay, 6 * 60 * 60 * 1000);
166+
167+
const reloadedRuntime = runBootstrap(
168+
source,
169+
[
170+
{ ok: true, token: 'second-jwt', username: 'astrbot' },
171+
{ ok: true, token: 'third-jwt', username: 'astrbot' },
172+
],
173+
sharedState,
174+
);
175+
await flushAsyncWork();
176+
await flushAsyncWork();
177+
assert.equal(reloadedRuntime.localStorage.getItem('token'), 'second-jwt');
178+
assert.equal(reloadedRuntime.navigation.reloads, 1);
179+
assert.equal(reloadedRuntime.window.location.hash, '#/auth/login');
132180

133-
runtime.localStorage.removeItem('token');
181+
sharedState.appMounted = true;
182+
reloadedRuntime.localStorage.removeItem('token');
134183
await flushAsyncWork();
135184
await flushAsyncWork();
136-
assert.equal(runtime.localStorage.getItem('token'), 'second-jwt');
185+
assert.equal(reloadedRuntime.localStorage.getItem('token'), 'third-jwt');
186+
assert.equal(reloadedRuntime.navigation.reloads, 1);
187+
assert.equal(reloadedRuntime.window.location.hash, '/welcome');
137188
assert.ok(
138-
runtime.invocations.filter(
189+
reloadedRuntime.invocations.filter(
139190
({ command }) => command === 'desktop_bridge_get_auth_token',
140191
).length >= 2,
141192
);

src-tauri/src/bridge_bootstrap.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@
152152
const TOKEN_STORAGE_KEY = 'token';
153153
const USER_STORAGE_KEY = 'user';
154154
const SHELL_LOCALE_STORAGE_KEY = 'astrbot-locale';
155+
const DESKTOP_AUTH_BOOTSTRAP_RELOAD_KEY =
156+
'astrbot:desktop-auth-bootstrap-reloaded:v1';
155157
const DESKTOP_AUTH_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
156158
// Values are injected from the shared desktop bridge transport contract.
157159
const CHAT_TRANSPORT = Object.freeze({
@@ -183,6 +185,20 @@
183185
locale: value,
184186
});
185187

188+
const shouldReloadAfterDesktopAuthBootstrap = () => {
189+
try {
190+
const storage = window.sessionStorage;
191+
if (!storage) return false;
192+
if (storage.getItem(DESKTOP_AUTH_BOOTSTRAP_RELOAD_KEY) === 'true') {
193+
return false;
194+
}
195+
storage.setItem(DESKTOP_AUTH_BOOTSTRAP_RELOAD_KEY, 'true');
196+
return true;
197+
} catch {
198+
return false;
199+
}
200+
};
201+
186202
let desktopAuthRefreshPromise = null;
187203
const refreshDesktopAuthSession = () => {
188204
if (desktopAuthRefreshPromise) {
@@ -213,7 +229,18 @@
213229
}
214230

215231
await syncAuthToken(token);
216-
if (/^#\/auth\/(?:login|setup)(?:[/?]|$)/.test(window.location.hash || '')) {
232+
// The dashboard waits for its initial router navigation before mounting Vue.
233+
// Changing the auth hash while that navigation is still in flight can leave
234+
// #app empty in WKWebView. Reload once after persisting the desktop session
235+
// so the dashboard starts with a stable token instead.
236+
if (shouldReloadAfterDesktopAuthBootstrap()) {
237+
window.location.reload();
238+
return result;
239+
}
240+
if (
241+
document.getElementById('app')?.hasChildNodes() &&
242+
/^#\/auth\/(?:login|setup)(?:[/?]|$)/.test(window.location.hash || '')
243+
) {
217244
window.location.hash = '/welcome';
218245
}
219246
return result;

0 commit comments

Comments
 (0)