TypeScriptify useInteractiveRegistration

This commit is contained in:
Robin Townsend 2022-05-27 16:08:03 -04:00
parent 35e2135e3c
commit c057713004
5 changed files with 133 additions and 112 deletions

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

View file

@ -42,7 +42,7 @@ export function RegisterPage() {
const [error, setError] = useState(); const [error, setError] = useState();
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState("");
const [{ privacyPolicyUrl, recaptchaKey }, register] = const [privacyPolicyUrl, recaptchaKey, register] =
useInteractiveRegistration(); useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey); const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);

View file

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

View file

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

View file

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