From 66e5ec976ba96e6ff2fdf70519cc6461475b182a Mon Sep 17 00:00:00 2001 From: Robert Long Date: Mon, 20 Dec 2021 13:15:35 -0800 Subject: [PATCH] Add privacy policy flow --- src/ConferenceCallManagerHooks.jsx | 132 ++++++++++++++++++++++++++++- src/Home.jsx | 39 ++++++++- src/Input.module.css | 1 + src/LoginPage.jsx | 4 +- src/RegisterPage.jsx | 41 ++++++++- 5 files changed, 209 insertions(+), 8 deletions(-) diff --git a/src/ConferenceCallManagerHooks.jsx b/src/ConferenceCallManagerHooks.jsx index 51583c23..cfed40c4 100644 --- a/src/ConferenceCallManagerHooks.jsx +++ b/src/ConferenceCallManagerHooks.jsx @@ -21,8 +21,9 @@ import React, { createContext, useMemo, useContext, + useRef, } from "react"; -import matrix from "matrix-js-sdk/src/browser-index"; +import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index"; import { GroupCallIntent, GroupCallType, @@ -385,6 +386,32 @@ export function ClientProvider({ children }) { [client] ); + const setClient = useCallback((client, session) => { + if (client) { + localStorage.setItem("matrix-auth-store", JSON.stringify(session)); + + setState({ + client, + loading: false, + isAuthenticated: true, + isPasswordlessUser: false, + isGuest: false, + userName: client.getUserIdLocalpart(), + }); + } else { + localStorage.removeItem("matrix-auth-store"); + + setState({ + client: undefined, + loading: false, + isAuthenticated: false, + isPasswordlessUser: false, + isGuest: false, + userName: null, + }); + } + }, []); + const logout = useCallback(() => { localStorage.removeItem("matrix-auth-store"); window.location = "/"; @@ -403,6 +430,7 @@ export function ClientProvider({ children }) { changePassword, logout, userName, + setClient, }), [ loading, @@ -416,6 +444,7 @@ export function ClientProvider({ children }) { changePassword, logout, userName, + setClient, ] ); @@ -718,3 +747,104 @@ export function useProfile(client) { return { loading, error, displayName, avatarUrl, saveProfile, success }; } + +export function useInteractiveLogin() { + const { setClient } = useClient(); + 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, + }); + }, + stateUpdated(nextStage, status) { + console.log({ nextStage, status }); + }, + }); + + const { user_id, access_token, device_id } = + await interactiveAuth.attemptAuth(); + + const client = await initClient({ + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }); + + setClient(client, { user_id, access_token, device_id }); + + return client; + }, []); + + return [state, auth]; +} + +export function useInteractiveRegistration() { + const { setClient } = useClient(); + const [state, setState] = useState({ privacyPolicyUrl: "#", loading: false }); + + const authClientRef = useRef(); + + useEffect(() => { + authClientRef.current = matrix.createClient(defaultHomeserver); + + authClientRef.current.registerRequest({}).catch((error) => { + const privacyPolicyUrl = + error.data?.params["m.login.terms"]?.policies?.privacy_policy?.en?.url; + + if (privacyPolicyUrl) { + setState((prev) => ({ ...prev, privacyPolicyUrl })); + } + }); + }, []); + + const register = useCallback(async (username, password) => { + const interactiveAuth = new InteractiveAuth({ + matrixClient: authClientRef.current, + busyChanged(loading) { + setState((prev) => ({ ...prev, loading })); + }, + async doRequest(auth, _background) { + return authClientRef.current.registerRequest({ + username, + password, + auth: auth || undefined, + }); + }, + stateUpdated(nextStage, status) { + if (nextStage === "m.login.terms") { + interactiveAuth.submitAuthDict({ type: "m.login.terms" }); + } + }, + }); + + const { user_id, access_token, device_id } = + await interactiveAuth.attemptAuth(); + + const client = await initClient({ + baseUrl: defaultHomeserver, + accessToken: access_token, + userId: user_id, + deviceId: device_id, + }); + + setClient(client, { user_id, access_token, device_id }); + + return client; + }, []); + + return [state, register]; +} diff --git a/src/Home.jsx b/src/Home.jsx index 7baa06a2..e64c7db8 100644 --- a/src/Home.jsx +++ b/src/Home.jsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useState, useRef, useEffect } from "react"; import { useHistory, Link } from "react-router-dom"; import { useClient, @@ -22,6 +22,7 @@ import { usePublicRooms, createRoom, roomAliasFromRoomName, + useInteractiveRegistration, } from "./ConferenceCallManagerHooks"; import { Header, HeaderLogo, LeftNav, RightNav } from "./Header"; import styles from "./Home.module.css"; @@ -43,9 +44,10 @@ export function Home() { loading, error, client, - register, } = useClient(); + const [{ privacyPolicyUrl }, register] = useInteractiveRegistration(); + const history = useHistory(); const [creatingRoom, setCreatingRoom] = useState(false); const [createRoomError, setCreateRoomError] = useState(); @@ -118,6 +120,7 @@ export function Home() { createRoomError={createRoomError} creatingRoom={creatingRoom} onJoinRoom={onJoinRoom} + privacyPolicyUrl={privacyPolicyUrl} /> ) : ( { + if (!acceptTermsRef.current) { + return; + } + + if (!acceptTerms) { + acceptTermsRef.current.setCustomValidity( + "You must accept the terms to continue." + ); + } else { + acceptTermsRef.current.setCustomValidity(""); + } + }, [acceptTerms]); + return (
@@ -202,6 +223,20 @@ function UnregisteredView({ placeholder="Room Name" /> + + setAcceptTerms(e.target.checked)} + checked={acceptTerms} + label="Accept Privacy Policy" + ref={acceptTermsRef} + /> + + Privacy Policy + + {createRoomError && ( {createRoomError.message} diff --git a/src/Input.module.css b/src/Input.module.css index de546387..4555d340 100644 --- a/src/Input.module.css +++ b/src/Input.module.css @@ -1,6 +1,7 @@ .fieldRow { display: flex; margin-bottom: 32px; + align-items: center; } .field { diff --git a/src/LoginPage.jsx b/src/LoginPage.jsx index 48654023..60992611 100644 --- a/src/LoginPage.jsx +++ b/src/LoginPage.jsx @@ -20,14 +20,14 @@ import { ReactComponent as Logo } from "./icons/LogoLarge.svg"; import { FieldRow, InputField, ErrorMessage } from "./Input"; import { Button } from "./button"; import { - useClient, defaultHomeserver, defaultHomeserverHost, + useInteractiveLogin, } from "./ConferenceCallManagerHooks"; import styles from "./LoginPage.module.css"; export function LoginPage() { - const { login } = useClient(); + const [_, login] = useInteractiveLogin(); const [homeserver, setHomeServer] = useState(defaultHomeserver); const usernameRef = useRef(); const passwordRef = useRef(); diff --git a/src/RegisterPage.jsx b/src/RegisterPage.jsx index 5bda78c3..e79587c9 100644 --- a/src/RegisterPage.jsx +++ b/src/RegisterPage.jsx @@ -18,7 +18,11 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { useHistory, useLocation, Link } from "react-router-dom"; import { FieldRow, InputField, ErrorMessage } from "./Input"; import { Button } from "./button"; -import { useClient, defaultHomeserverHost } from "./ConferenceCallManagerHooks"; +import { + useClient, + defaultHomeserverHost, + useInteractiveRegistration, +} from "./ConferenceCallManagerHooks"; import styles from "./LoginPage.module.css"; import { ReactComponent as Logo } from "./icons/LogoLarge.svg"; import { LoadingView } from "./FullScreenView"; @@ -27,18 +31,20 @@ export function RegisterPage() { const { loading, client, - register, changePassword, isAuthenticated, isPasswordlessUser, } = useClient(); const confirmPasswordRef = useRef(); + const acceptTermsRef = useRef(); const history = useHistory(); const location = useLocation(); const [registering, setRegistering] = useState(false); const [error, setError] = useState(); const [password, setPassword] = useState(""); const [passwordConfirmation, setPasswordConfirmation] = useState(""); + const [acceptTerms, setAcceptTerms] = useState(false); + const [{ privacyPolicyUrl }, register] = useInteractiveRegistration(); const onSubmitRegisterForm = useCallback( (e) => { @@ -47,8 +53,9 @@ export function RegisterPage() { const userName = data.get("userName"); const password = data.get("password"); const passwordConfirmation = data.get("passwordConfirmation"); + const acceptTerms = data.get("acceptTerms"); - if (password !== passwordConfirmation) { + if (password !== passwordConfirmation || !acceptTerms) { return; } @@ -97,6 +104,20 @@ export function RegisterPage() { } }, [password, passwordConfirmation]); + useEffect(() => { + if (!acceptTermsRef.current) { + return; + } + + if (!acceptTerms) { + acceptTermsRef.current.setCustomValidity( + "You must accept the terms to continue." + ); + } else { + acceptTermsRef.current.setCustomValidity(""); + } + }, [acceptTerms]); + useEffect(() => { if (!loading && isAuthenticated && !isPasswordlessUser) { history.push("/"); @@ -156,6 +177,20 @@ export function RegisterPage() { ref={confirmPasswordRef} /> + + setAcceptTerms(e.target.checked)} + checked={acceptTerms} + label="Accept Privacy Policy" + ref={acceptTermsRef} + /> + + Privacy Policy + + {error && ( {error.message}