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 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..cee6ccc7 100644
--- a/src/app/pages/Router.tsx
+++ b/src/app/pages/Router.tsx
@@ -9,16 +9,13 @@ import {
} from 'react-router-dom';
import { ClientConfig } from '../hooks/useClientConfig';
-import { AuthLayout, Login, Register, ResetPassword } from './auth';
+import { authRoutes } from './auth/authRoutes';
import {
DIRECT_PATH,
DESK_PATH,
EXPLORE_PATH,
HOME_PATH,
- LOGIN_PATH,
INBOX_PATH,
- REGISTER_PATH,
- RESET_PASSWORD_PATH,
SPACE_PATH,
_CREATE_PATH,
_FEATURED_PATH,
@@ -57,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';
@@ -88,25 +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
new file mode 100644
index 00000000..e594c83c
--- /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}
+
+
+ );
+}
diff --git a/src/app/pages/auth/SignedUp.tsx b/src/app/pages/auth/SignedUp.tsx
new file mode 100644
index 00000000..303e0d72
--- /dev/null
+++ b/src/app/pages/auth/SignedUp.tsx
@@ -0,0 +1,54 @@
+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. 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 (
+
+
+
+
+
+
+ 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/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
new file mode 100644
index 00000000..c5ad101f
--- /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 { RegisterRoute } from './register/RegisterRoute';
+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/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/register/PasswordRegisterForm.tsx b/src/app/pages/auth/register/PasswordRegisterForm.tsx
index 99feed1a..cd01d021 100644
--- a/src/app/pages/auth/register/PasswordRegisterForm.tsx
+++ b/src/app/pages/auth/register/PasswordRegisterForm.tsx
@@ -30,7 +30,15 @@ import {
import { useUIACompleted, useUIAFlow, useUIAParams } from '../../../hooks/useUIAFlows';
import { AsyncState, AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
-import { RegisterError, RegisterResult, register, useRegisterComplete } from './registerUtil';
+import {
+ RegisterError,
+ RegisterResult,
+ register,
+ registerErrorTrouble,
+ useRegisterComplete,
+} from './registerUtil';
+import { registerTroubleMessage } from '../authMessages';
+import { useAuthServer } from '../../../hooks/useAuthServer';
import { FieldError } from '../FiledError';
import { TrainingNotice } from '../TrainingNotice';
import {
@@ -185,6 +193,7 @@ export function PasswordRegisterForm({
defaultEmail,
defaultRegisterToken,
}: PasswordRegisterFormProps) {
+ const server = useAuthServer();
const serverDiscovery = useAutoDiscoveryInfo();
const baseUrl = serverDiscovery['m.homeserver'].base_url;
const mx = useMemo(() => 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)} />