From a7539eb34e7dfb538d7ad31f25760d8d940d19f2 Mon Sep 17 00:00:00 2001
From: Robert Long <robert@robertlong.me>
Date: Thu, 16 Dec 2021 16:43:35 -0800
Subject: [PATCH] Add change password flow for anonymous users

---
 src/ConferenceCallManagerHooks.jsx |  63 ++++++++++++++++--
 src/Input.jsx                      |  19 +++++-
 src/Input.module.css               |  11 +++-
 src/RegisterPage.jsx               | 100 ++++++++++++++++++++++-------
 src/Room.jsx                       |  33 ++++++----
 5 files changed, 181 insertions(+), 45 deletions(-)

diff --git a/src/ConferenceCallManagerHooks.jsx b/src/ConferenceCallManagerHooks.jsx
index b559a25..2e5d32d 100644
--- a/src/ConferenceCallManagerHooks.jsx
+++ b/src/ConferenceCallManagerHooks.jsx
@@ -130,8 +130,14 @@ export function ClientProvider({ homeserverUrl, children }) {
         const authStore = localStorage.getItem("matrix-auth-store");
 
         if (authStore) {
-          const { user_id, device_id, access_token, guest, passwordlessUser } =
-            JSON.parse(authStore);
+          const {
+            user_id,
+            device_id,
+            access_token,
+            guest,
+            passwordlessUser,
+            tempPassword,
+          } = JSON.parse(authStore);
 
           const client = await initClient(
             {
@@ -151,6 +157,7 @@ export function ClientProvider({ homeserverUrl, children }) {
               access_token,
               guest,
               passwordlessUser,
+              tempPassword,
             })
           );
 
@@ -311,10 +318,13 @@ export function ClientProvider({ homeserverUrl, children }) {
         deviceId: device_id,
       });
 
-      localStorage.setItem(
-        "matrix-auth-store",
-        JSON.stringify({ user_id, device_id, access_token, passwordlessUser })
-      );
+      const session = { user_id, device_id, access_token, passwordlessUser };
+
+      if (passwordlessUser) {
+        session.tempPassword = password;
+      }
+
+      localStorage.setItem("matrix-auth-store", JSON.stringify(session));
 
       setState({
         client,
@@ -340,6 +350,45 @@ export function ClientProvider({ homeserverUrl, children }) {
     }
   }, []);
 
+  const changePassword = useCallback(
+    async (password) => {
+      const { tempPassword, passwordlessUser, ...existingSession } = JSON.parse(
+        localStorage.getItem("matrix-auth-store")
+      );
+
+      await client.setPassword(
+        {
+          type: "m.login.password",
+          identifier: {
+            type: "m.id.user",
+            user: existingSession.user_id,
+          },
+          user: existingSession.user_id,
+          password: tempPassword,
+        },
+        password
+      );
+
+      localStorage.setItem(
+        "matrix-auth-store",
+        JSON.stringify({
+          ...existingSession,
+          passwordlessUser: false,
+        })
+      );
+
+      setState({
+        client,
+        loading: false,
+        isGuest: false,
+        isAuthenticated: true,
+        isPasswordlessUser: false,
+        userName: client.getUserIdLocalpart(),
+      });
+    },
+    [client]
+  );
+
   const logout = useCallback(() => {
     localStorage.removeItem("matrix-auth-store");
     window.location = "/";
@@ -355,6 +404,7 @@ export function ClientProvider({ homeserverUrl, children }) {
       login,
       registerGuest,
       register,
+      changePassword,
       logout,
       userName,
     }),
@@ -367,6 +417,7 @@ export function ClientProvider({ homeserverUrl, children }) {
       login,
       registerGuest,
       register,
+      changePassword,
       logout,
       userName,
     ]
diff --git a/src/Input.jsx b/src/Input.jsx
index d890ab3..b9ccdc1 100644
--- a/src/Input.jsx
+++ b/src/Input.jsx
@@ -22,17 +22,30 @@ export function Field({ children, className, ...rest }) {
 }
 
 export const InputField = forwardRef(
-  ({ id, label, className, type, checked, prefix, suffix, ...rest }, ref) => {
+  (
+    { id, label, className, type, checked, prefix, suffix, disabled, ...rest },
+    ref
+  ) => {
     return (
       <Field
         className={classNames(
           type === "checkbox" ? styles.checkboxField : styles.inputField,
-          { [styles.prefix]: !!prefix },
+          {
+            [styles.prefix]: !!prefix,
+            [styles.disabled]: disabled,
+          },
           className
         )}
       >
         {prefix && <span>{prefix}</span>}
-        <input id={id} {...rest} ref={ref} type={type} checked={checked} />
+        <input
+          id={id}
+          {...rest}
+          ref={ref}
+          type={type}
+          checked={checked}
+          disabled={disabled}
+        />
         <label htmlFor={id}>
           {type === "checkbox" && (
             <div className={styles.checkbox}>
diff --git a/src/Input.module.css b/src/Input.module.css
index b47a20d..de54638 100644
--- a/src/Input.module.css
+++ b/src/Input.module.css
@@ -33,17 +33,26 @@
   font-size: 15px;
   border: none;
   border-radius: 4px;
-  padding: 11px 9px;
+  padding: 12px 9px 10px 9px;
   color: var(--textColor1);
   background-color: var(--bgColor1);
   flex: 1;
   min-width: 0;
 }
 
+.inputField.disabled input,
+.inputField.disabled span {
+  color: var(--textColor2);
+}
+
 .inputField span {
   padding: 11px 9px;
 }
 
+.inputField span:first-child {
+  padding-right: 0;
+}
+
 .inputField input::placeholder {
   transition: color 0.25s ease-in 0s;
   color: transparent;
diff --git a/src/RegisterPage.jsx b/src/RegisterPage.jsx
index a5cf3bb..fe54371 100644
--- a/src/RegisterPage.jsx
+++ b/src/RegisterPage.jsx
@@ -21,14 +21,21 @@ import { Button } from "./button";
 import { useClient } from "./ConferenceCallManagerHooks";
 import styles from "./LoginPage.module.css";
 import { ReactComponent as Logo } from "./icons/LogoLarge.svg";
+import { LoadingView } from "./FullScreenView";
 
 export function RegisterPage() {
-  const { register } = useClient();
-  const registerUsernameRef = useRef();
+  const {
+    loading,
+    client,
+    register,
+    changePassword,
+    isAuthenticated,
+    isPasswordlessUser,
+  } = useClient();
   const confirmPasswordRef = useRef();
   const history = useHistory();
   const location = useLocation();
-  const [loading, setLoading] = useState(false);
+  const [registering, setRegistering] = useState(false);
   const [error, setError] = useState();
   const [password, setPassword] = useState("");
   const [passwordConfirmation, setPasswordConfirmation] = useState("");
@@ -36,27 +43,55 @@ export function RegisterPage() {
   const onSubmitRegisterForm = useCallback(
     (e) => {
       e.preventDefault();
-      setLoading(true);
-      register(
-        registerUsernameRef.current.value,
-        registerPasswordRef.current.value
-      )
-        .then(() => {
-          if (location.state && location.state.from) {
-            history.push(location.state.from);
-          } else {
-            history.push("/");
-          }
-        })
-        .catch((error) => {
-          setError(error);
-          setLoading(false);
-        });
+      const data = new FormData(e.target);
+      const userName = data.get("userName");
+      const password = data.get("password");
+      const passwordConfirmation = data.get("passwordConfirmation");
+
+      if (password !== passwordConfirmation) {
+        return;
+      }
+
+      setRegistering(true);
+
+      console.log(isPasswordlessUser);
+
+      if (isPasswordlessUser) {
+        changePassword(password)
+          .then(() => {
+            if (location.state && location.state.from) {
+              history.push(location.state.from);
+            } else {
+              history.push("/");
+            }
+          })
+          .catch((error) => {
+            setError(error);
+            setRegistering(false);
+          });
+      } else {
+        register(userName, password)
+          .then(() => {
+            if (location.state && location.state.from) {
+              history.push(location.state.from);
+            } else {
+              history.push("/");
+            }
+          })
+          .catch((error) => {
+            setError(error);
+            setRegistering(false);
+          });
+      }
     },
-    [register, location, history]
+    [register, changePassword, location, history, isPasswordlessUser]
   );
 
   useEffect(() => {
+    if (!confirmPasswordRef.current) {
+      return;
+    }
+
     if (password && passwordConfirmation && password !== passwordConfirmation) {
       confirmPasswordRef.current.setCustomValidity("Passwords must match");
     } else {
@@ -64,6 +99,16 @@ export function RegisterPage() {
     }
   }, [password, passwordConfirmation]);
 
+  useEffect(() => {
+    if (!loading && isAuthenticated && !isPasswordlessUser) {
+      history.push("/");
+    }
+  }, [history, isAuthenticated, isPasswordlessUser]);
+
+  if (loading) {
+    return <LoadingView />;
+  }
+
   return (
     <>
       <div className={styles.container}>
@@ -75,18 +120,26 @@ export function RegisterPage() {
               <FieldRow>
                 <InputField
                   type="text"
-                  ref={registerUsernameRef}
+                  name="userName"
                   placeholder="Username"
                   label="Username"
                   autoCorrect="off"
                   autoCapitalize="none"
                   prefix="@"
                   suffix={`:${window.location.host}`}
+                  value={
+                    isAuthenticated && isPasswordlessUser
+                      ? client.getUserIdLocalpart()
+                      : undefined
+                  }
+                  onChange={(e) => setUserName(e.target.value)}
+                  disabled={isAuthenticated && isPasswordlessUser}
                 />
               </FieldRow>
               <FieldRow>
                 <InputField
                   required
+                  name="password"
                   type="password"
                   onChange={(e) => setPassword(e.target.value)}
                   value={password}
@@ -98,6 +151,7 @@ export function RegisterPage() {
                 <InputField
                   required
                   type="password"
+                  name="passwordConfirmation"
                   onChange={(e) => setPasswordConfirmation(e.target.value)}
                   value={passwordConfirmation}
                   placeholder="Confirm Password"
@@ -111,8 +165,8 @@ export function RegisterPage() {
                 </FieldRow>
               )}
               <FieldRow>
-                <Button type="submit" disabled={loading}>
-                  {loading ? "Registering..." : "Register"}
+                <Button type="submit" disabled={registering}>
+                  {registering ? "Registering..." : "Register"}
                 </Button>
               </FieldRow>
             </form>
diff --git a/src/Room.jsx b/src/Room.jsx
index 0d2ed56..dc9f5f2 100644
--- a/src/Room.jsx
+++ b/src/Room.jsx
@@ -60,8 +60,15 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
 export function Room() {
   const [registeringGuest, setRegisteringGuest] = useState(false);
   const [registrationError, setRegistrationError] = useState();
-  const { loading, isAuthenticated, error, client, registerGuest, isGuest } =
-    useClient();
+  const {
+    loading,
+    isAuthenticated,
+    error,
+    client,
+    registerGuest,
+    isGuest,
+    isPasswordlessUser,
+  } = useClient();
 
   useEffect(() => {
     if (!loading && !isAuthenticated) {
@@ -86,10 +93,16 @@ export function Room() {
     return <ErrorView error={registrationError || error} />;
   }
 
-  return <GroupCall client={client} isGuest={isGuest} />;
+  return (
+    <GroupCall
+      client={client}
+      isGuest={isGuest}
+      isPasswordlessUser={isPasswordlessUser}
+    />
+  );
 }
 
-export function GroupCall({ client, isGuest }) {
+export function GroupCall({ client, isGuest, isPasswordlessUser }) {
   const { roomId: maybeRoomId } = useParams();
   const { hash, search } = useLocation();
   const [simpleGrid, viaServers] = useMemo(() => {
@@ -118,6 +131,7 @@ export function GroupCall({ client, isGuest }) {
   return (
     <GroupCallView
       isGuest={isGuest}
+      isPasswordlessUser={isPasswordlessUser}
       client={client}
       roomId={roomId}
       groupCall={groupCall}
@@ -129,6 +143,7 @@ export function GroupCall({ client, isGuest }) {
 export function GroupCallView({
   client,
   isGuest,
+  isPasswordlessUser,
   roomId,
   groupCall,
   simpleGrid,
@@ -184,7 +199,7 @@ export function GroupCallView({
   const onLeave = useCallback(() => {
     leave();
 
-    if (!isGuest) {
+    if (!isGuest && !isPasswordlessUser) {
       history.push("/");
     } else {
       setLeft(true);
@@ -494,18 +509,12 @@ export function CallEndedScreen() {
       <ul>
         <li>Easily access all your previous call links</li>
         <li>Set a username and avatar</li>
-        <li>
-          Get your own Matrix ID so you can log in to{" "}
-          <a href="https://element.io" target="_blank">
-            Element
-          </a>
-        </li>
       </ul>
       <LinkButton
         className={styles.callEndedButton}
         size="lg"
         variant="default"
-        to="/login"
+        to="/register"
       >
         Create account
       </LinkButton>