Skip to content

Commit c3ae0a5

Browse files
aaspinwallclaude
andauthored
fix(send): stop Send dashboard self-closing when opened from accounts dashboard (#945)
ProfileView auto-closed itself whenever it loaded inside Thunderbird without ?showDashboard=true. The accounts.tb.pro Send link does not append that flag, so clicking it briefly showed the dashboard and then ran window.close() + a fallback redirect, appearing to "fail to load". Invert the condition: a genuine web-app tab inside Thunderbird now renders the dashboard, and only the post-login extension popup (which always carries ?isExtension=true and relies on the auto-close) falls through to closing. Also implement the 5s timeout that queryAddonLoginState already documented but never had, so its promise can no longer hang forever and block the /send/profile router guard when the token-bridge does not respond. Adds ProfileView tests covering the regression and the popup auto-close path. Closes #944 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8055047 commit c3ae0a5

3 files changed

Lines changed: 129 additions & 2 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { mount } from '@vue/test-utils';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
import ProfileView from './ProfileView.vue';
4+
5+
const { isThunderbirdHost, environmentType, push } = vi.hoisted(() => {
6+
return { isThunderbirdHost: vi.fn(), environmentType: vi.fn(), push: vi.fn() };
7+
});
8+
9+
// Run debounced callbacks synchronously so we can assert without timers.
10+
vi.mock('@vueuse/core', () => ({
11+
useDebounceFn: (fn: (...args: unknown[]) => unknown) => fn,
12+
}));
13+
14+
vi.mock('@send-frontend/stores', () => ({
15+
useConfigStore: () => ({
16+
get isThunderbirdHost() {
17+
return isThunderbirdHost();
18+
},
19+
}),
20+
}));
21+
22+
vi.mock('@send-frontend/composables/useIsExtension', () => ({
23+
useIsExtension: () => ({
24+
environmentType: { value: environmentType() },
25+
}),
26+
}));
27+
28+
vi.mock('vue-router', () => ({
29+
useRouter: () => ({ push }),
30+
}));
31+
32+
const stubs = {
33+
UserDashboard: true,
34+
LoadingComponent: true,
35+
};
36+
37+
function setSearch(search: string) {
38+
Object.defineProperty(window, 'location', {
39+
value: { search },
40+
writable: true,
41+
});
42+
}
43+
44+
describe('ProfileView.vue', () => {
45+
beforeEach(() => {
46+
window.close = vi.fn();
47+
setSearch('');
48+
isThunderbirdHost.mockReturnValue(false);
49+
environmentType.mockReturnValue('WEB APP OUTSIDE THUNDERBIRD');
50+
});
51+
52+
afterEach(() => {
53+
vi.clearAllMocks();
54+
});
55+
56+
it('shows the dashboard for the web app outside Thunderbird', () => {
57+
environmentType.mockReturnValue('WEB APP OUTSIDE THUNDERBIRD');
58+
isThunderbirdHost.mockReturnValue(false);
59+
60+
const wrapper = mount(ProfileView, { global: { stubs } });
61+
62+
expect(wrapper.findComponent({ name: 'UserDashboard' }).exists()).toBe(true);
63+
expect(window.close).not.toHaveBeenCalled();
64+
});
65+
66+
it('shows the dashboard when ?showDashboard=true is present inside Thunderbird', () => {
67+
environmentType.mockReturnValue('WEB APP INSIDE THUNDERBIRD');
68+
isThunderbirdHost.mockReturnValue(true);
69+
setSearch('?showDashboard=true');
70+
71+
const wrapper = mount(ProfileView, { global: { stubs } });
72+
73+
expect(wrapper.findComponent({ name: 'UserDashboard' }).exists()).toBe(true);
74+
expect(window.close).not.toHaveBeenCalled();
75+
});
76+
77+
it('shows the dashboard for a genuine web-app tab inside Thunderbird (e.g. opened from accounts dashboard)', () => {
78+
// Regression test for bugzilla #2051092: the accounts.tb.pro Send link
79+
// navigates here without ?showDashboard=true and the page used to
80+
// self-close instead of rendering the dashboard.
81+
environmentType.mockReturnValue('WEB APP INSIDE THUNDERBIRD');
82+
isThunderbirdHost.mockReturnValue(true);
83+
setSearch('');
84+
85+
const wrapper = mount(ProfileView, { global: { stubs } });
86+
87+
expect(wrapper.findComponent({ name: 'UserDashboard' }).exists()).toBe(true);
88+
expect(window.close).not.toHaveBeenCalled();
89+
});
90+
91+
it('auto-closes the post-login extension popup (isExtension=true)', () => {
92+
environmentType.mockReturnValue('WEB APP INSIDE THUNDERBIRD');
93+
isThunderbirdHost.mockReturnValue(true);
94+
setSearch('?isExtension=true');
95+
96+
const wrapper = mount(ProfileView, { global: { stubs } });
97+
98+
expect(wrapper.findComponent({ name: 'UserDashboard' }).exists()).toBe(
99+
false
100+
);
101+
expect(window.close).toHaveBeenCalled();
102+
expect(push).toHaveBeenCalledWith('/close');
103+
});
104+
});

packages/send/frontend/src/apps/send/components/ProfileView.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,24 @@ const { isThunderbirdHost } = useConfigStore();
1111
const { environmentType } = useIsExtension();
1212
const router = useRouter();
1313
14+
// The post-login extension popup arrives at /send/profile?isExtension=true and
15+
// relies on the auto-close below to dismiss itself. A genuine user tab (e.g.
16+
// opened from the accounts.tb.pro dashboard) does not carry this flag.
17+
const isExtensionLoginPopup = computed(
18+
() =>
19+
new URLSearchParams(window.location.search).get('isExtension') === 'true'
20+
);
21+
1422
const shouldShowDashboard = computed(() => {
1523
if (environmentType.value === 'WEB APP OUTSIDE THUNDERBIRD') return true;
1624
1725
const urlParams = new URLSearchParams(window.location.search);
18-
return urlParams.get('showDashboard') === 'true';
26+
if (urlParams.get('showDashboard') === 'true') return true;
27+
28+
// A real web-app tab inside Thunderbird should render the dashboard rather
29+
// than self-close. Only the post-login extension popup (isExtension=true)
30+
// should fall through to the auto-close path.
31+
return !isExtensionLoginPopup.value;
1932
});
2033
2134
const close = useDebounceFn(() => {

packages/send/frontend/src/composables/useSendConfig.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,16 @@ export function useSendConfig() {
157157
isLoggedIn: boolean;
158158
username: string | null;
159159
}> => {
160-
return new Promise((resolve) => {
160+
return new Promise((resolve, reject) => {
161+
// Guards against the token-bridge content script being absent or
162+
// unresponsive (e.g. the page is open outside of Thunderbird, or the
163+
// add-on isn't installed). Without this the promise — and any router
164+
// guard awaiting it — would hang forever.
165+
const timeout = setTimeout(() => {
166+
cleanup();
167+
reject(new Error('Timed out waiting for addon login state'));
168+
}, 5000);
169+
161170
const messageHandler = (event: MessageEvent) => {
162171
if (event.data?.type === LOGIN_STATE_RESPONSE) {
163172
cleanup();
@@ -169,6 +178,7 @@ export function useSendConfig() {
169178
};
170179

171180
const cleanup = () => {
181+
clearTimeout(timeout);
172182
window.removeEventListener('message', messageHandler);
173183
};
174184

0 commit comments

Comments
 (0)