diff --git a/src/app/state/sessions.ts b/src/app/state/sessions.ts
index a9c34ce9..19f2212d 100644
--- a/src/app/state/sessions.ts
+++ b/src/app/state/sessions.ts
@@ -40,17 +40,17 @@ export function setFallbackSession(
localStorage.setItem('cinny_user_id', userId);
localStorage.setItem('cinny_hs_base_url', baseUrl);
}
-export const removeFallbackSession = () => {
- localStorage.removeItem('cinny_hs_base_url');
- localStorage.removeItem('cinny_user_id');
- localStorage.removeItem('cinny_device_id');
- localStorage.removeItem('cinny_access_token');
+export const removeFallbackSession = (storage: Storage = localStorage) => {
+ storage.removeItem('cinny_hs_base_url');
+ storage.removeItem('cinny_user_id');
+ storage.removeItem('cinny_device_id');
+ storage.removeItem('cinny_access_token');
};
-export const getFallbackSession = (): Session | undefined => {
- const baseUrl = localStorage.getItem('cinny_hs_base_url');
- const userId = localStorage.getItem('cinny_user_id');
- const deviceId = localStorage.getItem('cinny_device_id');
- const accessToken = localStorage.getItem('cinny_access_token');
+export const getFallbackSession = (storage: Storage = localStorage): Session | undefined => {
+ const baseUrl = storage.getItem('cinny_hs_base_url');
+ const userId = storage.getItem('cinny_user_id');
+ const deviceId = storage.getItem('cinny_device_id');
+ const accessToken = storage.getItem('cinny_access_token');
if (baseUrl && userId && deviceId && accessToken) {
const session: Session = {
diff --git a/src/index.tsx b/src/index.tsx
index 6c7a3bc2..51e9760f 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -30,29 +30,32 @@ import { getFallbackSession } from './app/state/sessions';
document.body.classList.add(configClass, varsClass);
-// Register Service Worker
-if ('serviceWorker' in navigator) {
- const swUrl =
- import.meta.env.MODE === 'production'
- ? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
- : `/dev-sw.js?dev-sw`;
+// Register Service Worker. After the preview demo has settled the stored session: the worker is
+// handed that session, and the demo may drop one it cannot serve.
+const registerServiceWorker = () => {
+ if ('serviceWorker' in navigator) {
+ const swUrl =
+ import.meta.env.MODE === 'production'
+ ? `${trimTrailingSlash(import.meta.env.BASE_URL)}/sw.js`
+ : `/dev-sw.js?dev-sw`;
- const sendSessionToSW = () => {
- const session = getFallbackSession();
- pushSessionToSW(session?.baseUrl, session?.accessToken);
- };
+ const sendSessionToSW = () => {
+ const session = getFallbackSession();
+ pushSessionToSW(session?.baseUrl, session?.accessToken);
+ };
- navigator.serviceWorker.register(swUrl).then(sendSessionToSW);
- navigator.serviceWorker.ready.then(sendSessionToSW);
+ navigator.serviceWorker.register(swUrl).then(sendSessionToSW);
+ navigator.serviceWorker.ready.then(sendSessionToSW);
- navigator.serviceWorker.addEventListener('message', (ev) => {
- const { type } = ev.data ?? {};
+ navigator.serviceWorker.addEventListener('message', (ev) => {
+ const { type } = ev.data ?? {};
- if (type === 'requestSession') {
- sendSessionToSW();
- }
- });
-}
+ if (type === 'requestSession') {
+ sendSessionToSW();
+ }
+ });
+ }
+};
// The pretend homeserver of the dev preview: built in only with VITE_PREVIEW_DEMO=1 (the dev
// workflow) and switched on by `previewDemo: true` in config.json. Any other build drops it.
@@ -76,4 +79,7 @@ const mountApp = () => {
root.render();
};
-installPreviewDemo().finally(mountApp);
+installPreviewDemo().finally(() => {
+ registerServiceWorker();
+ mountApp();
+});
diff --git a/src/preview-demo/install.ts b/src/preview-demo/install.ts
index a7b66de9..10b9a3ee 100644
--- a/src/preview-demo/install.ts
+++ b/src/preview-demo/install.ts
@@ -4,6 +4,7 @@
// only by a build with VITE_PREVIEW_DEMO=1 whose config.json says previewDemo: true (src/index.tsx).
// Own-origin requests (the app's static files) pass; the demo origin is answered in-page; any
// other origin is refused, so no real homeserver can be contacted while this is on.
+import { removeFallbackSession } from '../app/state/sessions';
import { DEMO_ORIGIN } from './fixtures';
import { createDemoServer, DemoServer } from './server';
@@ -27,8 +28,6 @@ const refusal = (what: string, url: string) =>
new Error(`preview: pretend server only; ${what} to ${url} refused`);
const BANNER_TEXT = 'preview: pretend server. Any password works.';
-const FOREIGN_SESSION_TEXT =
- 'preview: pretend server. A login for another server is stored here and cannot be used.';
// In flow, above the app: body becomes a column and the app root takes the rest, so the banner
// never covers the room header or the connection strip.
@@ -78,7 +77,16 @@ export const guardedFetch =
});
};
+// Every preview of this site shares one origin, so one localStorage: a login another preview left
+// (a real homeserver) would make the app connect to that server, which the guard refuses. The
+// demo has one server, so a stored login for any other is dropped before the app reads it.
+export const dropForeignSession = (storage: Storage) => {
+ const base = storage.getItem('cinny_hs_base_url');
+ if (base !== null && base !== DEMO_ORIGIN) removeFallbackSession(storage);
+};
+
export const installDemo = (win: Window & typeof globalThis = window) => {
+ dropForeignSession(win.localStorage);
const server = createDemoServer();
const { origin } = win.location;
const realFetch = win.fetch.bind(win);
@@ -111,7 +119,6 @@ export const installDemo = (win: Window & typeof globalThis = window) => {
routeOf(String(url), origin) === 'own' ? realBeacon(url, data) : false;
}
- const stored = win.localStorage.getItem('cinny_hs_base_url');
- showBanner(stored !== null && stored !== DEMO_ORIGIN ? FOREIGN_SESSION_TEXT : BANNER_TEXT);
+ showBanner(BANNER_TEXT);
return server;
};
diff --git a/src/preview-demo/server.test.ts b/src/preview-demo/server.test.ts
index d277b97b..ec9031c1 100644
--- a/src/preview-demo/server.test.ts
+++ b/src/preview-demo/server.test.ts
@@ -2,9 +2,10 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ClientEvent, MatrixClient, SyncState, createClient } from 'matrix-js-sdk';
import { DEMO_ORIGIN, ME, ROOMS, GENERAL, COMMONS } from './fixtures';
-import { guardedFetch, installDemo, routeOf } from './install';
+import { dropForeignSession, guardedFetch, installDemo, routeOf } from './install';
import { createDemoServer, DemoServer } from './server';
import { seedDemoSettings } from './seed';
+import { getFallbackSession } from '../app/state/sessions';
import { mindOfSender, POSE_KEY, resolvePose } from '../app/components/message/pose/poses';
import { parseRoomNotice, roomTrainingLine } from '../app/utils/trainingNotice';
@@ -233,6 +234,7 @@ describe('the guard', () => {
localStorage: {
getItem: (k: string) => store.get(k) ?? null,
setItem: (k: string, v: string) => void store.set(k, v),
+ removeItem: (k: string) => void store.delete(k),
},
} as unknown as Window & typeof globalThis;
vi.stubGlobal('document', {
@@ -254,6 +256,59 @@ describe('the guard', () => {
vi.unstubAllGlobals();
});
+ it('drops a stored login for a real homeserver before the app reads it, keeps the demo one', () => {
+ const SESSION = {
+ cinny_access_token: 'tok',
+ cinny_device_id: 'DEV',
+ cinny_user_id: '@a:b',
+ cinny_hs_base_url: 'https://chat.gamecult.org',
+ };
+ const store = new Map(Object.entries({ ...SESSION, settings: '{}' }));
+ const storage = {
+ getItem: (k: string) => store.get(k) ?? null,
+ setItem: (k: string, v: string) => void store.set(k, v),
+ removeItem: (k: string) => void store.delete(k),
+ } as Storage;
+ dropForeignSession(storage);
+ expect(getFallbackSession(storage)).toBeUndefined();
+ expect([...store.keys()]).toEqual(['settings']);
+
+ Object.entries({ ...SESSION, cinny_hs_base_url: DEMO_ORIGIN }).forEach(([k, v]) =>
+ store.set(k, v)
+ );
+ dropForeignSession(storage);
+ expect(getFallbackSession(storage)?.baseUrl).toBe(DEMO_ORIGIN);
+ });
+
+ it('installDemo clears a foreign login so the app starts at the login screen', () => {
+ const store = new Map([
+ ['cinny_hs_base_url', 'https://chat.gamecult.org'],
+ ['cinny_access_token', 'tok'],
+ ['cinny_device_id', 'DEV'],
+ ['cinny_user_id', '@a:b'],
+ ]);
+ const win = {
+ location: { origin: PAGE },
+ fetch: never,
+ WebSocket: class {},
+ XMLHttpRequest: { prototype: { open: vi.fn() } },
+ navigator: {},
+ localStorage: {
+ getItem: (k: string) => store.get(k) ?? null,
+ removeItem: (k: string) => void store.delete(k),
+ },
+ } as unknown as Window & typeof globalThis;
+ vi.stubGlobal('document', {
+ createElement: () => ({ style: {}, setAttribute: vi.fn() }),
+ getElementById: () => null,
+ body: { style: {}, insertBefore: vi.fn() },
+ });
+ const server = installDemo(win);
+ live.push({ server, mx: clientFor(server) });
+ expect(store.size).toBe(0);
+ vi.unstubAllGlobals();
+ });
+
it('seeds pose portraits on for a first visit and keeps what a visitor already chose', () => {
const fresh = new Map();
seedDemoSettings({