Add privacy policy flow
This commit is contained in:
parent
493445a6b0
commit
66e5ec976b
5 changed files with 209 additions and 8 deletions
|
@ -21,8 +21,9 @@ import React, {
|
||||||
createContext,
|
createContext,
|
||||||
useMemo,
|
useMemo,
|
||||||
useContext,
|
useContext,
|
||||||
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import matrix from "matrix-js-sdk/src/browser-index";
|
import matrix, { InteractiveAuth } from "matrix-js-sdk/src/browser-index";
|
||||||
import {
|
import {
|
||||||
GroupCallIntent,
|
GroupCallIntent,
|
||||||
GroupCallType,
|
GroupCallType,
|
||||||
|
@ -385,6 +386,32 @@ export function ClientProvider({ children }) {
|
||||||
[client]
|
[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(() => {
|
const logout = useCallback(() => {
|
||||||
localStorage.removeItem("matrix-auth-store");
|
localStorage.removeItem("matrix-auth-store");
|
||||||
window.location = "/";
|
window.location = "/";
|
||||||
|
@ -403,6 +430,7 @@ export function ClientProvider({ children }) {
|
||||||
changePassword,
|
changePassword,
|
||||||
logout,
|
logout,
|
||||||
userName,
|
userName,
|
||||||
|
setClient,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
loading,
|
loading,
|
||||||
|
@ -416,6 +444,7 @@ export function ClientProvider({ children }) {
|
||||||
changePassword,
|
changePassword,
|
||||||
logout,
|
logout,
|
||||||
userName,
|
userName,
|
||||||
|
setClient,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -718,3 +747,104 @@ export function useProfile(client) {
|
||||||
|
|
||||||
return { loading, error, displayName, avatarUrl, saveProfile, success };
|
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];
|
||||||
|
}
|
||||||
|
|
39
src/Home.jsx
39
src/Home.jsx
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { useHistory, Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
useClient,
|
useClient,
|
||||||
|
@ -22,6 +22,7 @@ import {
|
||||||
usePublicRooms,
|
usePublicRooms,
|
||||||
createRoom,
|
createRoom,
|
||||||
roomAliasFromRoomName,
|
roomAliasFromRoomName,
|
||||||
|
useInteractiveRegistration,
|
||||||
} from "./ConferenceCallManagerHooks";
|
} from "./ConferenceCallManagerHooks";
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||||
import styles from "./Home.module.css";
|
import styles from "./Home.module.css";
|
||||||
|
@ -43,9 +44,10 @@ export function Home() {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
client,
|
client,
|
||||||
register,
|
|
||||||
} = useClient();
|
} = useClient();
|
||||||
|
|
||||||
|
const [{ privacyPolicyUrl }, register] = useInteractiveRegistration();
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [creatingRoom, setCreatingRoom] = useState(false);
|
const [creatingRoom, setCreatingRoom] = useState(false);
|
||||||
const [createRoomError, setCreateRoomError] = useState();
|
const [createRoomError, setCreateRoomError] = useState();
|
||||||
|
@ -118,6 +120,7 @@ export function Home() {
|
||||||
createRoomError={createRoomError}
|
createRoomError={createRoomError}
|
||||||
creatingRoom={creatingRoom}
|
creatingRoom={creatingRoom}
|
||||||
onJoinRoom={onJoinRoom}
|
onJoinRoom={onJoinRoom}
|
||||||
|
privacyPolicyUrl={privacyPolicyUrl}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RegisteredView
|
<RegisteredView
|
||||||
|
@ -143,7 +146,25 @@ function UnregisteredView({
|
||||||
createRoomError,
|
createRoomError,
|
||||||
creatingRoom,
|
creatingRoom,
|
||||||
onJoinRoom,
|
onJoinRoom,
|
||||||
|
privacyPolicyUrl,
|
||||||
}) {
|
}) {
|
||||||
|
const acceptTermsRef = useRef();
|
||||||
|
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!acceptTermsRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptTerms) {
|
||||||
|
acceptTermsRef.current.setCustomValidity(
|
||||||
|
"You must accept the terms to continue."
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
acceptTermsRef.current.setCustomValidity("");
|
||||||
|
}
|
||||||
|
}, [acceptTerms]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.home, styles.fullWidth)}>
|
<div className={classNames(styles.home, styles.fullWidth)}>
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header}>
|
||||||
|
@ -202,6 +223,20 @@ function UnregisteredView({
|
||||||
placeholder="Room Name"
|
placeholder="Room Name"
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="acceptTerms"
|
||||||
|
type="checkbox"
|
||||||
|
name="acceptTerms"
|
||||||
|
onChange={(e) => setAcceptTerms(e.target.checked)}
|
||||||
|
checked={acceptTerms}
|
||||||
|
label="Accept Privacy Policy"
|
||||||
|
ref={acceptTermsRef}
|
||||||
|
/>
|
||||||
|
<a target="_blank" href={privacyPolicyUrl}>
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
</FieldRow>
|
||||||
{createRoomError && (
|
{createRoomError && (
|
||||||
<FieldRow className={styles.fieldRow}>
|
<FieldRow className={styles.fieldRow}>
|
||||||
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
<ErrorMessage>{createRoomError.message}</ErrorMessage>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
.fieldRow {
|
.fieldRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
|
|
|
@ -20,14 +20,14 @@ import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
import {
|
import {
|
||||||
useClient,
|
|
||||||
defaultHomeserver,
|
defaultHomeserver,
|
||||||
defaultHomeserverHost,
|
defaultHomeserverHost,
|
||||||
|
useInteractiveLogin,
|
||||||
} from "./ConferenceCallManagerHooks";
|
} from "./ConferenceCallManagerHooks";
|
||||||
import styles from "./LoginPage.module.css";
|
import styles from "./LoginPage.module.css";
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const { login } = useClient();
|
const [_, login] = useInteractiveLogin();
|
||||||
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
const [homeserver, setHomeServer] = useState(defaultHomeserver);
|
||||||
const usernameRef = useRef();
|
const usernameRef = useRef();
|
||||||
const passwordRef = useRef();
|
const passwordRef = useRef();
|
||||||
|
|
|
@ -18,7 +18,11 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useHistory, useLocation, Link } from "react-router-dom";
|
import { useHistory, useLocation, Link } from "react-router-dom";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
import { FieldRow, InputField, ErrorMessage } from "./Input";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
import { useClient, defaultHomeserverHost } from "./ConferenceCallManagerHooks";
|
import {
|
||||||
|
useClient,
|
||||||
|
defaultHomeserverHost,
|
||||||
|
useInteractiveRegistration,
|
||||||
|
} from "./ConferenceCallManagerHooks";
|
||||||
import styles from "./LoginPage.module.css";
|
import styles from "./LoginPage.module.css";
|
||||||
import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
|
import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
|
||||||
import { LoadingView } from "./FullScreenView";
|
import { LoadingView } from "./FullScreenView";
|
||||||
|
@ -27,18 +31,20 @@ export function RegisterPage() {
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
client,
|
client,
|
||||||
register,
|
|
||||||
changePassword,
|
changePassword,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
} = useClient();
|
} = useClient();
|
||||||
const confirmPasswordRef = useRef();
|
const confirmPasswordRef = useRef();
|
||||||
|
const acceptTermsRef = useRef();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [registering, setRegistering] = useState(false);
|
const [registering, setRegistering] = useState(false);
|
||||||
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 [acceptTerms, setAcceptTerms] = useState(false);
|
||||||
|
const [{ privacyPolicyUrl }, register] = useInteractiveRegistration();
|
||||||
|
|
||||||
const onSubmitRegisterForm = useCallback(
|
const onSubmitRegisterForm = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
@ -47,8 +53,9 @@ export function RegisterPage() {
|
||||||
const userName = data.get("userName");
|
const userName = data.get("userName");
|
||||||
const password = data.get("password");
|
const password = data.get("password");
|
||||||
const passwordConfirmation = data.get("passwordConfirmation");
|
const passwordConfirmation = data.get("passwordConfirmation");
|
||||||
|
const acceptTerms = data.get("acceptTerms");
|
||||||
|
|
||||||
if (password !== passwordConfirmation) {
|
if (password !== passwordConfirmation || !acceptTerms) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +104,20 @@ export function RegisterPage() {
|
||||||
}
|
}
|
||||||
}, [password, passwordConfirmation]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!loading && isAuthenticated && !isPasswordlessUser) {
|
if (!loading && isAuthenticated && !isPasswordlessUser) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
|
@ -156,6 +177,20 @@ export function RegisterPage() {
|
||||||
ref={confirmPasswordRef}
|
ref={confirmPasswordRef}
|
||||||
/>
|
/>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="acceptTerms"
|
||||||
|
type="checkbox"
|
||||||
|
name="acceptTerms"
|
||||||
|
onChange={(e) => setAcceptTerms(e.target.checked)}
|
||||||
|
checked={acceptTerms}
|
||||||
|
label="Accept Privacy Policy"
|
||||||
|
ref={acceptTermsRef}
|
||||||
|
/>
|
||||||
|
<a target="_blank" href={privacyPolicyUrl}>
|
||||||
|
Privacy Policy
|
||||||
|
</a>
|
||||||
|
</FieldRow>
|
||||||
{error && (
|
{error && (
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<ErrorMessage>{error.message}</ErrorMessage>
|
<ErrorMessage>{error.message}</ErrorMessage>
|
||||||
|
|
Loading…
Add table
Reference in a new issue