Merge pull request #357 from robintown/ts-auth

TypeScriptify the auth directory
This commit is contained in:
Robin 2022-05-31 10:35:39 -04:00 committed by GitHub
commit 9444f43c72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 284 additions and 252 deletions

View file

@ -16,9 +16,6 @@ module.exports = {
"sourceType": "module",
},
rules: {
// We break this rule in a few places: dial it back to a warning
// (and run with max warnings) to tolerate the existing code
"react-hooks/exhaustive-deps": ["warn"],
"jsx-a11y/media-has-caption": ["off"],
},
overrides: [

View file

@ -8,7 +8,7 @@
"build-storybook": "build-storybook",
"prettier:check": "prettier -c src",
"prettier:format": "prettier -w src",
"lint:js": "eslint --max-warnings 2 src",
"lint:js": "eslint --max-warnings 0 src",
"lint:types": "tsc"
},
"dependencies": {
@ -31,6 +31,7 @@
"@react-stately/tree": "^3.2.0",
"@sentry/react": "^6.13.3",
"@sentry/tracing": "^6.13.3",
"@types/grecaptcha": "^3.0.4",
"@use-gesture/react": "^10.2.11",
"classnames": "^2.3.1",
"color-hash": "^2.0.1",

View file

@ -1,5 +1,5 @@
/*
Copyright 2021 New Vector Ltd
Copyright 2021-2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,6 +15,7 @@ limitations under the License.
*/
import React, {
FC,
useCallback,
useEffect,
useState,
@ -23,17 +24,59 @@ import React, {
useContext,
} from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ErrorView } from "./FullScreenView";
import { initClient, defaultHomeserver } from "./matrix-utils";
const ClientContext = createContext();
declare global {
interface Window {
matrixclient: MatrixClient;
}
}
export function ClientProvider({ children }) {
export interface Session {
user_id: string;
device_id: string;
access_token: string;
passwordlessUser: boolean;
tempPassword?: string;
}
const loadSession = (): Session => {
const data = localStorage.getItem("matrix-auth-store");
if (data) return JSON.parse(data);
return null;
};
const saveSession = (session: Session) =>
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
const clearSession = () => localStorage.removeItem("matrix-auth-store");
interface ClientState {
loading: boolean;
isAuthenticated: boolean;
isPasswordlessUser: boolean;
client: MatrixClient;
userName: string;
changePassword: (password: string) => Promise<void>;
logout: () => void;
setClient: (client: MatrixClient, session: Session) => void;
}
const ClientContext = createContext<ClientState>(null);
type ClientProviderState = Omit<
ClientState,
"changePassword" | "logout" | "setClient"
> & { error?: Error };
export const ClientProvider: FC = ({ children }) => {
const history = useHistory();
const [
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
setState,
] = useState({
] = useState<ClientProviderState>({
loading: true,
isAuthenticated: false,
isPasswordlessUser: false,
@ -43,18 +86,16 @@ export function ClientProvider({ children }) {
});
useEffect(() => {
async function restore() {
const restore = async (): Promise<
Pick<ClientProviderState, "client" | "isPasswordlessUser">
> => {
try {
const authStore = localStorage.getItem("matrix-auth-store");
const session = loadSession();
if (authStore) {
const {
user_id,
device_id,
access_token,
passwordlessUser,
tempPassword,
} = JSON.parse(authStore);
if (session) {
/* eslint-disable camelcase */
const { user_id, device_id, access_token, passwordlessUser } =
session;
const client = await initClient({
baseUrl: defaultHomeserver,
@ -62,37 +103,26 @@ export function ClientProvider({ children }) {
userId: user_id,
deviceId: device_id,
});
/* eslint-enable camelcase */
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({
user_id,
device_id,
access_token,
passwordlessUser,
tempPassword,
})
);
return { client, passwordlessUser };
return { client, isPasswordlessUser: passwordlessUser };
}
return { client: undefined };
return { client: undefined, isPasswordlessUser: false };
} catch (err) {
console.error(err);
localStorage.removeItem("matrix-auth-store");
clearSession();
throw err;
}
}
};
restore()
.then(({ client, passwordlessUser }) => {
.then(({ client, isPasswordlessUser }) => {
setState({
client,
loading: false,
isAuthenticated: !!client,
isPasswordlessUser: !!passwordlessUser,
isAuthenticated: Boolean(client),
isPasswordlessUser,
userName: client?.getUserIdLocalpart(),
});
})
@ -108,31 +138,23 @@ export function ClientProvider({ children }) {
}, []);
const changePassword = useCallback(
async (password) => {
const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse(
localStorage.getItem("matrix-auth-store")
);
async (password: string) => {
const { tempPassword, ...session } = loadSession();
await client.setPassword(
{
type: "m.login.password",
identifier: {
type: "m.id.user",
user: existingSession.user_id,
user: session.user_id,
},
user: existingSession.user_id,
user: session.user_id,
password: tempPassword,
},
password
);
localStorage.setItem(
"matrix-auth-store",
JSON.stringify({
...existingSession,
passwordlessUser: false,
})
);
saveSession({ ...session, passwordlessUser: false });
setState({
client,
@ -146,23 +168,23 @@ export function ClientProvider({ children }) {
);
const setClient = useCallback(
(newClient, session) => {
(newClient: MatrixClient, session: Session) => {
if (client && client !== newClient) {
client.stopClient();
}
if (newClient) {
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
saveSession(session);
setState({
client: newClient,
loading: false,
isAuthenticated: true,
isPasswordlessUser: !!session.passwordlessUser,
isPasswordlessUser: session.passwordlessUser,
userName: newClient.getUserIdLocalpart(),
});
} else {
localStorage.removeItem("matrix-auth-store");
clearSession();
setState({
client: undefined,
@ -177,29 +199,23 @@ export function ClientProvider({ children }) {
);
const logout = useCallback(() => {
localStorage.removeItem("matrix-auth-store");
window.location = "/";
clearSession();
history.push("/");
}, [history]);
useEffect(() => {
if (client) {
const loadTime = Date.now();
const onToDeviceEvent = (event) => {
if (event.getType() !== "org.matrix.call_duplicate_session") {
return;
}
const onToDeviceEvent = (event: MatrixEvent) => {
if (event.getType() !== "org.matrix.call_duplicate_session") return;
const content = event.getContent();
if (content.session_id === client.getSessionId()) {
return;
}
if (content.session_id === client.getSessionId()) return;
if (content.timestamp > loadTime) {
if (client) {
client.stopClient();
}
client?.stopClient();
setState((prev) => ({
...prev,
@ -210,7 +226,7 @@ export function ClientProvider({ children }) {
}
};
client.on("toDeviceEvent", onToDeviceEvent);
client.on(ClientEvent.ToDeviceEvent, onToDeviceEvent);
client.sendToDevice("org.matrix.call_duplicate_session", {
[client.getUserId()]: {
@ -219,12 +235,12 @@ export function ClientProvider({ children }) {
});
return () => {
client.removeListener("toDeviceEvent", onToDeviceEvent);
client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
};
}
}, [client]);
const context = useMemo(
const context = useMemo<ClientState>(
() => ({
loading,
isAuthenticated,
@ -258,8 +274,6 @@ export function ClientProvider({ children }) {
return (
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
);
}
};
export function useClient() {
return useContext(ClientContext);
}
export const useClient = () => useContext(ClientContext);

View file

@ -1,5 +1,5 @@
/*
Copyright 2021 New Vector Ltd
Copyright 2021-2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,8 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useRef, useState, useMemo } from "react";
import React, {
FC,
FormEvent,
useCallback,
useRef,
useState,
useMemo,
} from "react";
import { useHistory, useLocation, Link } from "react-router-dom";
import { ReactComponent as Logo } from "../icons/LogoLarge.svg";
import { useClient } from "../ClientContext";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
@ -25,23 +33,23 @@ import styles from "./LoginPage.module.css";
import { useInteractiveLogin } from "./useInteractiveLogin";
import { usePageTitle } from "../usePageTitle";
export function LoginPage() {
export const LoginPage: FC = () => {
usePageTitle("Login");
const { setClient } = useClient();
const [_, login] = useInteractiveLogin();
const [homeserver, setHomeServer] = useState(defaultHomeserver);
const usernameRef = useRef();
const passwordRef = useRef();
const login = useInteractiveLogin();
const homeserver = defaultHomeserver; // TODO: Make this configurable
const usernameRef = useRef<HTMLInputElement>();
const passwordRef = useRef<HTMLInputElement>();
const history = useHistory();
const location = useLocation();
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [error, setError] = useState<Error>();
// TODO: Handle hitting login page with authenticated client
const onSubmitLoginForm = useCallback(
(e) => {
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
@ -60,13 +68,13 @@ export function LoginPage() {
setLoading(false);
});
},
[login, location, history, homeserver]
[login, location, history, homeserver, setClient]
);
const homeserverHost = useMemo(() => {
try {
return new URL(homeserver).host;
} catch (_error) {
} catch (error) {
return defaultHomeserverHost;
}
}, [homeserver]);
@ -125,4 +133,4 @@ export function LoginPage() {
</div>
</>
);
}
};

View file

@ -1,5 +1,5 @@
/*
Copyright 2021 New Vector Ltd
Copyright 2021-2022 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,10 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, {
FC,
FormEvent,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { useHistory, useLocation } from "react-router-dom";
import { captureException } from "@sentry/react";
import { sleep } from "matrix-js-sdk/src/utils";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Button } from "../button";
import { useClient } from "../ClientContext";
@ -30,46 +38,35 @@ import { useRecaptcha } from "./useRecaptcha";
import { Caption, Link } from "../typography/Typography";
import { usePageTitle } from "../usePageTitle";
export function RegisterPage() {
export const RegisterPage: FC = () => {
usePageTitle("Register");
const { loading, isAuthenticated, isPasswordlessUser, client, setClient } =
useClient();
const confirmPasswordRef = useRef();
const confirmPasswordRef = useRef<HTMLInputElement>();
const history = useHistory();
const location = useLocation();
const [registering, setRegistering] = useState(false);
const [error, setError] = useState();
const [error, setError] = useState<Error>();
const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");
const [{ privacyPolicyUrl, recaptchaKey }, register] =
const [privacyPolicyUrl, recaptchaKey, register] =
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const onSubmitRegisterForm = useCallback(
(e) => {
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.target);
const userName = data.get("userName");
const password = data.get("password");
const passwordConfirmation = data.get("passwordConfirmation");
const data = new FormData(e.target as HTMLFormElement);
const userName = data.get("userName") as string;
const password = data.get("password") as string;
const passwordConfirmation = data.get("passwordConfirmation") as string;
if (password !== passwordConfirmation) {
return;
}
if (password !== passwordConfirmation) return;
async function submit() {
const submit = async () => {
setRegistering(true);
let roomIds;
if (client && isPasswordlessUser) {
const groupCalls = client.groupCallEventHandler.groupCalls.values();
roomIds = Array.from(groupCalls).map(
(groupCall) => groupCall.room.roomId
);
}
const recaptchaResponse = await execute();
const [newClient, session] = await register(
userName,
@ -78,8 +75,11 @@ export function RegisterPage() {
recaptchaResponse
);
if (roomIds) {
for (const roomId of roomIds) {
if (client && isPasswordlessUser) {
// Migrate the user's rooms
for (const groupCall of client.groupCallEventHandler.groupCalls.values()) {
const roomId = groupCall.room.roomId;
try {
await newClient.joinRoom(roomId);
} catch (error) {
@ -95,11 +95,11 @@ export function RegisterPage() {
}
setClient(newClient, session);
}
};
submit()
.then(() => {
if (location.state && location.state.from) {
if (location.state?.from) {
history.push(location.state.from);
} else {
history.push("/");
@ -111,18 +111,23 @@ export function RegisterPage() {
reset();
});
},
[register, location, history, isPasswordlessUser, reset, execute, client]
[
register,
location,
history,
isPasswordlessUser,
reset,
execute,
client,
setClient,
]
);
useEffect(() => {
if (!confirmPasswordRef.current) {
return;
}
if (password && passwordConfirmation && password !== passwordConfirmation) {
confirmPasswordRef.current.setCustomValidity("Passwords must match");
confirmPasswordRef.current?.setCustomValidity("Passwords must match");
} else {
confirmPasswordRef.current.setCustomValidity("");
confirmPasswordRef.current?.setCustomValidity("");
}
}, [password, passwordConfirmation]);
@ -130,7 +135,7 @@ export function RegisterPage() {
if (!loading && isAuthenticated && !isPasswordlessUser && !registering) {
history.push("/");
}
}, [history, isAuthenticated, isPasswordlessUser, registering]);
}, [loading, history, isAuthenticated, isPasswordlessUser, registering]);
if (loading) {
return <LoadingView />;
@ -218,4 +223,4 @@ export function RegisterPage() {
</div>
</>
);
}
};

View file

@ -1,58 +0,0 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
import { useState, useCallback } from "react";
import { initClient, defaultHomeserver } from "../matrix-utils";
export function useInteractiveLogin() {
const [state, setState] = useState({ loading: false });
const auth = useCallback(async (homeserver, username, password) => {
const authClient = matrix.createClient(homeserver);
const interactiveAuth = new InteractiveAuth({
matrixClient: authClient,
busyChanged(loading) {
setState((prev) => ({ ...prev, loading }));
},
async doRequest(_auth, _background) {
return authClient.login("m.login.password", {
identifier: {
type: "m.id.user",
user: username,
},
password,
});
},
});
const { user_id, access_token, device_id } =
await interactiveAuth.attemptAuth();
const session = { user_id, access_token, device_id };
const client = await initClient({
baseUrl: defaultHomeserver,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
return [client, session];
}, []);
return [state, auth];
}

View file

@ -0,0 +1,55 @@
/*
Copyright 2022 Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useCallback } from "react";
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
import { initClient, defaultHomeserver } from "../matrix-utils";
export const useInteractiveLogin = () =>
useCallback(
async (homeserver: string, username: string, password: string) => {
const authClient = matrix.createClient(homeserver);
const interactiveAuth = new InteractiveAuth({
matrixClient: authClient,
doRequest: () =>
authClient.login("m.login.password", {
identifier: {
type: "m.id.user",
user: username,
},
password,
}),
});
/* eslint-disable camelcase */
const { user_id, access_token, device_id } =
await interactiveAuth.attemptAuth();
const session = { user_id, access_token, device_id };
const client = await initClient({
baseUrl: defaultHomeserver,
accessToken: access_token,
userId: user_id,
deviceId: device_id,
});
/* eslint-enable camelcase */
return [client, session];
},
[]
);

View file

@ -14,56 +14,60 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
import { useState, useEffect, useCallback, useRef } from "react";
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { initClient, defaultHomeserver } from "../matrix-utils";
import { Session } from "../ClientContext";
export function useInteractiveRegistration() {
const [state, setState] = useState({
privacyPolicyUrl: null,
loading: false,
});
export const useInteractiveRegistration = (): [
string,
string,
(
username: string,
password: string,
displayName: string,
recaptchaResponse: string,
passwordlessUser?: boolean
) => Promise<[MatrixClient, Session]>
] => {
const [privacyPolicyUrl, setPrivacyPolicyUrl] = useState<string>();
const [recaptchaKey, setRecaptchaKey] = useState<string>();
const authClientRef = useRef();
const authClient = useRef<MatrixClient>();
if (!authClient.current) {
authClient.current = matrix.createClient(defaultHomeserver);
}
useEffect(() => {
authClientRef.current = matrix.createClient(defaultHomeserver);
authClientRef.current.registerRequest({}).catch((error) => {
const privacyPolicyUrl =
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url;
const recaptchaKey = error.data?.params["m.login.recaptcha"]?.public_key;
if (privacyPolicyUrl || recaptchaKey) {
setState((prev) => ({ ...prev, privacyPolicyUrl, recaptchaKey }));
}
authClient.current.registerRequest({}).catch((error) => {
setPrivacyPolicyUrl(
error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url
);
setRecaptchaKey(error.data?.params["m.login.recaptcha"]?.public_key);
});
}, []);
const register = useCallback(
async (
username,
password,
displayName,
recaptchaResponse,
passwordlessUser
) => {
username: string,
password: string,
displayName: string,
recaptchaResponse: string,
passwordlessUser?: boolean
): Promise<[MatrixClient, Session]> => {
const interactiveAuth = new InteractiveAuth({
matrixClient: authClientRef.current,
busyChanged(loading) {
setState((prev) => ({ ...prev, loading }));
},
async doRequest(auth, _background) {
return authClientRef.current.registerRequest({
matrixClient: authClient.current,
doRequest: (auth) =>
authClient.current.registerRequest({
username,
password,
auth: auth || undefined,
});
},
stateUpdated(nextStage, status) {
}),
stateUpdated: (nextStage, status) => {
if (status.error) {
throw new Error(error);
throw new Error(status.error);
}
if (nextStage === "m.login.terms") {
@ -79,6 +83,7 @@ export function useInteractiveRegistration() {
},
});
/* eslint-disable camelcase */
const { user_id, access_token, device_id } =
await interactiveAuth.attemptAuth();
@ -91,14 +96,19 @@ export function useInteractiveRegistration() {
await client.setDisplayName(displayName);
const session = { user_id, device_id, access_token, passwordlessUser };
const session: Session = {
user_id,
device_id,
access_token,
passwordlessUser,
};
/* eslint-enable camelcase */
if (passwordlessUser) {
session.tempPassword = password;
}
const user = client.getUser(client.getUserId());
user.setRawDisplayName(displayName);
user.setDisplayName(displayName);
@ -107,5 +117,5 @@ export function useInteractiveRegistration() {
[]
);
return [state, register];
}
return [privacyPolicyUrl, recaptchaKey, register];
};

View file

@ -14,52 +14,49 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { randomString } from "matrix-js-sdk/src/randomstring";
import { useEffect, useCallback, useRef, useState } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
declare global {
interface Window {
mxOnRecaptchaLoaded: () => void;
}
}
const RECAPTCHA_SCRIPT_URL =
"https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit";
export function useRecaptcha(sitekey) {
interface RecaptchaPromiseRef {
resolve: (response: string) => void;
reject: (error: Error) => void;
}
export const useRecaptcha = (sitekey: string) => {
const [recaptchaId] = useState(() => randomString(16));
const promiseRef = useRef();
const promiseRef = useRef<RecaptchaPromiseRef>();
useEffect(() => {
if (!sitekey) {
return;
}
if (!sitekey) return;
const onRecaptchaLoaded = () => {
if (!document.getElementById(recaptchaId)) {
return;
}
if (!document.getElementById(recaptchaId)) return;
window.grecaptcha.render(recaptchaId, {
sitekey,
size: "invisible",
callback: (response) => {
if (promiseRef.current) {
promiseRef.current.resolve(response);
}
},
"error-callback": (error) => {
if (promiseRef.current) {
promiseRef.current.reject(error);
}
},
callback: (response: string) => promiseRef.current?.resolve(response),
// eslint-disable-next-line @typescript-eslint/naming-convention
"error-callback": () => promiseRef.current?.reject(new Error()),
});
};
if (
typeof window.grecaptcha !== "undefined" &&
typeof window.grecaptcha.render === "function"
) {
if (typeof window.grecaptcha?.render === "function") {
onRecaptchaLoaded();
} else {
window.mxOnRecaptchaLoaded = onRecaptchaLoaded;
if (!document.querySelector(`script[src="${RECAPTCHA_SCRIPT_URL}"]`)) {
const scriptTag = document.createElement("script");
const scriptTag = document.createElement("script") as HTMLScriptElement;
scriptTag.src = RECAPTCHA_SCRIPT_URL;
scriptTag.async = true;
document.body.appendChild(scriptTag);
@ -80,7 +77,7 @@ export function useRecaptcha(sitekey) {
return new Promise((resolve, reject) => {
const observer = new MutationObserver((mutationsList) => {
for (const item of mutationsList) {
if (item.target?.style?.visibility !== "visible") {
if ((item.target as HTMLElement)?.style?.visibility !== "visible") {
reject(new Error("Recaptcha dismissed"));
observer.disconnect();
return;
@ -101,7 +98,7 @@ export function useRecaptcha(sitekey) {
window.grecaptcha.execute();
const iframe = document.querySelector(
const iframe = document.querySelector<HTMLIFrameElement>(
'iframe[src*="recaptcha/api2/bframe"]'
);
@ -111,13 +108,11 @@ export function useRecaptcha(sitekey) {
});
}
});
}, [recaptchaId, sitekey]);
}, [sitekey]);
const reset = useCallback(() => {
if (window.grecaptcha) {
window.grecaptcha.reset();
}
}, [recaptchaId]);
window.grecaptcha?.reset();
}, []);
return { execute, reset, recaptchaId };
}
};

View file

@ -39,7 +39,7 @@ export function UnauthenticatedView() {
const [callType, setCallType] = useState(CallType.Video);
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [{ privacyPolicyUrl, recaptchaKey }, register] =
const [privacyPolicyUrl, recaptchaKey, register] =
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);

View file

@ -33,7 +33,7 @@ export function RoomAuthView() {
const { setClient } = useClient();
const [loading, setLoading] = useState(false);
const [error, setError] = useState();
const [{ privacyPolicyUrl, recaptchaKey }, register] =
const [privacyPolicyUrl, recaptchaKey, register] =
useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);

View file

@ -2876,6 +2876,11 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/grecaptcha@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/grecaptcha/-/grecaptcha-3.0.4.tgz#3de601f3b0cd0298faf052dd5bd62aff64c2be2e"
integrity sha512-7l1Y8DTGXkx/r4pwU1nMVAR+yD/QC+MCHKXAyEX/7JZhwcN1IED09aZ9vCjjkcGdhSQiu/eJqcXInpl6eEEEwg==
"@types/hast@^2.0.0":
version "2.3.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"