From 408d47a53d1386e4430a00ecd9420ad155ba2419 Mon Sep 17 00:00:00 2001 From: Void Date: Tue, 10 Nov 2026 16:55:26 +0000 Subject: [PATCH 1/4] auth: Register entry beside Sign in, behind a config switch that is off config.registration {enabled, url}: off by default and in config.json. When on with a http(s) url, the sign-in page shows Register (a link to the account service's own page) with the consent sentence and its version, both read from trainingNotice.ts (new TRAINING_NOTICE_VERSION shared with the room notice). /signed-up/ is the return page: it says to follow the steps from the sign-up page and promises nothing about mail; with the switch off it redirects to sign in. Off keeps the existing Register link. Co-Authored-By: Claude Sonnet 5.5 --- config.json | 19 +++++--- src/app/hooks/useClientConfig.ts | 9 ++++ src/app/pages/Router.tsx | 4 +- src/app/pages/auth/RegisterEntry.tsx | 22 +++++++++ src/app/pages/auth/SignedUp.tsx | 24 ++++++++++ src/app/pages/auth/index.ts | 1 + src/app/pages/auth/login/Login.tsx | 15 +++++-- src/app/pages/auth/registrationEntry.test.ts | 47 ++++++++++++++++++++ src/app/pages/auth/registrationEntry.ts | 25 +++++++++++ src/app/pages/pathUtils.ts | 3 ++ src/app/pages/paths.ts | 2 + src/app/utils/trainingNotice.ts | 7 +++ 12 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 src/app/pages/auth/RegisterEntry.tsx create mode 100644 src/app/pages/auth/SignedUp.tsx create mode 100644 src/app/pages/auth/registrationEntry.test.ts create mode 100644 src/app/pages/auth/registrationEntry.ts diff --git a/config.json b/config.json index ed1bca6b..085b8758 100644 --- a/config.json +++ b/config.json @@ -5,12 +5,14 @@ "featuredCommunities": { "openAsDefault": false, "spaces": ["#gamecult:gamecult.org"], - "rooms": [ - "#tuwunel:grin.hu", - "#free-software-movement:matrix.org", - "#gentoo:matrix.org" - ], - "servers": ["activematrixrooms.com", "matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"] + "rooms": ["#tuwunel:grin.hu", "#free-software-movement:matrix.org", "#gentoo:matrix.org"], + "servers": [ + "activematrixrooms.com", + "matrixrooms.info", + "matrix.org", + "mozilla.org", + "unredacted.org" + ] }, "hashRouter": { @@ -24,5 +26,10 @@ "dings": { "enabled": false + }, + + "registration": { + "enabled": false, + "url": "" } } diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts index bcf1c475..ab3d67b7 100644 --- a/src/app/hooks/useClientConfig.ts +++ b/src/app/hooks/useClientConfig.ts @@ -7,6 +7,11 @@ export type HashRouterConfig = { basename?: string; }; +export type RegistrationConfig = { + enabled?: boolean; + url?: string; +}; + export type ClientConfig = { defaultHomeserver?: number; homeserverList?: string[]; @@ -29,6 +34,10 @@ export type ClientConfig = { // Rooms (ids or aliases) shown as the album view; a room whose create type is org.gamecult.file_zone needs no entry. fileZoneRooms?: string[]; + // The sign-up entry on the sign-in page. Off unless `enabled` is true; `url` is the account + // service's own registration page (a value, never a route this client invents). + registration?: RegistrationConfig; + // Dev preview only: the build answers a pretend homeserver in-page (src/preview-demo). previewDemo?: boolean; }; diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index c7444942..71ead024 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -9,7 +9,7 @@ import { } from 'react-router-dom'; import { ClientConfig } from '../hooks/useClientConfig'; -import { AuthLayout, Login, Register, ResetPassword } from './auth'; +import { AuthLayout, Login, Register, ResetPassword, SignedUp } from './auth'; import { DIRECT_PATH, DESK_PATH, @@ -18,6 +18,7 @@ import { LOGIN_PATH, INBOX_PATH, REGISTER_PATH, + SIGNED_UP_PATH, RESET_PASSWORD_PATH, SPACE_PATH, _CREATE_PATH, @@ -105,6 +106,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) > } /> } /> + } /> } /> diff --git a/src/app/pages/auth/RegisterEntry.tsx b/src/app/pages/auth/RegisterEntry.tsx new file mode 100644 index 00000000..d60e19a6 --- /dev/null +++ b/src/app/pages/auth/RegisterEntry.tsx @@ -0,0 +1,22 @@ +import { Box, Text, color } from 'folds'; +import React from 'react'; +import { useClientConfig } from '../../hooks/useClientConfig'; +import { registrationEntry } from './registrationEntry'; + +// Beside Sign in. Nothing renders unless the config switch is on; the consent sentence and its +// version come from trainingNotice.ts, the same ones a room shows. +export function RegisterEntry() { + const config = useClientConfig(); + const entry = registrationEntry(config); + if (!entry) return null; + return ( + + + Do not have an account? Register + + + {entry.notice} (notice version {entry.noticeVersion}) + + + ); +} diff --git a/src/app/pages/auth/SignedUp.tsx b/src/app/pages/auth/SignedUp.tsx new file mode 100644 index 00000000..70ceff06 --- /dev/null +++ b/src/app/pages/auth/SignedUp.tsx @@ -0,0 +1,24 @@ +import { Box, Text } from 'folds'; +import React from 'react'; +import { Link, Navigate } from 'react-router-dom'; +import { useClientConfig } from '../../hooks/useClientConfig'; +import { getLoginPath } from '../pathUtils'; +import { registrationEntry } from './registrationEntry'; + +// Where the account service sends a newcomer back. It promises nothing about mail or timing: +// the sign-up page said what happens next, and this page points back to it. +export function SignedUp() { + const config = useClientConfig(); + if (!registrationEntry(config)) return ; + return ( + + + You signed up + + Follow the steps from the sign-up page to finish. Then sign in here. + + Sign in + + + ); +} diff --git a/src/app/pages/auth/index.ts b/src/app/pages/auth/index.ts index c4bd0476..78910d5f 100644 --- a/src/app/pages/auth/index.ts +++ b/src/app/pages/auth/index.ts @@ -1,4 +1,5 @@ export * from './AuthLayout'; export * from './login'; export * from './register'; +export * from './SignedUp'; export * from './reset-password'; diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx index bc3136f4..ea08e4f0 100644 --- a/src/app/pages/auth/login/Login.tsx +++ b/src/app/pages/auth/login/Login.tsx @@ -9,6 +9,8 @@ import { PasswordLoginForm } from './PasswordLoginForm'; import { SSOLogin } from '../SSOLogin'; import { TokenLogin } from './TokenLogin'; import { OrDivider } from '../OrDivider'; +import { RegisterEntry } from '../RegisterEntry'; +import { registrationEntry } from '../registrationEntry'; import { getLoginPath, getRegisterPath, withSearchParam } from '../../pathUtils'; import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin'; import { LoginPathSearchParams } from '../../paths'; @@ -37,7 +39,8 @@ const useLoginSearchParams = (searchParams: URLSearchParams): LoginPathSearchPar export function Login() { const server = useAuthServer(); - const { hashRouter, previewDemo } = useClientConfig(); + const config = useClientConfig(); + const { hashRouter, previewDemo } = config; const { loginFlows } = useAuthFlows(); const [searchParams] = useSearchParams(); const loginSearchParams = useLoginSearchParams(searchParams); @@ -90,9 +93,13 @@ export function Login() { )} - - Do not have an account? Register - + {registrationEntry(config) ? ( + + ) : ( + + Do not have an account? Register + + )} ); } diff --git a/src/app/pages/auth/registrationEntry.test.ts b/src/app/pages/auth/registrationEntry.test.ts new file mode 100644 index 00000000..3af32faa --- /dev/null +++ b/src/app/pages/auth/registrationEntry.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { registrationEntry } from './registrationEntry'; +import { + PREVIEW_NOTICE_TEXT, + TRAINING_NOTICE_TEXT, + TRAINING_NOTICE_VERSION, +} from '../../utils/trainingNotice'; +import config from '../../../../config.json'; + +const url = 'https://accounts.example/register'; + +describe('registration entry', () => { + it('is hidden when the switch is absent or off', () => { + expect(registrationEntry({})).toBeUndefined(); + expect(registrationEntry({ registration: { url } })).toBeUndefined(); + expect(registrationEntry({ registration: { enabled: false, url } })).toBeUndefined(); + }); + + it('ships off in config.json', () => { + expect(registrationEntry(config)).toBeUndefined(); + expect(config.registration.enabled).toBe(false); + }); + + it('is hidden when the switch is on but the url is no web address', () => { + expect(registrationEntry({ registration: { enabled: true } })).toBeUndefined(); + expect(registrationEntry({ registration: { enabled: true, url: '' } })).toBeUndefined(); + expect( + registrationEntry({ registration: { enabled: true, url: '/register' } }) + ).toBeUndefined(); + expect( + registrationEntry({ registration: { enabled: true, url: 'data:text/html,x' } }) + ).toBeUndefined(); + }); + + it('reads the consent text and version from trainingNotice.ts', () => { + expect(registrationEntry({ registration: { enabled: true, url } })).toEqual({ + url, + notice: TRAINING_NOTICE_TEXT, + noticeVersion: TRAINING_NOTICE_VERSION, + }); + }); + + it('says the pretend sentence on the pretend server', () => { + const entry = registrationEntry({ previewDemo: true, registration: { enabled: true, url } }); + expect(entry?.notice).toBe(PREVIEW_NOTICE_TEXT); + }); +}); diff --git a/src/app/pages/auth/registrationEntry.ts b/src/app/pages/auth/registrationEntry.ts new file mode 100644 index 00000000..3049403e --- /dev/null +++ b/src/app/pages/auth/registrationEntry.ts @@ -0,0 +1,25 @@ +import type { ClientConfig } from '../../hooks/useClientConfig'; +import { TRAINING_NOTICE_VERSION, trainingNoticeText } from '../../utils/trainingNotice'; + +export type RegistrationEntry = { url: string; notice: string; noticeVersion: number }; + +/** + * The sign-up entry the sign-in page may show: the account service's registration page, when the + * switch is on and the configured url is a plain http(s) address. Anything else is no entry. The + * consent sentence and its version are trainingNotice.ts's, the ones a room shows. + */ +export const registrationEntry = (config: ClientConfig): RegistrationEntry | undefined => { + const { enabled, url } = config.registration ?? {}; + if (enabled !== true || typeof url !== 'string') return undefined; + try { + const { protocol } = new URL(url); + if (protocol !== 'https:' && protocol !== 'http:') return undefined; + } catch { + return undefined; + } + return { + url, + notice: trainingNoticeText(config.previewDemo), + noticeVersion: TRAINING_NOTICE_VERSION, + }; +}; diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts index 2738c4c9..d5d81e56 100644 --- a/src/app/pages/pathUtils.ts +++ b/src/app/pages/pathUtils.ts @@ -16,6 +16,7 @@ import { INBOX_NOTIFICATIONS_PATH, INBOX_PATH, REGISTER_PATH, + SIGNED_UP_PATH, RESET_PASSWORD_PATH, ROOT_PATH, SPACE_LOBBY_PATH, @@ -83,6 +84,8 @@ export const getRegisterPath = (server?: string): string => { return generatePath(REGISTER_PATH, params); }; +export const getSignedUpPath = (): string => SIGNED_UP_PATH; + export const getResetPasswordPath = (server?: string): string => { const params = server ? { server: encodeURIComponent(server) } : undefined; return generatePath(RESET_PASSWORD_PATH, params); diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts index 6894b477..eba4394d 100644 --- a/src/app/pages/paths.ts +++ b/src/app/pages/paths.ts @@ -17,6 +17,8 @@ export const REGISTER_PATH = '/register/:server?/'; export type ResetPasswordPathSearchParams = { email?: string; }; +export const SIGNED_UP_PATH = '/signed-up/'; + export const RESET_PASSWORD_PATH = '/reset-password/:server?/'; export const _CREATE_PATH = 'create/'; diff --git a/src/app/utils/trainingNotice.ts b/src/app/utils/trainingNotice.ts index c3d6e42a..7d7f6fa1 100644 --- a/src/app/utils/trainingNotice.ts +++ b/src/app/utils/trainingNotice.ts @@ -28,6 +28,13 @@ export const trainingNoticeText = (previewDemo?: boolean): string => * else is malformed. Absent or malformed in any way means no train line. */ export const MAX_NOTICE_VERSION = 4294967295; + +/** + * The version of the notice wording above. Registration shows it with the text, and a room's + * notice event carries the same number, so one text has one version on both surfaces. Bump it + * with the wording and nowhere else. + */ +export const TRAINING_NOTICE_VERSION = 1; export const ROOM_NOTICE_EVENT_TYPE = StateEvent.GameCultNotice; export type RoomNotice = { version: number; scope: 'show' | 'study' | 'train' }; -- 2.47.3 From 3851d0302b0eda14749fde3c6c76c45851efff5f Mon Sep 17 00:00:00 2001 From: Void Date: Tue, 10 Nov 2026 17:23:51 +0000 Subject: [PATCH 2/4] auth: sign-up return page renders; gamecult.org is the default homeserver /signed-up/ sat inside AuthLayout, which discovers a homeserver and sends paths it does not know to sign in, so it never rendered. The signed-out routes move to authRoutes.tsx with the return page beside the layout (its own card, no discovery); a test matches the real route table and renders the page. Drop the visible notice version from the entry (it stays in registrationEntry's data and tests) and the unused getSignedUpPath. clientDefaultServer falls back to gamecult.org, and the repo config.json lists it first and defaults to it; a config that names its own list (the production pin) is unaffected. Co-Authored-By: Claude Sonnet 5.5 --- config.json | 11 ++++-- src/app/hooks/useClientConfig.test.ts | 20 +++++++++++ src/app/hooks/useClientConfig.ts | 2 +- src/app/pages/Router.tsx | 29 ++------------- src/app/pages/auth/RegisterEntry.tsx | 2 +- src/app/pages/auth/SignedUp.tsx | 52 +++++++++++++++++++++------ src/app/pages/auth/authRoutes.tsx | 40 +++++++++++++++++++++ src/app/pages/pathUtils.ts | 3 -- src/app/pages/signedUpRoute.test.tsx | 51 ++++++++++++++++++++++++++ 9 files changed, 166 insertions(+), 44 deletions(-) create mode 100644 src/app/hooks/useClientConfig.test.ts create mode 100644 src/app/pages/auth/authRoutes.tsx create mode 100644 src/app/pages/signedUpRoute.test.tsx diff --git a/config.json b/config.json index 085b8758..06e8f8ac 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,13 @@ { - "defaultHomeserver": 1, - "homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"], + "defaultHomeserver": 0, + "homeserverList": [ + "gamecult.org", + "converser.eu", + "matrix.org", + "mozilla.org", + "unredacted.org", + "xmr.se" + ], "allowCustomHomeservers": true, "featuredCommunities": { "openAsDefault": false, diff --git a/src/app/hooks/useClientConfig.test.ts b/src/app/hooks/useClientConfig.test.ts new file mode 100644 index 00000000..a2004d2f --- /dev/null +++ b/src/app/hooks/useClientConfig.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { clientDefaultServer } from './useClientConfig'; +import config from '../../../config.json'; + +describe('default homeserver', () => { + it('is gamecult.org when the config names none', () => { + expect(clientDefaultServer({})).toBe('gamecult.org'); + expect(clientDefaultServer({ homeserverList: [] })).toBe('gamecult.org'); + }); + + it('is the listed server the config points at', () => { + expect( + clientDefaultServer({ homeserverList: ['chat.gamecult.org'], defaultHomeserver: 0 }) + ).toBe('chat.gamecult.org'); + }); + + it('is gamecult.org in the repo config.json too', () => { + expect(clientDefaultServer(config)).toBe('gamecult.org'); + }); +}); diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts index ab3d67b7..feb38e0a 100644 --- a/src/app/hooks/useClientConfig.ts +++ b/src/app/hooks/useClientConfig.ts @@ -53,7 +53,7 @@ export function useClientConfig(): ClientConfig { } export const clientDefaultServer = (clientConfig: ClientConfig): string => - clientConfig.homeserverList?.[clientConfig.defaultHomeserver ?? 0] ?? 'matrix.org'; + clientConfig.homeserverList?.[clientConfig.defaultHomeserver ?? 0] ?? 'gamecult.org'; export const clientAllowedServer = (clientConfig: ClientConfig, server: string): boolean => { const { homeserverList, allowCustomHomeservers } = clientConfig; diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 71ead024..cee6ccc7 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -9,17 +9,13 @@ import { } from 'react-router-dom'; import { ClientConfig } from '../hooks/useClientConfig'; -import { AuthLayout, Login, Register, ResetPassword, SignedUp } from './auth'; +import { authRoutes } from './auth/authRoutes'; import { DIRECT_PATH, DESK_PATH, EXPLORE_PATH, HOME_PATH, - LOGIN_PATH, INBOX_PATH, - REGISTER_PATH, - SIGNED_UP_PATH, - RESET_PASSWORD_PATH, SPACE_PATH, _CREATE_PATH, _FEATURED_PATH, @@ -58,7 +54,7 @@ import { ScreenSize } from '../hooks/useScreenSize'; import { MobileFriendlyPageNav } from './MobileFriendly'; import { ClientInitStorageAtom } from './client/ClientInitStorageAtom'; import { ClientNonUIFeatures } from './client/ClientNonUIFeatures'; -import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager'; +import { AuthRouteThemeManager } from './ThemeManager'; import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification'; import { AutoRestoreBackupOnVerification } from '../components/BackupRestore'; import { RoomSettingsRenderer } from '../features/room-settings'; @@ -89,26 +85,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) return redirect(getLoginPath()); }} /> - { - if (getFallbackSession()) { - return redirect(getHomePath()); - } - - return null; - }} - element={ - <> - - - - } - > - } /> - } /> - } /> - } /> - + {authRoutes()} { diff --git a/src/app/pages/auth/RegisterEntry.tsx b/src/app/pages/auth/RegisterEntry.tsx index d60e19a6..e594c83c 100644 --- a/src/app/pages/auth/RegisterEntry.tsx +++ b/src/app/pages/auth/RegisterEntry.tsx @@ -15,7 +15,7 @@ export function RegisterEntry() { Do not have an account? Register - {entry.notice} (notice version {entry.noticeVersion}) + {entry.notice} ); diff --git a/src/app/pages/auth/SignedUp.tsx b/src/app/pages/auth/SignedUp.tsx index 70ceff06..303e0d72 100644 --- a/src/app/pages/auth/SignedUp.tsx +++ b/src/app/pages/auth/SignedUp.tsx @@ -1,24 +1,54 @@ -import { Box, Text } from 'folds'; +import { Box, Header, Scroll, Text } from 'folds'; +import classNames from 'classnames'; import React from 'react'; import { Link, Navigate } from 'react-router-dom'; import { useClientConfig } from '../../hooks/useClientConfig'; import { getLoginPath } from '../pathUtils'; import { registrationEntry } from './registrationEntry'; +import { AuthFooter } from './AuthFooter'; +import * as css from './styles.css'; +import * as PatternsCss from '../../styles/Patterns.css'; +import RatatoskrSVG from '../../../../public/res/svg/ratatoskr.svg'; +import { LogoMark } from '../../components/logo/logoMark.css'; // Where the account service sends a newcomer back. It promises nothing about mail or timing: -// the sign-up page said what happens next, and this page points back to it. +// the sign-up page said what happens next, and this page points back to it. It has its own card +// because AuthLayout looks up a homeserver first and sends paths it does not know to sign in. export function SignedUp() { const config = useClientConfig(); if (!registrationEntry(config)) return ; return ( - - - You signed up - - Follow the steps from the sign-up page to finish. Then sign in here. - - Sign in - - + + + +
+ + Ratatoskr logo + Ratatoskr + +
+ + + You signed up + + Follow the steps from the sign-up page to finish. Then sign in here. + + Sign in + + +
+ +
+
); } diff --git a/src/app/pages/auth/authRoutes.tsx b/src/app/pages/auth/authRoutes.tsx new file mode 100644 index 00000000..c6fcd2c1 --- /dev/null +++ b/src/app/pages/auth/authRoutes.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Route, redirect } from 'react-router-dom'; +import { getFallbackSession } from '../../state/sessions'; +import { UnAuthRouteThemeManager } from '../ThemeManager'; +import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, SIGNED_UP_PATH } from '../paths'; +import { getHomePath } from '../pathUtils'; +import { AuthLayout } from './AuthLayout'; +import { Login } from './login'; +import { Register } from './register'; +import { ResetPassword } from './reset-password'; +import { SignedUp } from './SignedUp'; + +// The routes a signed-out visitor sees. The sign-up return page sits beside the layout, not in +// it: the layout looks up a homeserver and sends any path it does not know to sign in. +export const authRoutes = () => ( + <> + (getFallbackSession() ? redirect(getHomePath()) : null)} + element={ + <> + + + + } + > + } /> + } /> + } /> + + + + + + } + /> + +); diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts index d5d81e56..2738c4c9 100644 --- a/src/app/pages/pathUtils.ts +++ b/src/app/pages/pathUtils.ts @@ -16,7 +16,6 @@ import { INBOX_NOTIFICATIONS_PATH, INBOX_PATH, REGISTER_PATH, - SIGNED_UP_PATH, RESET_PASSWORD_PATH, ROOT_PATH, SPACE_LOBBY_PATH, @@ -84,8 +83,6 @@ export const getRegisterPath = (server?: string): string => { return generatePath(REGISTER_PATH, params); }; -export const getSignedUpPath = (): string => SIGNED_UP_PATH; - export const getResetPasswordPath = (server?: string): string => { const params = server ? { server: encodeURIComponent(server) } : undefined; return generatePath(RESET_PASSWORD_PATH, params); diff --git a/src/app/pages/signedUpRoute.test.tsx b/src/app/pages/signedUpRoute.test.tsx new file mode 100644 index 00000000..e1f69d31 --- /dev/null +++ b/src/app/pages/signedUpRoute.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { renderToString } from 'react-dom/server'; +import { Route, createRoutesFromElements } from 'react-router-dom'; +import { StaticRouter, createStaticHandler } from 'react-router-dom/server'; +import { ClientConfigProvider } from '../hooks/useClientConfig'; +import type { ClientConfig } from '../hooks/useClientConfig'; +import { SIGNED_UP_PATH } from './paths'; + +const config: ClientConfig = { registration: { enabled: true, url: 'https://accounts.example/' } }; + +// The routes read localStorage when imported (settings atoms); the node environment has none. +let authRoutes: typeof import('./auth/authRoutes').authRoutes; +let SignedUp: typeof import('./auth').SignedUp; +beforeAll(async () => { + vi.stubGlobal('localStorage', { + getItem: () => null, + setItem: () => undefined, + removeItem: () => undefined, + }); + ({ authRoutes } = await import('./auth/authRoutes')); + ({ SignedUp } = await import('./auth')); +}, 120_000); + +const matchPath = async (path: string) => { + const { query } = createStaticHandler(createRoutesFromElements({authRoutes()})); + const context = await query(new Request(`http://localhost${path}`)); + if (context instanceof Response) { + throw new Error(`redirected to ${context.headers.get('Location')}`); + } + return context.matches; +}; + +describe('the sign-up return page in the real route table', () => { + it('is its own route, outside AuthLayout (which would send it to sign in)', async () => { + const matches = await matchPath(SIGNED_UP_PATH); + // the pathless root, then the page itself: no layout route in between + expect(matches?.map((m) => m.route.path)).toEqual([undefined, SIGNED_UP_PATH]); + }); + + it('renders its words, not the sign-in form, when the switch is on', () => { + const html = renderToString( + + + + + + ); + expect(html).toContain('Follow the steps from the sign-up page'); + }); +}); -- 2.47.3 From 4148a52c4736927dd521251b555fdc1bd2c48903 Mon Sep 17 00:00:00 2001 From: Void Date: Tue, 10 Nov 2026 23:41:22 +0000 Subject: [PATCH 3/4] auth: Libby's read: fallback split out, register errors in plain words, /register goes to the account service when the switch is on - The gamecult.org default homeserver moves to its own PR (#86); config.json and clientDefaultServer are dev's again. - PasswordRegisterForm no longer shows a server's own error text or the old "Failed to register" lines: registerErrorTrouble maps the error code to registerTroubleMessage (new troubles for weak and short passwords and for the unknown case). A consumer-side test goes through the code to the words and checks no Matrix code leaks. - Per Heimdall: with the Register switch on, /register is RegisterRoute, which sends a stranger to the account service's page (one way to register); with it off the in-client form stays. Tested through the component, rendered on and off and with an unusable url. Co-Authored-By: Claude Sonnet 5.5 --- config.json | 30 +++++++-------------------- src/app/hooks/useClientConfig.test.ts | 20 ------------------ 2 files changed, 8 insertions(+), 42 deletions(-) delete mode 100644 src/app/hooks/useClientConfig.test.ts diff --git a/config.json b/config.json index 06e8f8ac..ed1bca6b 100644 --- a/config.json +++ b/config.json @@ -1,25 +1,16 @@ { - "defaultHomeserver": 0, - "homeserverList": [ - "gamecult.org", - "converser.eu", - "matrix.org", - "mozilla.org", - "unredacted.org", - "xmr.se" - ], + "defaultHomeserver": 1, + "homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"], "allowCustomHomeservers": true, "featuredCommunities": { "openAsDefault": false, "spaces": ["#gamecult:gamecult.org"], - "rooms": ["#tuwunel:grin.hu", "#free-software-movement:matrix.org", "#gentoo:matrix.org"], - "servers": [ - "activematrixrooms.com", - "matrixrooms.info", - "matrix.org", - "mozilla.org", - "unredacted.org" - ] + "rooms": [ + "#tuwunel:grin.hu", + "#free-software-movement:matrix.org", + "#gentoo:matrix.org" + ], + "servers": ["activematrixrooms.com", "matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"] }, "hashRouter": { @@ -33,10 +24,5 @@ "dings": { "enabled": false - }, - - "registration": { - "enabled": false, - "url": "" } } diff --git a/src/app/hooks/useClientConfig.test.ts b/src/app/hooks/useClientConfig.test.ts deleted file mode 100644 index a2004d2f..00000000 --- a/src/app/hooks/useClientConfig.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { clientDefaultServer } from './useClientConfig'; -import config from '../../../config.json'; - -describe('default homeserver', () => { - it('is gamecult.org when the config names none', () => { - expect(clientDefaultServer({})).toBe('gamecult.org'); - expect(clientDefaultServer({ homeserverList: [] })).toBe('gamecult.org'); - }); - - it('is the listed server the config points at', () => { - expect( - clientDefaultServer({ homeserverList: ['chat.gamecult.org'], defaultHomeserver: 0 }) - ).toBe('chat.gamecult.org'); - }); - - it('is gamecult.org in the repo config.json too', () => { - expect(clientDefaultServer(config)).toBe('gamecult.org'); - }); -}); -- 2.47.3 From a4942eef86a33170ddc48a3b5c22819817daa22b Mon Sep 17 00:00:00 2001 From: Void Date: Tue, 10 Nov 2026 23:41:29 +0000 Subject: [PATCH 4/4] auth: register failures in plain words; /register goes to the account service when the switch is on The previous commit carried only the fallback split. This one carries the rest of Libby's two blockers and Heimdall's decision: - config.json keeps only the registration block over dev's; clientDefaultServer is dev's again (the gamecult.org default is #86). - registerErrorTrouble maps a register error code to registerTroubleMessage (new troubles: password-weak, password-short, unknown); PasswordRegisterForm shows those, never the server's text. - RegisterRoute: switch on, /register offers the account service link and redirects there; switch off, the in-client form. - Tests: registerErrors.test.ts (code to words, no Matrix code leaks), RegisterRoute.test.tsx (on, off, unusable url). Co-Authored-By: Claude Sonnet 5.5 --- config.json | 5 +++ src/app/hooks/useClientConfig.ts | 2 +- src/app/pages/auth/authMessages.ts | 15 ++++++- src/app/pages/auth/authRoutes.tsx | 4 +- .../auth/register/PasswordRegisterForm.tsx | 41 ++++++++----------- .../auth/register/RegisterRoute.test.tsx | 37 +++++++++++++++++ src/app/pages/auth/register/RegisterRoute.tsx | 26 ++++++++++++ .../auth/register/registerErrors.test.ts | 39 ++++++++++++++++++ src/app/pages/auth/register/registerUtil.ts | 22 ++++++++++ 9 files changed, 162 insertions(+), 29 deletions(-) create mode 100644 src/app/pages/auth/register/RegisterRoute.test.tsx create mode 100644 src/app/pages/auth/register/RegisterRoute.tsx create mode 100644 src/app/pages/auth/register/registerErrors.test.ts diff --git a/config.json b/config.json index ed1bca6b..772b61c3 100644 --- a/config.json +++ b/config.json @@ -24,5 +24,10 @@ "dings": { "enabled": false + }, + + "registration": { + "enabled": false, + "url": "" } } diff --git a/src/app/hooks/useClientConfig.ts b/src/app/hooks/useClientConfig.ts index feb38e0a..ab3d67b7 100644 --- a/src/app/hooks/useClientConfig.ts +++ b/src/app/hooks/useClientConfig.ts @@ -53,7 +53,7 @@ export function useClientConfig(): ClientConfig { } export const clientDefaultServer = (clientConfig: ClientConfig): string => - clientConfig.homeserverList?.[clientConfig.defaultHomeserver ?? 0] ?? 'gamecult.org'; + clientConfig.homeserverList?.[clientConfig.defaultHomeserver ?? 0] ?? 'matrix.org'; export const clientAllowedServer = (clientConfig: ClientConfig, server: string): boolean => { const { homeserverList, allowCustomHomeservers } = clientConfig; diff --git a/src/app/pages/auth/authMessages.ts b/src/app/pages/auth/authMessages.ts index 3cc858fe..4d9575a5 100644 --- a/src/app/pages/auth/authMessages.ts +++ b/src/app/pages/auth/authMessages.ts @@ -57,7 +57,14 @@ export const serverTroubleMessage = ( export const noSignInMethodMessage = (server: string): string => `${server} does not offer a way to sign in that this app can use. Pick another server from the Homeserver list, or tell whoever runs ${server}.`; -export type RegisterTrouble = 'closed' | 'rate-limited' | 'invalid' | 'unsupported'; +export type RegisterTrouble = + | 'closed' + | 'rate-limited' + | 'invalid' + | 'unsupported' + | 'password-weak' + | 'password-short' + | 'unknown'; export const registerTroubleMessage = (trouble: RegisterTrouble, server: string): string => { switch (trouble) { @@ -67,6 +74,12 @@ export const registerTroubleMessage = (trouble: RegisterTrouble, server: string) return 'Too many tries in a short time. Wait a minute, then try again.'; case 'invalid': return `${server} did not accept the request to make an account. Reload the page and try again; if it keeps failing, tell whoever invited you.`; + case 'password-weak': + return 'That password is too easy to guess. Pick a longer one, with more than one kind of character.'; + case 'password-short': + return 'That password is too short. Pick a longer one.'; + case 'unknown': + return `Something went wrong making the account on ${server}. Try again in a minute; if it keeps failing, tell whoever invited you.`; case 'unsupported': default: return `${server} asks for something this app cannot do when making an account. Tell whoever invited you.`; diff --git a/src/app/pages/auth/authRoutes.tsx b/src/app/pages/auth/authRoutes.tsx index c6fcd2c1..c5ad101f 100644 --- a/src/app/pages/auth/authRoutes.tsx +++ b/src/app/pages/auth/authRoutes.tsx @@ -6,7 +6,7 @@ import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, SIGNED_UP_PATH } from ' import { getHomePath } from '../pathUtils'; import { AuthLayout } from './AuthLayout'; import { Login } from './login'; -import { Register } from './register'; +import { RegisterRoute } from './register/RegisterRoute'; import { ResetPassword } from './reset-password'; import { SignedUp } from './SignedUp'; @@ -24,7 +24,7 @@ export const authRoutes = () => ( } > } /> - } /> + } /> } /> createClient({ baseUrl }), [baseUrl]); @@ -207,6 +216,9 @@ export function PasswordRegisterForm({ const registerError = registerState.status === AsyncStatus.Error ? registerState.error : undefined; + // The password troubles show under the password field; the rest is the form's. + const registerTrouble = registerError && registerErrorTrouble(registerError.errcode); + const formTrouble = registerTrouble?.startsWith('password-') ? undefined : registerTrouble; useRegisterComplete(customRegisterResp); const handleSubmit: ChangeEventHandler = (evt) => { @@ -298,20 +310,10 @@ export function PasswordRegisterForm({ required /> {registerError?.errcode === RegisterError.PasswordWeak && ( - + )} {registerError?.errcode === RegisterError.PasswordShort && ( - + )} @@ -378,18 +380,7 @@ export function PasswordRegisterForm({ )} - {registerError?.errcode === RegisterError.RateLimited && ( - - )} - {registerError?.errcode === RegisterError.Forbidden && ( - - )} - {registerError?.errcode === RegisterError.InvalidRequest && ( - - )} - {registerError?.errcode === RegisterError.Unknown && ( - - )} + {formTrouble && } setTrainingAck((ack) => !ack)} />