diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx
index 49c7b774..a2d652a3 100644
--- a/src/app/pages/auth/AuthLayout.tsx
+++ b/src/app/pages/auth/AuthLayout.tsx
@@ -32,6 +32,7 @@ import { AuthFlowsLoader } from '../../components/AuthFlowsLoader';
import { AuthFlowsProvider } from '../../hooks/useAuthFlows';
import { AuthServerProvider } from '../../hooks/useAuthServer';
import { tryDecodeURIComponent } from '../../utils/dom';
+import { serverTroubleMessage } from './authMessages';
const currentAuthPath = (pathname: string): string => {
if (matchPath(LOGIN_PATH, pathname)) {
@@ -158,18 +159,18 @@ export function AuthLayout() {
/>
{discoveryState.status === AsyncStatus.Loading && (
-
+
)}
{discoveryState.status === AsyncStatus.Error && (
-
+
)}
{autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
)}
{autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
-
+
)}
{discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
@@ -182,7 +183,7 @@ export function AuthLayout() {
/>
)}
error={() => (
-
+
)}
>
{(specVersions) => (
@@ -192,7 +193,9 @@ export function AuthLayout() {
)}
error={() => (
-
+
)}
>
{(authFlows) => (
diff --git a/src/app/pages/auth/authMessages.test.ts b/src/app/pages/auth/authMessages.test.ts
new file mode 100644
index 00000000..04b43ffb
--- /dev/null
+++ b/src/app/pages/auth/authMessages.test.ts
@@ -0,0 +1,68 @@
+import { describe, expect, it } from 'vitest';
+import {
+ loginErrorMessage,
+ noSignInMethodMessage,
+ registerTroubleMessage,
+ serverTroubleMessage,
+ type RegisterTrouble,
+ type ServerTrouble,
+} from './authMessages';
+import { LoginError } from './login/loginUtil';
+
+const stock = [
+ 'Invalid Username or Password.',
+ 'Failed to find homeserver.',
+ 'Failed to connect',
+ 'Failed to login',
+];
+const troubles: ServerTrouble[] = [
+ 'not-found',
+ 'unusable-setup',
+ 'bad-address',
+ 'unreachable',
+ 'no-sign-in-info',
+];
+const registerTroubles: RegisterTrouble[] = ['closed', 'rate-limited', 'invalid', 'unsupported'];
+
+describe('sign-in and sign-up messages', () => {
+ it('has a message for every sign-in error, each different, none the stock wording', () => {
+ const messages = Object.values(LoginError).map((e) => loginErrorMessage(e));
+ expect(new Set(messages).size).toBe(messages.length);
+ messages.forEach((m) => stock.forEach((s) => expect(m).not.toContain(s)));
+ });
+
+ it('keeps wrong credentials apart from a missing server', () => {
+ expect(loginErrorMessage(LoginError.Forbidden)).toContain('username and password');
+ expect(loginErrorMessage(LoginError.InvalidServer)).toContain('@name:server');
+ });
+
+ it('names the server in every server message and says what to do', () => {
+ troubles.forEach((t) => {
+ const m = serverTroubleMessage(t, 'chat.example', 'bad.host');
+ expect(m).toContain('chat.example');
+ stock.forEach((s) => expect(m).not.toContain(s));
+ });
+ expect(serverTroubleMessage('unusable-setup', 'chat.example', 'bad.host')).toContain(
+ 'bad.host'
+ );
+ expect(serverTroubleMessage('unreachable', 'chat.example')).toContain('reload');
+ });
+
+ it('tells a person who cannot fix a broken server setup to tell its owner', () => {
+ expect(serverTroubleMessage('bad-address', 'chat.example')).toContain(
+ 'whoever runs the server'
+ );
+ expect(noSignInMethodMessage('chat.example')).toContain('chat.example');
+ });
+
+ it('says closed sign-up is closed and points to sign in or the inviter, without a Register link to nowhere', () => {
+ const m = registerTroubleMessage('closed', 'chat.example');
+ expect(m).toContain('chat.example');
+ expect(m).toContain('sign in');
+ expect(m).toContain('invited you');
+ expect(m).not.toContain('disabled');
+ registerTroubles.forEach((t) =>
+ expect(registerTroubleMessage(t, 'chat.example').length).toBeGreaterThan(20)
+ );
+ });
+});
diff --git a/src/app/pages/auth/authMessages.ts b/src/app/pages/auth/authMessages.ts
new file mode 100644
index 00000000..3cc858fe
--- /dev/null
+++ b/src/app/pages/auth/authMessages.ts
@@ -0,0 +1,74 @@
+import { LoginError } from './login/loginUtil';
+
+// What the sign-in and sign-up pages say when something goes wrong. Each message says what happened
+// in plain words and what the person can do next; none is a status code or a Matrix term.
+
+export const loginErrorMessage = (error: LoginError): string => {
+ switch (error) {
+ case LoginError.ServerNotAllowed:
+ return 'This app only signs in to the servers in its Homeserver list. Pick one from the list above.';
+ case LoginError.InvalidServer:
+ return 'We could not find the server in that username. A full username looks like @name:server; the part after the colon is the server.';
+ case LoginError.Forbidden:
+ return 'That username and password did not match. Check both and try again, or use "Forget Password?" beside the password.';
+ case LoginError.UserDeactivated:
+ return 'This account has been turned off. Ask whoever runs this server to turn it back on.';
+ case LoginError.InvalidRequest:
+ return 'The server could not read that sign-in. Check the username and password for stray spaces and try again.';
+ case LoginError.RateLimited:
+ return 'Too many tries in a short time. Wait a minute, then try again.';
+ case LoginError.Unknown:
+ default:
+ return 'Sign-in did not work and the server gave no reason. Wait a minute and try again; if it keeps failing, tell whoever invited you.';
+ }
+};
+
+export type ServerTrouble =
+ | 'not-found'
+ | 'unusable-setup'
+ | 'bad-address'
+ | 'unreachable'
+ | 'no-sign-in-info';
+
+/** `server` is the name shown in the Homeserver box; `host` is the name the server's own setup gave. */
+export const serverTroubleMessage = (
+ trouble: ServerTrouble,
+ server: string,
+ host?: string
+): string => {
+ switch (trouble) {
+ case 'not-found':
+ return `We could not find a chat server called ${server}. Check the name for typos, or try again in a minute.`;
+ case 'unusable-setup':
+ return `We found ${server}, but the setup it points to (${
+ host ?? 'unknown'
+ }) is not usable. You cannot fix this from here: tell whoever runs the server.`;
+ case 'bad-address':
+ return `We found ${server}, but the address it gives for chat is not valid. You cannot fix this from here: tell whoever runs the server.`;
+ case 'unreachable':
+ return `We found ${server} but could not connect to it. It may be down for a moment: wait a minute and reload this page. If it stays down, tell whoever invited you.`;
+ case 'no-sign-in-info':
+ return `Connected to ${server}, but it did not say how to sign in. Reload the page; if it happens again, tell whoever runs the server.`;
+ default:
+ return `We could not reach ${server}. Reload the page and try again.`;
+ }
+};
+
+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 const registerTroubleMessage = (trouble: RegisterTrouble, server: string): string => {
+ switch (trouble) {
+ case 'closed':
+ return `New accounts cannot be made on ${server} from this page yet. If you were given an account, sign in with it. If you were not, ask whoever invited you how to get one.`;
+ case 'rate-limited':
+ 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 '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/login/Login.tsx b/src/app/pages/auth/login/Login.tsx
index 9d890bef..bc3136f4 100644
--- a/src/app/pages/auth/login/Login.tsx
+++ b/src/app/pages/auth/login/Login.tsx
@@ -13,6 +13,7 @@ import { getLoginPath, getRegisterPath, withSearchParam } from '../../pathUtils'
import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin';
import { LoginPathSearchParams } from '../../paths';
import { useClientConfig } from '../../../hooks/useClientConfig';
+import { noSignInMethodMessage } from '../authMessages';
const getLoginTokenSearchParam = () => {
// when using hasRouter query params in existing route
@@ -85,9 +86,7 @@ export function Login() {
)}
{!parsedFlows.password && !parsedFlows.sso && (
<>
-
- {`This client does not support login on "${server}" homeserver. Password and SSO based login method not found.`}
-
+ {noSignInMethodMessage(server)}
>
)}
diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx
index 3c8641d9..7934bc9f 100644
--- a/src/app/pages/auth/login/PasswordLoginForm.tsx
+++ b/src/app/pages/auth/login/PasswordLoginForm.tsx
@@ -35,6 +35,7 @@ import {
} from './loginUtil';
import { PasswordInput } from '../../../components/password-input';
import { FieldError } from '../FiledError';
+import { loginErrorMessage } from '../authMessages';
import { getResetPasswordPath } from '../../pathUtils';
import { stopPropagation } from '../../../utils/keyboard';
@@ -215,10 +216,10 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
{loginState.status === AsyncStatus.Error && (
<>
{loginState.error.errcode === LoginError.ServerNotAllowed && (
-
+
)}
{loginState.error.errcode === LoginError.InvalidServer && (
-
+
)}
>
)}
@@ -232,19 +233,19 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
{loginState.status === AsyncStatus.Error && (
<>
{loginState.error.errcode === LoginError.Forbidden && (
-
+
)}
{loginState.error.errcode === LoginError.UserDeactivated && (
-
+
)}
{loginState.error.errcode === LoginError.InvalidRequest && (
-
+
)}
{loginState.error.errcode === LoginError.RateLimited && (
-
+
)}
{loginState.error.errcode === LoginError.Unknown && (
-
+
)}
>
)}
diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx
index 7176489b..286fa5d3 100644
--- a/src/app/pages/auth/register/Register.tsx
+++ b/src/app/pages/auth/register/Register.tsx
@@ -12,6 +12,7 @@ import { SupportedUIAFlowsLoader } from '../../../components/SupportedUIAFlowsLo
import { getLoginPath } from '../../pathUtils';
import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin';
import { RegisterPathSearchParams } from '../../paths';
+import { registerTroubleMessage } from '../authMessages';
const useRegisterSearchParams = (searchParams: URLSearchParams): RegisterPathSearchParams =>
useMemo(
@@ -40,17 +41,17 @@ export function Register() {
{registerFlows.status === RegisterFlowStatus.RegistrationDisabled && !sso && (
- Registration has been disabled on this homeserver.
+ {registerTroubleMessage('closed', server)}
)}
{registerFlows.status === RegisterFlowStatus.RateLimited && !sso && (
- You have been rate-limited! Please try after some time.
+ {registerTroubleMessage('rate-limited', server)}
)}
{registerFlows.status === RegisterFlowStatus.InvalidRequest && !sso && (
- Invalid Request! Failed to get any registration options.
+ {registerTroubleMessage('invalid', server)}
)}
{registerFlows.status === RegisterFlowStatus.FlowRequired && (
@@ -62,7 +63,7 @@ export function Register() {
{(supportedFlows) =>
supportedFlows.length === 0 ? (
- This application does not support registration on this homeserver.
+ {registerTroubleMessage('unsupported', server)}
) : (