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 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/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)} />