mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	feat: URL navigation in auth (#1603)
* bump to react 18 and install react-router-dom * Upgrade to react 18 root * update vite * add cs api's * convert state/auth to ts * add client config context * add auto discovery context * add spec version context * add auth flow context * add background dot pattern css * add promise utils * init url based routing * update auth route server path as effect * add auth server hook * always use server from discovery info in context * login - WIP * upgrade jotai to v2 * add atom with localStorage util * add multi account sessions atom * add default IGNORE res to auto discovery * add error type in async callback hook * handle password login error * fix async callback hook * allow password login * Show custom server not allowed error in mxId login * add sso login component * add token login * fix hardcoded m.login.password in login func * update server input on url change * Improve sso login labels * update folds * fix async callback batching state update in safari * wrap async callback set state in queueMicrotask * wip * wip - register * arrange auth file structure * add error codes * extract filed error component form password login * add register util function * handle register flow - WIP * update unsupported auth flow method reasons * improve password input styles * Improve UIA flow next stage calculation complete stages can have any order so we will look for first stage which is not in completed * process register UIA flow stages * Extract register UIA stages component * improve register error messages * add focus trap & step count in UIA stages * add reset password path and path utils * add path with origin hook * fix sso redirect url * rename register token query param to token * restyle auth screen header * add reset password component - WIP * add reset password form * add netlify rewrites * fix netlify file indentation * test netlify redirect * fix vite to include netlify toml * add more netlify redirects * add splat to public and assets path * fix vite base name * add option to use hash router in config and remove appVersion * add splash screen component * add client config loading and error screen * fix server picker bug * fix reset password email input type * make auth page small screen responsive * fix typo in reset password screen
This commit is contained in:
		
							parent
							
								
									bb88eb7154
								
							
						
					
					
						commit
						20db27fa7e
					
				
					 103 changed files with 4772 additions and 543 deletions
				
			
		| 
						 | 
					@ -1,3 +0,0 @@
 | 
				
			||||||
# Redirects from what the browser requests to what we serve
 | 
					 | 
				
			||||||
/login       /
 | 
					 | 
				
			||||||
/register    /
 | 
					 | 
				
			||||||
							
								
								
									
										3
									
								
								build.config.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								build.config.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  base: '/',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -8,5 +8,10 @@
 | 
				
			||||||
    "mozilla.org",
 | 
					    "mozilla.org",
 | 
				
			||||||
    "xmr.se"
 | 
					    "xmr.se"
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "allowCustomHomeservers": true
 | 
					  "allowCustomHomeservers": true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "hashRouter": {
 | 
				
			||||||
 | 
					    "enabled": false,
 | 
				
			||||||
 | 
					    "basename": "/"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,6 +96,6 @@
 | 
				
			||||||
    <audio id="inviteSound">
 | 
					    <audio id="inviteSound">
 | 
				
			||||||
      <source src="./public/sound/invite.ogg" type="audio/ogg" />
 | 
					      <source src="./public/sound/invite.ogg" type="audio/ogg" />
 | 
				
			||||||
    </audio>
 | 
					    </audio>
 | 
				
			||||||
    <script type="module" src="./src/index.jsx"></script>
 | 
					    <script type="module" src="./src/index.tsx"></script>
 | 
				
			||||||
  </body>
 | 
					  </body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										34
									
								
								netlify.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								netlify.toml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					  from = "/config.json"
 | 
				
			||||||
 | 
					  to = "/config.json"
 | 
				
			||||||
 | 
					  status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					  from = "/manifest.json"
 | 
				
			||||||
 | 
					  to = "/manifest.json"
 | 
				
			||||||
 | 
					  status = 200
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					  from = "/olm.wasm"
 | 
				
			||||||
 | 
					  to = "/olm.wasm"
 | 
				
			||||||
 | 
					  status = 200
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					  from = "/pdf.worker.min.js"
 | 
				
			||||||
 | 
					  to = "/pdf.worker.min.js"
 | 
				
			||||||
 | 
					  status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					  from = "/public/*"
 | 
				
			||||||
 | 
					  to = "/public/:splat"
 | 
				
			||||||
 | 
					  status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					  from = "/assets/*"
 | 
				
			||||||
 | 
					  to = "/assets/:splat"
 | 
				
			||||||
 | 
					  status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					  from = "/*"
 | 
				
			||||||
 | 
					  to = "/index.html"
 | 
				
			||||||
 | 
					  status = 200
 | 
				
			||||||
							
								
								
									
										1018
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1018
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										22
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
					@ -39,13 +39,13 @@
 | 
				
			||||||
    "file-saver": "2.0.5",
 | 
					    "file-saver": "2.0.5",
 | 
				
			||||||
    "flux": "4.0.3",
 | 
					    "flux": "4.0.3",
 | 
				
			||||||
    "focus-trap-react": "10.0.2",
 | 
					    "focus-trap-react": "10.0.2",
 | 
				
			||||||
    "folds": "1.5.0",
 | 
					    "folds": "1.5.1",
 | 
				
			||||||
    "formik": "2.2.9",
 | 
					    "formik": "2.2.9",
 | 
				
			||||||
    "html-dom-parser": "4.0.0",
 | 
					    "html-dom-parser": "4.0.0",
 | 
				
			||||||
    "html-react-parser": "4.2.0",
 | 
					    "html-react-parser": "4.2.0",
 | 
				
			||||||
    "immer": "9.0.16",
 | 
					    "immer": "9.0.16",
 | 
				
			||||||
    "is-hotkey": "0.2.0",
 | 
					    "is-hotkey": "0.2.0",
 | 
				
			||||||
    "jotai": "1.12.0",
 | 
					    "jotai": "2.6.0",
 | 
				
			||||||
    "katex": "0.16.4",
 | 
					    "katex": "0.16.4",
 | 
				
			||||||
    "linkify-html": "4.0.2",
 | 
					    "linkify-html": "4.0.2",
 | 
				
			||||||
    "linkify-react": "4.1.1",
 | 
					    "linkify-react": "4.1.1",
 | 
				
			||||||
| 
						 | 
					@ -55,17 +55,18 @@
 | 
				
			||||||
    "pdfjs-dist": "3.10.111",
 | 
					    "pdfjs-dist": "3.10.111",
 | 
				
			||||||
    "prismjs": "1.29.0",
 | 
					    "prismjs": "1.29.0",
 | 
				
			||||||
    "prop-types": "15.8.1",
 | 
					    "prop-types": "15.8.1",
 | 
				
			||||||
    "react": "17.0.2",
 | 
					    "react": "18.2.0",
 | 
				
			||||||
    "react-aria": "3.29.1",
 | 
					    "react-aria": "3.29.1",
 | 
				
			||||||
    "react-autosize-textarea": "7.1.0",
 | 
					    "react-autosize-textarea": "7.1.0",
 | 
				
			||||||
    "react-blurhash": "0.2.0",
 | 
					    "react-blurhash": "0.2.0",
 | 
				
			||||||
    "react-dnd": "15.1.2",
 | 
					    "react-dnd": "16.0.1",
 | 
				
			||||||
    "react-dnd-html5-backend": "15.1.3",
 | 
					    "react-dnd-html5-backend": "16.0.1",
 | 
				
			||||||
    "react-dom": "17.0.2",
 | 
					    "react-dom": "18.2.0",
 | 
				
			||||||
    "react-error-boundary": "4.0.10",
 | 
					    "react-error-boundary": "4.0.10",
 | 
				
			||||||
    "react-google-recaptcha": "2.1.0",
 | 
					    "react-google-recaptcha": "2.1.0",
 | 
				
			||||||
    "react-modal": "3.16.1",
 | 
					    "react-modal": "3.16.1",
 | 
				
			||||||
    "react-range": "1.8.14",
 | 
					    "react-range": "1.8.14",
 | 
				
			||||||
 | 
					    "react-router-dom": "6.20.0",
 | 
				
			||||||
    "sanitize-html": "2.8.0",
 | 
					    "sanitize-html": "2.8.0",
 | 
				
			||||||
    "slate": "0.94.1",
 | 
					    "slate": "0.94.1",
 | 
				
			||||||
    "slate-history": "0.93.0",
 | 
					    "slate-history": "0.93.0",
 | 
				
			||||||
| 
						 | 
					@ -81,13 +82,14 @@
 | 
				
			||||||
    "@types/file-saver": "2.0.5",
 | 
					    "@types/file-saver": "2.0.5",
 | 
				
			||||||
    "@types/node": "18.11.18",
 | 
					    "@types/node": "18.11.18",
 | 
				
			||||||
    "@types/prismjs": "1.26.0",
 | 
					    "@types/prismjs": "1.26.0",
 | 
				
			||||||
    "@types/react": "18.0.26",
 | 
					    "@types/react": "18.2.39",
 | 
				
			||||||
    "@types/react-dom": "18.0.9",
 | 
					    "@types/react-dom": "18.2.17",
 | 
				
			||||||
 | 
					    "@types/react-google-recaptcha": "2.1.8",
 | 
				
			||||||
    "@types/sanitize-html": "2.9.0",
 | 
					    "@types/sanitize-html": "2.9.0",
 | 
				
			||||||
    "@types/ua-parser-js": "0.7.36",
 | 
					    "@types/ua-parser-js": "0.7.36",
 | 
				
			||||||
    "@typescript-eslint/eslint-plugin": "5.46.1",
 | 
					    "@typescript-eslint/eslint-plugin": "5.46.1",
 | 
				
			||||||
    "@typescript-eslint/parser": "5.46.1",
 | 
					    "@typescript-eslint/parser": "5.46.1",
 | 
				
			||||||
    "@vitejs/plugin-react": "3.0.0",
 | 
					    "@vitejs/plugin-react": "4.2.0",
 | 
				
			||||||
    "buffer": "6.0.3",
 | 
					    "buffer": "6.0.3",
 | 
				
			||||||
    "eslint": "8.29.0",
 | 
					    "eslint": "8.29.0",
 | 
				
			||||||
    "eslint-config-airbnb": "19.0.4",
 | 
					    "eslint-config-airbnb": "19.0.4",
 | 
				
			||||||
| 
						 | 
					@ -100,7 +102,7 @@
 | 
				
			||||||
    "prettier": "2.8.1",
 | 
					    "prettier": "2.8.1",
 | 
				
			||||||
    "sass": "1.56.2",
 | 
					    "sass": "1.56.2",
 | 
				
			||||||
    "typescript": "4.9.4",
 | 
					    "typescript": "4.9.4",
 | 
				
			||||||
    "vite": "4.3.9",
 | 
					    "vite": "5.0.8",
 | 
				
			||||||
    "vite-plugin-static-copy": "0.13.0"
 | 
					    "vite-plugin-static-copy": "0.13.0"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										64
									
								
								src/app/components/AuthFlowsLoader.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/app/components/AuthFlowsLoader.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					import { ReactNode, useCallback, useEffect, useMemo } from 'react';
 | 
				
			||||||
 | 
					import { MatrixError, createClient } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
 | 
				
			||||||
 | 
					import { promiseFulfilledResult, promiseRejectedResult } from '../utils/common';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AuthFlows,
 | 
				
			||||||
 | 
					  RegisterFlowStatus,
 | 
				
			||||||
 | 
					  RegisterFlowsResponse,
 | 
				
			||||||
 | 
					  parseRegisterErrResp,
 | 
				
			||||||
 | 
					} from '../hooks/useAuthFlows';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AuthFlowsLoaderProps = {
 | 
				
			||||||
 | 
					  fallback?: () => ReactNode;
 | 
				
			||||||
 | 
					  error?: (err: unknown) => ReactNode;
 | 
				
			||||||
 | 
					  children: (authFlows: AuthFlows) => ReactNode;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderProps) {
 | 
				
			||||||
 | 
					  const autoDiscoveryInfo = useAutoDiscoveryInfo();
 | 
				
			||||||
 | 
					  const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [state, load] = useAsyncCallback(
 | 
				
			||||||
 | 
					    useCallback(async () => {
 | 
				
			||||||
 | 
					      const result = await Promise.allSettled([mx.loginFlows(), mx.registerRequest({})]);
 | 
				
			||||||
 | 
					      const loginFlows = promiseFulfilledResult(result[0]);
 | 
				
			||||||
 | 
					      const registerResp = promiseRejectedResult(result[1]) as MatrixError | undefined;
 | 
				
			||||||
 | 
					      let registerFlows: RegisterFlowsResponse = { status: RegisterFlowStatus.InvalidRequest };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (typeof registerResp === 'object' && registerResp.httpStatus) {
 | 
				
			||||||
 | 
					        registerFlows = parseRegisterErrResp(registerResp);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!loginFlows) {
 | 
				
			||||||
 | 
					        throw new Error('Missing auth flow!');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if ('errcode' in loginFlows) {
 | 
				
			||||||
 | 
					        throw new Error('Failed to load auth flow!');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const authFlows: AuthFlows = {
 | 
				
			||||||
 | 
					        loginFlows,
 | 
				
			||||||
 | 
					        registerFlows,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return authFlows;
 | 
				
			||||||
 | 
					    }, [mx])
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    load();
 | 
				
			||||||
 | 
					  }, [load]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
 | 
				
			||||||
 | 
					    return fallback?.();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (state.status === AsyncStatus.Error) {
 | 
				
			||||||
 | 
					    return error?.(state.error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return children(state.data);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										38
									
								
								src/app/components/ClientConfigLoader.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/app/components/ClientConfigLoader.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,38 @@
 | 
				
			||||||
 | 
					import { ReactNode, useCallback, useEffect, useState } from 'react';
 | 
				
			||||||
 | 
					import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { ClientConfig } from '../hooks/useClientConfig';
 | 
				
			||||||
 | 
					import { trimTrailingSlash } from '../utils/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getClientConfig = async (): Promise<ClientConfig> => {
 | 
				
			||||||
 | 
					  const url = `${trimTrailingSlash(import.meta.env.BASE_URL)}/config.json`;
 | 
				
			||||||
 | 
					  const config = await fetch(url, { method: 'GET' });
 | 
				
			||||||
 | 
					  return config.json();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ClientConfigLoaderProps = {
 | 
				
			||||||
 | 
					  fallback?: () => ReactNode;
 | 
				
			||||||
 | 
					  error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode;
 | 
				
			||||||
 | 
					  children: (config: ClientConfig) => ReactNode;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function ClientConfigLoader({ fallback, error, children }: ClientConfigLoaderProps) {
 | 
				
			||||||
 | 
					  const [state, load] = useAsyncCallback(getClientConfig);
 | 
				
			||||||
 | 
					  const [ignoreError, setIgnoreError] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const ignoreCallback = useCallback(() => setIgnoreError(true), []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    load();
 | 
				
			||||||
 | 
					  }, [load]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
 | 
				
			||||||
 | 
					    return fallback?.();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!ignoreError && state.status === AsyncStatus.Error) {
 | 
				
			||||||
 | 
					    return error?.(state.error, load, ignoreCallback);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const config: ClientConfig = state.status === AsyncStatus.Success ? state.data : {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return children(config);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/app/components/ConfirmPasswordMatch.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/app/components/ConfirmPasswordMatch.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
 | 
				
			||||||
 | 
					import { useDebounce } from '../hooks/useDebounce';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ConfirmPasswordMatchProps = {
 | 
				
			||||||
 | 
					  initialValue: boolean;
 | 
				
			||||||
 | 
					  children: (
 | 
				
			||||||
 | 
					    match: boolean,
 | 
				
			||||||
 | 
					    doMatch: () => void,
 | 
				
			||||||
 | 
					    passRef: RefObject<HTMLInputElement>,
 | 
				
			||||||
 | 
					    confPassRef: RefObject<HTMLInputElement>
 | 
				
			||||||
 | 
					  ) => ReactNode;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function ConfirmPasswordMatch({ initialValue, children }: ConfirmPasswordMatchProps) {
 | 
				
			||||||
 | 
					  const [match, setMatch] = useState(initialValue);
 | 
				
			||||||
 | 
					  const passRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					  const confPassRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const doMatch = useDebounce(
 | 
				
			||||||
 | 
					    useCallback(() => {
 | 
				
			||||||
 | 
					      const pass = passRef.current?.value;
 | 
				
			||||||
 | 
					      const confPass = confPassRef.current?.value;
 | 
				
			||||||
 | 
					      if (!confPass) {
 | 
				
			||||||
 | 
					        setMatch(initialValue);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      setMatch(pass === confPass);
 | 
				
			||||||
 | 
					    }, [initialValue]),
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      wait: 500,
 | 
				
			||||||
 | 
					      immediate: false,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return children(match, doMatch, passRef, confPassRef);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/app/components/SpecVersionsLoader.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/app/components/SpecVersionsLoader.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					import { ReactNode, useCallback, useEffect } from 'react';
 | 
				
			||||||
 | 
					import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { SpecVersions, specVersions } from '../cs-api';
 | 
				
			||||||
 | 
					import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SpecVersionsLoaderProps = {
 | 
				
			||||||
 | 
					  fallback?: () => ReactNode;
 | 
				
			||||||
 | 
					  error?: (err: unknown) => ReactNode;
 | 
				
			||||||
 | 
					  children: (versions: SpecVersions) => ReactNode;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLoaderProps) {
 | 
				
			||||||
 | 
					  const autoDiscoveryInfo = useAutoDiscoveryInfo();
 | 
				
			||||||
 | 
					  const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [state, load] = useAsyncCallback(
 | 
				
			||||||
 | 
					    useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    load();
 | 
				
			||||||
 | 
					  }, [load]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
 | 
				
			||||||
 | 
					    return fallback?.();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (state.status === AsyncStatus.Error) {
 | 
				
			||||||
 | 
					    return error?.(state.error);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return children(state.data);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/app/components/SupportedUIAFlowsLoader.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/app/components/SupportedUIAFlowsLoader.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					import { ReactNode } from 'react';
 | 
				
			||||||
 | 
					import { UIAFlow } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { useSupportedUIAFlows } from '../hooks/useUIAFlows';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function SupportedUIAFlowsLoader({
 | 
				
			||||||
 | 
					  flows,
 | 
				
			||||||
 | 
					  supportedStages,
 | 
				
			||||||
 | 
					  children,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  supportedStages: string[];
 | 
				
			||||||
 | 
					  flows: UIAFlow[];
 | 
				
			||||||
 | 
					  children: (supportedFlows: UIAFlow[]) => ReactNode;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const supportedFlows = useSupportedUIAFlows(flows, supportedStages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return children(supportedFlows);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										72
									
								
								src/app/components/UIAFlowOverlay.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/app/components/UIAFlowOverlay.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,72 @@
 | 
				
			||||||
 | 
					import React, { ReactNode } from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Overlay,
 | 
				
			||||||
 | 
					  OverlayBackdrop,
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  config,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  TooltipProvider,
 | 
				
			||||||
 | 
					  Tooltip,
 | 
				
			||||||
 | 
					  Icons,
 | 
				
			||||||
 | 
					  Icon,
 | 
				
			||||||
 | 
					  Chip,
 | 
				
			||||||
 | 
					  IconButton,
 | 
				
			||||||
 | 
					} from 'folds';
 | 
				
			||||||
 | 
					import FocusTrap from 'focus-trap-react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type UIAFlowOverlayProps = {
 | 
				
			||||||
 | 
					  currentStep: number;
 | 
				
			||||||
 | 
					  stepCount: number;
 | 
				
			||||||
 | 
					  children: ReactNode;
 | 
				
			||||||
 | 
					  onCancel: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function UIAFlowOverlay({
 | 
				
			||||||
 | 
					  currentStep,
 | 
				
			||||||
 | 
					  stepCount,
 | 
				
			||||||
 | 
					  children,
 | 
				
			||||||
 | 
					  onCancel,
 | 
				
			||||||
 | 
					}: UIAFlowOverlayProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Overlay open backdrop={<OverlayBackdrop />}>
 | 
				
			||||||
 | 
					      <FocusTrap focusTrapOptions={{ initialFocus: false }}>
 | 
				
			||||||
 | 
					        <Box style={{ height: '100%' }} direction="Column" grow="Yes" gap="400">
 | 
				
			||||||
 | 
					          <Box grow="Yes" direction="Column" alignItems="Center" justifyContent="Center">
 | 
				
			||||||
 | 
					            {children}
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					          <Box
 | 
				
			||||||
 | 
					            style={{ padding: config.space.S200 }}
 | 
				
			||||||
 | 
					            shrink="No"
 | 
				
			||||||
 | 
					            justifyContent="Center"
 | 
				
			||||||
 | 
					            alignItems="Center"
 | 
				
			||||||
 | 
					            gap="200"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Chip as="div" radii="Pill" outlined>
 | 
				
			||||||
 | 
					              <Text as="span" size="T300">{`Step ${currentStep}/${stepCount}`}</Text>
 | 
				
			||||||
 | 
					            </Chip>
 | 
				
			||||||
 | 
					            <TooltipProvider
 | 
				
			||||||
 | 
					              tooltip={
 | 
				
			||||||
 | 
					                <Tooltip variant="Critical">
 | 
				
			||||||
 | 
					                  <Text>Exit</Text>
 | 
				
			||||||
 | 
					                </Tooltip>
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              position="Top"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {(anchorRef) => (
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  ref={anchorRef}
 | 
				
			||||||
 | 
					                  variant="Critical"
 | 
				
			||||||
 | 
					                  size="300"
 | 
				
			||||||
 | 
					                  onClick={onCancel}
 | 
				
			||||||
 | 
					                  radii="Pill"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <Icon size="50" src={Icons.Cross} />
 | 
				
			||||||
 | 
					                </IconButton>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </TooltipProvider>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					      </FocusTrap>
 | 
				
			||||||
 | 
					    </Overlay>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/app/components/password-input/PasswordInput.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/app/components/password-input/PasswordInput.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					import React, { ComponentProps, forwardRef } from 'react';
 | 
				
			||||||
 | 
					import { Icon, IconButton, Input, config, Icons } from 'folds';
 | 
				
			||||||
 | 
					import { UseStateProvider } from '../UseStateProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PasswordInputProps = Omit<ComponentProps<typeof Input>, 'type' | 'size'> & {
 | 
				
			||||||
 | 
					  size: '400' | '500';
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
 | 
				
			||||||
 | 
					  ({ variant, size, style, after, ...props }, ref) => {
 | 
				
			||||||
 | 
					    const paddingRight: string = size === '500' ? config.space.S300 : config.space.S200;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <UseStateProvider initial={false}>
 | 
				
			||||||
 | 
					        {(visible, setVisible) => (
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            style={{ paddingRight, ...style }}
 | 
				
			||||||
 | 
					            type={visible ? 'text' : 'password'}
 | 
				
			||||||
 | 
					            size={size}
 | 
				
			||||||
 | 
					            variant={variant}
 | 
				
			||||||
 | 
					            after={
 | 
				
			||||||
 | 
					              <>
 | 
				
			||||||
 | 
					                {after}
 | 
				
			||||||
 | 
					                <IconButton
 | 
				
			||||||
 | 
					                  onClick={() => setVisible(!visible)}
 | 
				
			||||||
 | 
					                  type="button"
 | 
				
			||||||
 | 
					                  variant={visible ? 'Warning' : variant}
 | 
				
			||||||
 | 
					                  size="300"
 | 
				
			||||||
 | 
					                  radii="300"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <Icon
 | 
				
			||||||
 | 
					                    style={{ opacity: config.opacity.P300 }}
 | 
				
			||||||
 | 
					                    size="100"
 | 
				
			||||||
 | 
					                    src={visible ? Icons.Eye : Icons.EyeBlind}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                </IconButton>
 | 
				
			||||||
 | 
					              </>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </UseStateProvider>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/app/components/splash-screen/SplashScreen.css.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/app/components/splash-screen/SplashScreen.css.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { style } from '@vanilla-extract/css';
 | 
				
			||||||
 | 
					import { color, config } from 'folds';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SplashScreen = style({
 | 
				
			||||||
 | 
					  minHeight: '100%',
 | 
				
			||||||
 | 
					  backgroundColor: color.Background.Container,
 | 
				
			||||||
 | 
					  color: color.Background.OnContainer,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SplashScreenFooter = style({
 | 
				
			||||||
 | 
					  padding: config.space.S400,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										29
									
								
								src/app/components/splash-screen/SplashScreen.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/app/components/splash-screen/SplashScreen.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					import { Box, Text } from 'folds';
 | 
				
			||||||
 | 
					import React, { ReactNode } from 'react';
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					import * as patternsCSS from '../../styles/Patterns.css';
 | 
				
			||||||
 | 
					import * as css from './SplashScreen.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SplashScreenProps = {
 | 
				
			||||||
 | 
					  children: ReactNode;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function SplashScreen({ children }: SplashScreenProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box
 | 
				
			||||||
 | 
					      className={classNames(css.SplashScreen, patternsCSS.BackgroundDotPattern)}
 | 
				
			||||||
 | 
					      direction="Column"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					      <Box
 | 
				
			||||||
 | 
					        className={css.SplashScreenFooter}
 | 
				
			||||||
 | 
					        shrink="No"
 | 
				
			||||||
 | 
					        alignItems="Center"
 | 
				
			||||||
 | 
					        justifyContent="Center"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Text size="H2" align="Center">
 | 
				
			||||||
 | 
					          Cinny
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/app/components/splash-screen/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/components/splash-screen/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export * from './SplashScreen';
 | 
				
			||||||
							
								
								
									
										65
									
								
								src/app/components/uia-stages/DummyStage.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/app/components/uia-stages/DummyStage.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,65 @@
 | 
				
			||||||
 | 
					import React, { useEffect, useCallback } from 'react';
 | 
				
			||||||
 | 
					import { Dialog, Text, Box, Button, config } from 'folds';
 | 
				
			||||||
 | 
					import { AuthType } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { StageComponentProps } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function DummyErrorDialog({
 | 
				
			||||||
 | 
					  title,
 | 
				
			||||||
 | 
					  message,
 | 
				
			||||||
 | 
					  onRetry,
 | 
				
			||||||
 | 
					  onCancel,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					  onRetry: () => void;
 | 
				
			||||||
 | 
					  onCancel: () => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog>
 | 
				
			||||||
 | 
					      <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
 | 
				
			||||||
 | 
					        <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					          <Text size="H4">{title}</Text>
 | 
				
			||||||
 | 
					          <Text>{message}</Text>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        <Button variant="Critical" onClick={onRetry}>
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Retry
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <Button variant="Critical" fill="None" outlined onClick={onCancel}>
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Cancel
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function AutoDummyStageDialog({ stageData, submitAuthDict, onCancel }: StageComponentProps) {
 | 
				
			||||||
 | 
					  const { errorCode, error, session } = stageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = useCallback(() => {
 | 
				
			||||||
 | 
					    submitAuthDict({
 | 
				
			||||||
 | 
					      type: AuthType.Dummy,
 | 
				
			||||||
 | 
					      session,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, [session, submitAuthDict]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!errorCode) handleSubmit();
 | 
				
			||||||
 | 
					  }, [handleSubmit, errorCode]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (errorCode) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <DummyErrorDialog
 | 
				
			||||||
 | 
					        title={errorCode}
 | 
				
			||||||
 | 
					        message={error ?? 'Failed to register.'}
 | 
				
			||||||
 | 
					        onRetry={handleSubmit}
 | 
				
			||||||
 | 
					        onCancel={onCancel}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										172
									
								
								src/app/components/uia-stages/EmailStage.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/app/components/uia-stages/EmailStage.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,172 @@
 | 
				
			||||||
 | 
					import React, { useEffect, useCallback, FormEventHandler } from 'react';
 | 
				
			||||||
 | 
					import { Dialog, Text, Box, Button, config, Input, color, Spinner } from 'folds';
 | 
				
			||||||
 | 
					import { AuthType, MatrixError } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { StageComponentProps } from './types';
 | 
				
			||||||
 | 
					import { AsyncState, AsyncStatus } from '../../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../hooks/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function EmailErrorDialog({
 | 
				
			||||||
 | 
					  title,
 | 
				
			||||||
 | 
					  message,
 | 
				
			||||||
 | 
					  defaultEmail,
 | 
				
			||||||
 | 
					  onRetry,
 | 
				
			||||||
 | 
					  onCancel,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					  defaultEmail?: string;
 | 
				
			||||||
 | 
					  onRetry: (email: string) => void;
 | 
				
			||||||
 | 
					  onCancel: () => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const handleFormSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
 | 
				
			||||||
 | 
					    evt.preventDefault();
 | 
				
			||||||
 | 
					    const { retryEmailInput } = evt.target as HTMLFormElement & {
 | 
				
			||||||
 | 
					      retryEmailInput: HTMLInputElement;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const t = retryEmailInput.value;
 | 
				
			||||||
 | 
					    onRetry(t);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog>
 | 
				
			||||||
 | 
					      <Box
 | 
				
			||||||
 | 
					        as="form"
 | 
				
			||||||
 | 
					        onSubmit={handleFormSubmit}
 | 
				
			||||||
 | 
					        style={{ padding: config.space.S400 }}
 | 
				
			||||||
 | 
					        direction="Column"
 | 
				
			||||||
 | 
					        gap="400"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					          <Text size="H4">{title}</Text>
 | 
				
			||||||
 | 
					          <Text>{message}</Text>
 | 
				
			||||||
 | 
					          <Text as="label" size="L400" style={{ paddingTop: config.space.S400 }}>
 | 
				
			||||||
 | 
					            Email
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            name="retryEmailInput"
 | 
				
			||||||
 | 
					            variant="Background"
 | 
				
			||||||
 | 
					            size="500"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            defaultValue={defaultEmail}
 | 
				
			||||||
 | 
					            required
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        <Button variant="Primary" type="submit">
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Send Verification Email
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <Button variant="Critical" fill="None" outlined type="button" onClick={onCancel}>
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Cancel
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function EmailStageDialog({
 | 
				
			||||||
 | 
					  email,
 | 
				
			||||||
 | 
					  clientSecret,
 | 
				
			||||||
 | 
					  stageData,
 | 
				
			||||||
 | 
					  emailTokenState,
 | 
				
			||||||
 | 
					  requestEmailToken,
 | 
				
			||||||
 | 
					  submitAuthDict,
 | 
				
			||||||
 | 
					  onCancel,
 | 
				
			||||||
 | 
					}: StageComponentProps & {
 | 
				
			||||||
 | 
					  email?: string;
 | 
				
			||||||
 | 
					  clientSecret: string;
 | 
				
			||||||
 | 
					  emailTokenState: AsyncState<RequestEmailTokenResponse, MatrixError>;
 | 
				
			||||||
 | 
					  requestEmailToken: RequestEmailTokenCallback;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const { errorCode, error, session } = stageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = useCallback(
 | 
				
			||||||
 | 
					    (sessionId: string) => {
 | 
				
			||||||
 | 
					      const threepIDCreds = {
 | 
				
			||||||
 | 
					        sid: sessionId,
 | 
				
			||||||
 | 
					        client_secret: clientSecret,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      submitAuthDict({
 | 
				
			||||||
 | 
					        type: AuthType.Email,
 | 
				
			||||||
 | 
					        threepid_creds: threepIDCreds,
 | 
				
			||||||
 | 
					        threepidCreds: threepIDCreds,
 | 
				
			||||||
 | 
					        session,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [submitAuthDict, session, clientSecret]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleEmailSubmit = useCallback(
 | 
				
			||||||
 | 
					    (userEmail: string) => {
 | 
				
			||||||
 | 
					      requestEmailToken(userEmail, clientSecret);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [clientSecret, requestEmailToken]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (email && !errorCode && emailTokenState.status === AsyncStatus.Idle) {
 | 
				
			||||||
 | 
					      requestEmailToken(email, clientSecret);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [email, errorCode, clientSecret, emailTokenState, requestEmailToken]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (emailTokenState.status === AsyncStatus.Loading) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Box direction="Column" alignItems="Center" gap="400">
 | 
				
			||||||
 | 
					        <Spinner variant="Secondary" size="600" />
 | 
				
			||||||
 | 
					        <Text style={{ color: color.Secondary.Main }}>Sending verification email...</Text>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (emailTokenState.status === AsyncStatus.Error) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <EmailErrorDialog
 | 
				
			||||||
 | 
					        title={emailTokenState.error.errcode ?? 'Verify Email'}
 | 
				
			||||||
 | 
					        message={
 | 
				
			||||||
 | 
					          emailTokenState.error?.data?.error ??
 | 
				
			||||||
 | 
					          emailTokenState.error.message ??
 | 
				
			||||||
 | 
					          'Failed to send verification Email request.'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        onRetry={handleEmailSubmit}
 | 
				
			||||||
 | 
					        onCancel={onCancel}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (emailTokenState.status === AsyncStatus.Success) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Dialog>
 | 
				
			||||||
 | 
					        <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
 | 
				
			||||||
 | 
					          <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					            <Text size="H4">Verification Request Sent</Text>
 | 
				
			||||||
 | 
					            <Text>{`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}</Text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {errorCode && (
 | 
				
			||||||
 | 
					              <Text style={{ color: color.Critical.Main }}>{`${errorCode}: ${error}`}</Text>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					          <Button variant="Primary" onClick={() => handleSubmit(emailTokenState.data.result.sid)}>
 | 
				
			||||||
 | 
					            <Text as="span" size="B400">
 | 
				
			||||||
 | 
					              Continue
 | 
				
			||||||
 | 
					            </Text>
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					      </Dialog>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!email) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <EmailErrorDialog
 | 
				
			||||||
 | 
					        title="Provide Email"
 | 
				
			||||||
 | 
					        message="Please provide email to send verification request."
 | 
				
			||||||
 | 
					        onRetry={handleEmailSubmit}
 | 
				
			||||||
 | 
					        onCancel={onCancel}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										64
									
								
								src/app/components/uia-stages/ReCaptchaStage.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/app/components/uia-stages/ReCaptchaStage.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Dialog, Text, Box, Button, config } from 'folds';
 | 
				
			||||||
 | 
					import { AuthType } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import ReCAPTCHA from 'react-google-recaptcha';
 | 
				
			||||||
 | 
					import { StageComponentProps } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function ReCaptchaErrorDialog({
 | 
				
			||||||
 | 
					  title,
 | 
				
			||||||
 | 
					  message,
 | 
				
			||||||
 | 
					  onCancel,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					  onCancel: () => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog>
 | 
				
			||||||
 | 
					      <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
 | 
				
			||||||
 | 
					        <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					          <Text size="H4">{title}</Text>
 | 
				
			||||||
 | 
					          <Text>{message}</Text>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        <Button variant="Critical" fill="None" outlined onClick={onCancel}>
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Cancel
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ReCaptchaStageDialog({ stageData, submitAuthDict, onCancel }: StageComponentProps) {
 | 
				
			||||||
 | 
					  const { info, session } = stageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const publicKey = info?.public_key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleChange = (token: string | null) => {
 | 
				
			||||||
 | 
					    submitAuthDict({
 | 
				
			||||||
 | 
					      type: AuthType.Recaptcha,
 | 
				
			||||||
 | 
					      response: token,
 | 
				
			||||||
 | 
					      session,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (typeof publicKey !== 'string' || !session) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <ReCaptchaErrorDialog
 | 
				
			||||||
 | 
					        title="Invalid Data"
 | 
				
			||||||
 | 
					        message="No valid data found to proceed with ReCAPTCHA."
 | 
				
			||||||
 | 
					        onCancel={onCancel}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog>
 | 
				
			||||||
 | 
					      <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
 | 
				
			||||||
 | 
					        <Text>Please check the box below to proceed.</Text>
 | 
				
			||||||
 | 
					        <ReCAPTCHA sitekey={publicKey} onChange={handleChange} />
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										117
									
								
								src/app/components/uia-stages/RegistrationTokenStage.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/app/components/uia-stages/RegistrationTokenStage.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,117 @@
 | 
				
			||||||
 | 
					import React, { useEffect, useCallback, FormEventHandler } from 'react';
 | 
				
			||||||
 | 
					import { Dialog, Text, Box, Button, config, Input } from 'folds';
 | 
				
			||||||
 | 
					import { AuthType } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { StageComponentProps } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function RegistrationTokenErrorDialog({
 | 
				
			||||||
 | 
					  title,
 | 
				
			||||||
 | 
					  message,
 | 
				
			||||||
 | 
					  defaultToken,
 | 
				
			||||||
 | 
					  onRetry,
 | 
				
			||||||
 | 
					  onCancel,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					  defaultToken?: string;
 | 
				
			||||||
 | 
					  onRetry: (token: string) => void;
 | 
				
			||||||
 | 
					  onCancel: () => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const handleFormSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
 | 
				
			||||||
 | 
					    evt.preventDefault();
 | 
				
			||||||
 | 
					    const { retryTokenInput } = evt.target as HTMLFormElement & {
 | 
				
			||||||
 | 
					      retryTokenInput: HTMLInputElement;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const t = retryTokenInput.value;
 | 
				
			||||||
 | 
					    onRetry(t);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog>
 | 
				
			||||||
 | 
					      <Box
 | 
				
			||||||
 | 
					        as="form"
 | 
				
			||||||
 | 
					        onSubmit={handleFormSubmit}
 | 
				
			||||||
 | 
					        style={{ padding: config.space.S400 }}
 | 
				
			||||||
 | 
					        direction="Column"
 | 
				
			||||||
 | 
					        gap="400"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					          <Text size="H4">{title}</Text>
 | 
				
			||||||
 | 
					          <Text>{message}</Text>
 | 
				
			||||||
 | 
					          <Text as="label" size="L400" style={{ paddingTop: config.space.S400 }}>
 | 
				
			||||||
 | 
					            Registration Token
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            name="retryTokenInput"
 | 
				
			||||||
 | 
					            variant="Background"
 | 
				
			||||||
 | 
					            size="500"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            defaultValue={defaultToken}
 | 
				
			||||||
 | 
					            required
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        <Button variant="Critical" type="submit">
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Retry
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <Button variant="Critical" fill="None" outlined type="button" onClick={onCancel}>
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Cancel
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function RegistrationTokenStageDialog({
 | 
				
			||||||
 | 
					  token,
 | 
				
			||||||
 | 
					  stageData,
 | 
				
			||||||
 | 
					  submitAuthDict,
 | 
				
			||||||
 | 
					  onCancel,
 | 
				
			||||||
 | 
					}: StageComponentProps & {
 | 
				
			||||||
 | 
					  token?: string;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const { errorCode, error, session } = stageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = useCallback(
 | 
				
			||||||
 | 
					    (t: string) => {
 | 
				
			||||||
 | 
					      submitAuthDict({
 | 
				
			||||||
 | 
					        type: AuthType.RegistrationToken,
 | 
				
			||||||
 | 
					        token: t,
 | 
				
			||||||
 | 
					        session,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [session, submitAuthDict]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (token && !errorCode) handleSubmit(token);
 | 
				
			||||||
 | 
					  }, [handleSubmit, token, errorCode]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (errorCode) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <RegistrationTokenErrorDialog
 | 
				
			||||||
 | 
					        defaultToken={token}
 | 
				
			||||||
 | 
					        title={errorCode}
 | 
				
			||||||
 | 
					        message={error ?? 'Invalid registration token provided.'}
 | 
				
			||||||
 | 
					        onRetry={handleSubmit}
 | 
				
			||||||
 | 
					        onCancel={onCancel}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!token) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <RegistrationTokenErrorDialog
 | 
				
			||||||
 | 
					        defaultToken={token}
 | 
				
			||||||
 | 
					        title="Registration Token"
 | 
				
			||||||
 | 
					        message="Please submit registration token provided by you homeserver admin."
 | 
				
			||||||
 | 
					        onRetry={handleSubmit}
 | 
				
			||||||
 | 
					        onCancel={onCancel}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										69
									
								
								src/app/components/uia-stages/TermsStage.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/app/components/uia-stages/TermsStage.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,69 @@
 | 
				
			||||||
 | 
					import React, { useEffect, useCallback } from 'react';
 | 
				
			||||||
 | 
					import { Dialog, Text, Box, Button, config } from 'folds';
 | 
				
			||||||
 | 
					import { AuthType } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { StageComponentProps } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function TermsErrorDialog({
 | 
				
			||||||
 | 
					  title,
 | 
				
			||||||
 | 
					  message,
 | 
				
			||||||
 | 
					  onRetry,
 | 
				
			||||||
 | 
					  onCancel,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  title: string;
 | 
				
			||||||
 | 
					  message: string;
 | 
				
			||||||
 | 
					  onRetry: () => void;
 | 
				
			||||||
 | 
					  onCancel: () => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Dialog>
 | 
				
			||||||
 | 
					      <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
 | 
				
			||||||
 | 
					        <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					          <Text size="H4">{title}</Text>
 | 
				
			||||||
 | 
					          <Text>{message}</Text>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        <Button variant="Critical" onClick={onRetry}>
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Retry
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <Button variant="Critical" fill="None" outlined onClick={onCancel}>
 | 
				
			||||||
 | 
					          <Text as="span" size="B400">
 | 
				
			||||||
 | 
					            Cancel
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Dialog>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function AutoTermsStageDialog({ stageData, submitAuthDict, onCancel }: StageComponentProps) {
 | 
				
			||||||
 | 
					  const { errorCode, error, session } = stageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit = useCallback(
 | 
				
			||||||
 | 
					    () =>
 | 
				
			||||||
 | 
					      submitAuthDict({
 | 
				
			||||||
 | 
					        type: AuthType.Terms,
 | 
				
			||||||
 | 
					        session,
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    [session, submitAuthDict]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!errorCode) {
 | 
				
			||||||
 | 
					      handleSubmit();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [session, errorCode, handleSubmit]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (errorCode) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <TermsErrorDialog
 | 
				
			||||||
 | 
					        title={errorCode}
 | 
				
			||||||
 | 
					        message={error ?? 'Failed to submit Terms and Condition Acceptance.'}
 | 
				
			||||||
 | 
					        onRetry={handleSubmit}
 | 
				
			||||||
 | 
					        onCancel={onCancel}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/app/components/uia-stages/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/app/components/uia-stages/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					export * from './types';
 | 
				
			||||||
 | 
					export * from './DummyStage';
 | 
				
			||||||
 | 
					export * from './EmailStage';
 | 
				
			||||||
 | 
					export * from './ReCaptchaStage';
 | 
				
			||||||
 | 
					export * from './RegistrationTokenStage';
 | 
				
			||||||
 | 
					export * from './TermsStage';
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/app/components/uia-stages/types.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/app/components/uia-stages/types.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					import { AuthDict } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { AuthStageData } from '../../hooks/useUIAFlows';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type StageComponentProps = {
 | 
				
			||||||
 | 
					  stageData: AuthStageData;
 | 
				
			||||||
 | 
					  submitAuthDict: (authDict: AuthDict) => void;
 | 
				
			||||||
 | 
					  onCancel: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										115
									
								
								src/app/cs-api.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/app/cs-api.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,115 @@
 | 
				
			||||||
 | 
					import to from 'await-to-js';
 | 
				
			||||||
 | 
					import { trimTrailingSlash } from './utils/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum AutoDiscoveryAction {
 | 
				
			||||||
 | 
					  PROMPT = 'PROMPT',
 | 
				
			||||||
 | 
					  IGNORE = 'IGNORE',
 | 
				
			||||||
 | 
					  FAIL_PROMPT = 'FAIL_PROMPT',
 | 
				
			||||||
 | 
					  FAIL_ERROR = 'FAIL_ERROR',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AutoDiscoveryError = {
 | 
				
			||||||
 | 
					  host: string;
 | 
				
			||||||
 | 
					  action: AutoDiscoveryAction;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AutoDiscoveryInfo = Record<string, unknown> & {
 | 
				
			||||||
 | 
					  'm.homeserver': {
 | 
				
			||||||
 | 
					    base_url: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  'm.identity_server'?: {
 | 
				
			||||||
 | 
					    base_url: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const autoDiscovery = async (
 | 
				
			||||||
 | 
					  request: typeof fetch,
 | 
				
			||||||
 | 
					  server: string
 | 
				
			||||||
 | 
					): Promise<[AutoDiscoveryError, undefined] | [undefined, AutoDiscoveryInfo]> => {
 | 
				
			||||||
 | 
					  const host = /^https?:\/\//.test(server) ? trimTrailingSlash(server) : `https://${server}`;
 | 
				
			||||||
 | 
					  const autoDiscoveryUrl = `${host}/.well-known/matrix/client`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [err, response] = await to(request(autoDiscoveryUrl, { method: 'GET' }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (err || response.status === 404) {
 | 
				
			||||||
 | 
					    // AutoDiscoveryAction.IGNORE
 | 
				
			||||||
 | 
					    // We will use default value for IGNORE action
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      undefined,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        'm.homeserver': {
 | 
				
			||||||
 | 
					          base_url: host,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (response.status !== 200) {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        host,
 | 
				
			||||||
 | 
					        action: AutoDiscoveryAction.FAIL_PROMPT,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      undefined,
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [contentErr, content] = await to<AutoDiscoveryInfo>(response.json());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (contentErr || typeof content !== 'object') {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        host,
 | 
				
			||||||
 | 
					        action: AutoDiscoveryAction.FAIL_PROMPT,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      undefined,
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const baseUrl = content['m.homeserver']?.base_url;
 | 
				
			||||||
 | 
					  if (typeof baseUrl !== 'string') {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        host,
 | 
				
			||||||
 | 
					        action: AutoDiscoveryAction.FAIL_PROMPT,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      undefined,
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (/^https?:\/\//.test(baseUrl) === false) {
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        host,
 | 
				
			||||||
 | 
					        action: AutoDiscoveryAction.FAIL_ERROR,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      undefined,
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  content['m.homeserver'].base_url = trimTrailingSlash(baseUrl);
 | 
				
			||||||
 | 
					  if (content['m.identity_server']) {
 | 
				
			||||||
 | 
					    content['m.identity_server'].base_url = trimTrailingSlash(
 | 
				
			||||||
 | 
					      content['m.identity_server'].base_url
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [undefined, content];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SpecVersions = {
 | 
				
			||||||
 | 
					  versions: string[];
 | 
				
			||||||
 | 
					  unstable_features?: Record<string, boolean>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const specVersions = async (
 | 
				
			||||||
 | 
					  request: typeof fetch,
 | 
				
			||||||
 | 
					  baseUrl: string
 | 
				
			||||||
 | 
					): Promise<SpecVersions> => {
 | 
				
			||||||
 | 
					  const res = await request(`${baseUrl}/_matrix/client/versions`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const data = (await res.json()) as unknown;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (data && typeof data === 'object' && 'versions' in data && Array.isArray(data.versions)) {
 | 
				
			||||||
 | 
					    return data as SpecVersions;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  throw new Error('Homeserver URL does not appear to be a valid Matrix homeserver');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/app/cs-errorcode.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/app/cs-errorcode.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,37 @@
 | 
				
			||||||
 | 
					export enum ErrorCode {
 | 
				
			||||||
 | 
					  M_FORBIDDEN = 'M_FORBIDDEN',
 | 
				
			||||||
 | 
					  M_UNKNOWN_TOKEN = 'M_UNKNOWN_TOKEN',
 | 
				
			||||||
 | 
					  M_MISSING_TOKEN = 'M_MISSING_TOKEN',
 | 
				
			||||||
 | 
					  M_BAD_JSON = 'M_BAD_JSON',
 | 
				
			||||||
 | 
					  M_NOT_JSON = 'M_NOT_JSON',
 | 
				
			||||||
 | 
					  M_NOT_FOUND = 'M_NOT_FOUND',
 | 
				
			||||||
 | 
					  M_LIMIT_EXCEEDED = 'M_LIMIT_EXCEEDED',
 | 
				
			||||||
 | 
					  M_UNRECOGNIZED = 'M_UNRECOGNIZED',
 | 
				
			||||||
 | 
					  M_UNKNOWN = 'M_UNKNOWN',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  M_UNAUTHORIZED = 'M_UNAUTHORIZED',
 | 
				
			||||||
 | 
					  M_USER_DEACTIVATED = 'M_USER_DEACTIVATED',
 | 
				
			||||||
 | 
					  M_USER_IN_USE = 'M_USER_IN_USE',
 | 
				
			||||||
 | 
					  M_INVALID_USERNAME = 'M_INVALID_USERNAME',
 | 
				
			||||||
 | 
					  M_WEAK_PASSWORD = 'M_WEAK_PASSWORD',
 | 
				
			||||||
 | 
					  M_PASSWORD_TOO_SHORT = 'M_PASSWORD_TOO_SHORT',
 | 
				
			||||||
 | 
					  M_ROOM_IN_USE = 'M_ROOM_IN_USE',
 | 
				
			||||||
 | 
					  M_INVALID_ROOM_STATE = 'M_INVALID_ROOM_STATE',
 | 
				
			||||||
 | 
					  M_THREEPID_IN_USE = 'M_THREEPID_IN_USE',
 | 
				
			||||||
 | 
					  M_THREEPID_NOT_FOUND = 'M_THREEPID_NOT_FOUND',
 | 
				
			||||||
 | 
					  M_THREEPID_AUTH_FAILED = 'M_THREEPID_AUTH_FAILED',
 | 
				
			||||||
 | 
					  M_THREEPID_DENIED = 'M_THREEPID_DENIED',
 | 
				
			||||||
 | 
					  M_SERVER_NOT_TRUSTED = 'M_SERVER_NOT_TRUSTED',
 | 
				
			||||||
 | 
					  M_UNSUPPORTED_ROOM_VERSION = 'M_UNSUPPORTED_ROOM_VERSION',
 | 
				
			||||||
 | 
					  M_INCOMPATIBLE_ROOM_VERSION = 'M_INCOMPATIBLE_ROOM_VERSION',
 | 
				
			||||||
 | 
					  M_BAD_STATE = 'M_BAD_STATE',
 | 
				
			||||||
 | 
					  M_GUEST_ACCESS_FORBIDDEN = 'M_GUEST_ACCESS_FORBIDDEN',
 | 
				
			||||||
 | 
					  M_CAPTCHA_NEEDED = 'M_CAPTCHA_NEEDED',
 | 
				
			||||||
 | 
					  M_CAPTCHA_INVALID = 'M_CAPTCHA_INVALID',
 | 
				
			||||||
 | 
					  M_MISSING_PARAM = 'M_MISSING_PARAM',
 | 
				
			||||||
 | 
					  M_INVALID_PARAM = 'M_INVALID_PARAM',
 | 
				
			||||||
 | 
					  M_TOO_LARGE = 'M_TOO_LARGE',
 | 
				
			||||||
 | 
					  M_EXCLUSIVE = 'M_EXCLUSIVE',
 | 
				
			||||||
 | 
					  M_RESOURCE_LIMIT_EXCEEDED = 'M_RESOURCE_LIMIT_EXCEEDED',
 | 
				
			||||||
 | 
					  M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/app/hooks/types.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/app/hooks/types.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { IRequestTokenResponse } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type RequestEmailTokenResponse = {
 | 
				
			||||||
 | 
					  email: string;
 | 
				
			||||||
 | 
					  clientSecret: string;
 | 
				
			||||||
 | 
					  result: IRequestTokenResponse;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export type RequestEmailTokenCallback = (
 | 
				
			||||||
 | 
					  email: string,
 | 
				
			||||||
 | 
					  clientSecret: string,
 | 
				
			||||||
 | 
					  nextLink?: string
 | 
				
			||||||
 | 
					) => Promise<RequestEmailTokenResponse>;
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import { useCallback, useState } from 'react';
 | 
					import { useCallback, useRef, useState } from 'react';
 | 
				
			||||||
 | 
					import { flushSync } from 'react-dom';
 | 
				
			||||||
import { useAlive } from './useAlive';
 | 
					import { useAlive } from './useAlive';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum AsyncStatus {
 | 
					export enum AsyncStatus {
 | 
				
			||||||
| 
						 | 
					@ -16,36 +17,56 @@ export type AsyncLoading = {
 | 
				
			||||||
  status: AsyncStatus.Loading;
 | 
					  status: AsyncStatus.Loading;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AsyncSuccess<T> = {
 | 
					export type AsyncSuccess<D> = {
 | 
				
			||||||
  status: AsyncStatus.Success;
 | 
					  status: AsyncStatus.Success;
 | 
				
			||||||
  data: T;
 | 
					  data: D;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AsyncError = {
 | 
					export type AsyncError<E = unknown> = {
 | 
				
			||||||
  status: AsyncStatus.Error;
 | 
					  status: AsyncStatus.Error;
 | 
				
			||||||
  error: unknown;
 | 
					  error: E;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AsyncState<T> = AsyncIdle | AsyncLoading | AsyncSuccess<T> | AsyncError;
 | 
					export type AsyncState<D, E = unknown> = AsyncIdle | AsyncLoading | AsyncSuccess<D> | AsyncError<E>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type AsyncCallback<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData>;
 | 
					export type AsyncCallback<TArgs extends unknown[], TData> = (...args: TArgs) => Promise<TData>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useAsyncCallback = <TArgs extends unknown[], TData>(
 | 
					export const useAsyncCallback = <TData, TError, TArgs extends unknown[]>(
 | 
				
			||||||
  asyncCallback: AsyncCallback<TArgs, TData>
 | 
					  asyncCallback: AsyncCallback<TArgs, TData>
 | 
				
			||||||
): [AsyncState<TData>, AsyncCallback<TArgs, TData>] => {
 | 
					): [AsyncState<TData, TError>, AsyncCallback<TArgs, TData>] => {
 | 
				
			||||||
  const [state, setState] = useState<AsyncState<TData>>({
 | 
					  const [state, setState] = useState<AsyncState<TData, TError>>({
 | 
				
			||||||
    status: AsyncStatus.Idle,
 | 
					    status: AsyncStatus.Idle,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const alive = useAlive();
 | 
					  const alive = useAlive();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Tracks the request number.
 | 
				
			||||||
 | 
					  // If two or more requests are made subsequently
 | 
				
			||||||
 | 
					  // we will throw all old request's response after they resolved.
 | 
				
			||||||
 | 
					  const reqNumberRef = useRef(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const callback: AsyncCallback<TArgs, TData> = useCallback(
 | 
					  const callback: AsyncCallback<TArgs, TData> = useCallback(
 | 
				
			||||||
    async (...args) => {
 | 
					    async (...args) => {
 | 
				
			||||||
      setState({
 | 
					      queueMicrotask(() => {
 | 
				
			||||||
        status: AsyncStatus.Loading,
 | 
					        // Warning: flushSync was called from inside a lifecycle method.
 | 
				
			||||||
 | 
					        // React cannot flush when React is already rendering.
 | 
				
			||||||
 | 
					        // Consider moving this call to a scheduler task or micro task.
 | 
				
			||||||
 | 
					        flushSync(() => {
 | 
				
			||||||
 | 
					          // flushSync because
 | 
				
			||||||
 | 
					          // https://github.com/facebook/react/issues/26713#issuecomment-1872085134
 | 
				
			||||||
 | 
					          setState({
 | 
				
			||||||
 | 
					            status: AsyncStatus.Loading,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      reqNumberRef.current += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const currentReqNumber = reqNumberRef.current;
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const data = await asyncCallback(...args);
 | 
					        const data = await asyncCallback(...args);
 | 
				
			||||||
 | 
					        if (currentReqNumber !== reqNumberRef.current) {
 | 
				
			||||||
 | 
					          throw new Error('AsyncCallbackHook: Request replaced!');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (alive()) {
 | 
					        if (alive()) {
 | 
				
			||||||
          setState({
 | 
					          setState({
 | 
				
			||||||
            status: AsyncStatus.Success,
 | 
					            status: AsyncStatus.Success,
 | 
				
			||||||
| 
						 | 
					@ -54,10 +75,13 @@ export const useAsyncCallback = <TArgs extends unknown[], TData>(
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return data;
 | 
					        return data;
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        if (currentReqNumber !== reqNumberRef.current) {
 | 
				
			||||||
 | 
					          throw new Error('AsyncCallbackHook: Request replaced!');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (alive()) {
 | 
					        if (alive()) {
 | 
				
			||||||
          setState({
 | 
					          setState({
 | 
				
			||||||
            status: AsyncStatus.Error,
 | 
					            status: AsyncStatus.Error,
 | 
				
			||||||
            error: e,
 | 
					            error: e as TError,
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        throw e;
 | 
					        throw e;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										59
									
								
								src/app/hooks/useAuthFlows.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/app/hooks/useAuthFlows.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,59 @@
 | 
				
			||||||
 | 
					import { createContext, useContext } from 'react';
 | 
				
			||||||
 | 
					import { IAuthData, MatrixError } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { ILoginFlowsResponse } from 'matrix-js-sdk/lib/@types/auth';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum RegisterFlowStatus {
 | 
				
			||||||
 | 
					  FlowRequired = 401,
 | 
				
			||||||
 | 
					  InvalidRequest = 400,
 | 
				
			||||||
 | 
					  RegistrationDisabled = 403,
 | 
				
			||||||
 | 
					  RateLimited = 429,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type RegisterFlowsResponse =
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      status: RegisterFlowStatus.FlowRequired;
 | 
				
			||||||
 | 
					      data: IAuthData;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      status: Exclude<RegisterFlowStatus, RegisterFlowStatus.FlowRequired>;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const parseRegisterErrResp = (matrixError: MatrixError): RegisterFlowsResponse => {
 | 
				
			||||||
 | 
					  switch (matrixError.httpStatus) {
 | 
				
			||||||
 | 
					    case RegisterFlowStatus.InvalidRequest: {
 | 
				
			||||||
 | 
					      return { status: RegisterFlowStatus.InvalidRequest };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case RegisterFlowStatus.RateLimited: {
 | 
				
			||||||
 | 
					      return { status: RegisterFlowStatus.RateLimited };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case RegisterFlowStatus.RegistrationDisabled: {
 | 
				
			||||||
 | 
					      return { status: RegisterFlowStatus.RegistrationDisabled };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case RegisterFlowStatus.FlowRequired: {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        status: RegisterFlowStatus.FlowRequired,
 | 
				
			||||||
 | 
					        data: matrixError.data as IAuthData,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    default: {
 | 
				
			||||||
 | 
					      return { status: RegisterFlowStatus.InvalidRequest };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type AuthFlows = {
 | 
				
			||||||
 | 
					  loginFlows: ILoginFlowsResponse;
 | 
				
			||||||
 | 
					  registerFlows: RegisterFlowsResponse;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AuthFlowsContext = createContext<AuthFlows | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AuthFlowsProvider = AuthFlowsContext.Provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useAuthFlows = (): AuthFlows => {
 | 
				
			||||||
 | 
					  const authFlows = useContext(AuthFlowsContext);
 | 
				
			||||||
 | 
					  if (!authFlows) {
 | 
				
			||||||
 | 
					    throw new Error('Auth Flow info is not loaded!');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return authFlows;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/app/hooks/useAuthServer.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/app/hooks/useAuthServer.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					import { createContext, useContext } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AuthServerContext = createContext<string | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AuthServerProvider = AuthServerContext.Provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useAuthServer = (): string => {
 | 
				
			||||||
 | 
					  const server = useContext(AuthServerContext);
 | 
				
			||||||
 | 
					  if (server === null) {
 | 
				
			||||||
 | 
					    throw new Error('Auth server is not provided!');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return server;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/app/hooks/useAutoDiscoveryInfo.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/app/hooks/useAutoDiscoveryInfo.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					import { createContext, useContext } from 'react';
 | 
				
			||||||
 | 
					import { AutoDiscoveryInfo } from '../cs-api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const AutoDiscoverInfoContext = createContext<AutoDiscoveryInfo | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AutoDiscoveryInfoProvider = AutoDiscoverInfoContext.Provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useAutoDiscoveryInfo = (): AutoDiscoveryInfo => {
 | 
				
			||||||
 | 
					  const autoDiscoveryInfo = useContext(AutoDiscoverInfoContext);
 | 
				
			||||||
 | 
					  if (!autoDiscoveryInfo) {
 | 
				
			||||||
 | 
					    throw new Error('Auto Discovery Info not loaded');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return autoDiscoveryInfo;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/app/hooks/useClientConfig.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/app/hooks/useClientConfig.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					import { createContext, useContext } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ClientConfig = {
 | 
				
			||||||
 | 
					  defaultHomeserver?: number;
 | 
				
			||||||
 | 
					  homeserverList?: string[];
 | 
				
			||||||
 | 
					  allowCustomHomeservers?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  hashRouter?: {
 | 
				
			||||||
 | 
					    enabled?: boolean;
 | 
				
			||||||
 | 
					    basename?: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ClientConfigContext = createContext<ClientConfig | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ClientConfigProvider = ClientConfigContext.Provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useClientConfig(): ClientConfig {
 | 
				
			||||||
 | 
					  const config = useContext(ClientConfigContext);
 | 
				
			||||||
 | 
					  if (!config) throw new Error('Client config are not provided!');
 | 
				
			||||||
 | 
					  return config;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const clientDefaultServer = (clientConfig: ClientConfig): string =>
 | 
				
			||||||
 | 
					  clientConfig.homeserverList?.[clientConfig.defaultHomeserver ?? 0] ?? 'matrix.org';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const clientAllowedServer = (clientConfig: ClientConfig, server: string): boolean => {
 | 
				
			||||||
 | 
					  const { homeserverList, allowCustomHomeservers } = clientConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (allowCustomHomeservers) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return homeserverList?.includes(server) === true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ export function useCrossSigningStatus() {
 | 
				
			||||||
  const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData());
 | 
					  const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (isCSEnabled) return null;
 | 
					    if (isCSEnabled) return undefined;
 | 
				
			||||||
    const handleAccountData = (event) => {
 | 
					    const handleAccountData = (event) => {
 | 
				
			||||||
      if (event.getType() === 'm.cross_signing.master') {
 | 
					      if (event.getType() === 'm.cross_signing.master') {
 | 
				
			||||||
        setIsCSEnabled(true);
 | 
					        setIsCSEnabled(true);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										38
									
								
								src/app/hooks/useParsedLoginFlows.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/app/hooks/useParsedLoginFlows.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,38 @@
 | 
				
			||||||
 | 
					import { useMemo } from 'react';
 | 
				
			||||||
 | 
					import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth';
 | 
				
			||||||
 | 
					import { WithRequiredProp } from '../../types/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Required_SSOFlow = WithRequiredProp<ISSOFlow, 'identity_providers'>;
 | 
				
			||||||
 | 
					export const getSSOFlow = (loginFlows: LoginFlow[]): Required_SSOFlow | undefined =>
 | 
				
			||||||
 | 
					  loginFlows.find(
 | 
				
			||||||
 | 
					    (flow) =>
 | 
				
			||||||
 | 
					      (flow.type === 'm.login.sso' || flow.type === 'm.login.cas') &&
 | 
				
			||||||
 | 
					      'identity_providers' in flow &&
 | 
				
			||||||
 | 
					      Array.isArray(flow.identity_providers) &&
 | 
				
			||||||
 | 
					      flow.identity_providers.length > 0
 | 
				
			||||||
 | 
					  ) as Required_SSOFlow | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getPasswordFlow = (loginFlows: LoginFlow[]): IPasswordFlow | undefined =>
 | 
				
			||||||
 | 
					  loginFlows.find((flow) => flow.type === 'm.login.password') as IPasswordFlow;
 | 
				
			||||||
 | 
					export const getTokenFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined =>
 | 
				
			||||||
 | 
					  loginFlows.find((flow) => flow.type === 'm.login.token') as ILoginFlow & {
 | 
				
			||||||
 | 
					    type: 'm.login.token';
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ParsedLoginFlows = {
 | 
				
			||||||
 | 
					  password?: LoginFlow;
 | 
				
			||||||
 | 
					  token?: LoginFlow;
 | 
				
			||||||
 | 
					  sso?: Required_SSOFlow;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const useParsedLoginFlows = (loginFlows: LoginFlow[]) => {
 | 
				
			||||||
 | 
					  const parsedFlow: ParsedLoginFlows = useMemo<ParsedLoginFlows>(
 | 
				
			||||||
 | 
					    () => ({
 | 
				
			||||||
 | 
					      password: getPasswordFlow(loginFlows),
 | 
				
			||||||
 | 
					      token: getTokenFlow(loginFlows),
 | 
				
			||||||
 | 
					      sso: getSSOFlow(loginFlows),
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    [loginFlows]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return parsedFlow;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/app/hooks/usePasswordEmail.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/app/hooks/usePasswordEmail.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					import { MatrixClient, MatrixError } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { useCallback, useRef } from 'react';
 | 
				
			||||||
 | 
					import { AsyncState, useAsyncCallback } from './useAsyncCallback';
 | 
				
			||||||
 | 
					import { RequestEmailTokenCallback, RequestEmailTokenResponse } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const usePasswordEmail = (
 | 
				
			||||||
 | 
					  mx: MatrixClient
 | 
				
			||||||
 | 
					): [AsyncState<RequestEmailTokenResponse, MatrixError>, RequestEmailTokenCallback] => {
 | 
				
			||||||
 | 
					  const sendAttemptRef = useRef(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const passwordEmailCallback: RequestEmailTokenCallback = useCallback(
 | 
				
			||||||
 | 
					    async (email, clientSecret, nextLink) => {
 | 
				
			||||||
 | 
					      const sendAttempt = sendAttemptRef.current;
 | 
				
			||||||
 | 
					      sendAttemptRef.current += 1;
 | 
				
			||||||
 | 
					      const result = await mx.requestPasswordEmailToken(email, clientSecret, sendAttempt, nextLink);
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        email,
 | 
				
			||||||
 | 
					        clientSecret,
 | 
				
			||||||
 | 
					        result,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [mx]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [passwordEmailState, passwordEmail] = useAsyncCallback<
 | 
				
			||||||
 | 
					    RequestEmailTokenResponse,
 | 
				
			||||||
 | 
					    MatrixError,
 | 
				
			||||||
 | 
					    Parameters<RequestEmailTokenCallback>
 | 
				
			||||||
 | 
					  >(passwordEmailCallback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [passwordEmailState, passwordEmail];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/app/hooks/usePathWithOrigin.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/app/hooks/usePathWithOrigin.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,26 @@
 | 
				
			||||||
 | 
					import { useMemo } from 'react';
 | 
				
			||||||
 | 
					import { useClientConfig } from './useClientConfig';
 | 
				
			||||||
 | 
					import { trimLeadingSlash, trimSlash, trimTrailingSlash } from '../utils/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const usePathWithOrigin = (path: string): string => {
 | 
				
			||||||
 | 
					  const { hashRouter } = useClientConfig();
 | 
				
			||||||
 | 
					  const { origin } = window.location;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const pathWithOrigin = useMemo(() => {
 | 
				
			||||||
 | 
					    let url: string = trimSlash(origin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    url += `/${trimSlash(import.meta.env.BASE_URL ?? '')}`;
 | 
				
			||||||
 | 
					    url = trimTrailingSlash(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (hashRouter?.enabled) {
 | 
				
			||||||
 | 
					      url += `/#/${trimSlash(hashRouter.basename ?? '')}`;
 | 
				
			||||||
 | 
					      url = trimTrailingSlash(url);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    url += `/${trimLeadingSlash(path)}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return url;
 | 
				
			||||||
 | 
					  }, [path, hashRouter, origin]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return pathWithOrigin;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/app/hooks/useRegisterEmail.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/app/hooks/useRegisterEmail.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					import { MatrixClient, MatrixError } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { useCallback, useRef } from 'react';
 | 
				
			||||||
 | 
					import { AsyncState, useAsyncCallback } from './useAsyncCallback';
 | 
				
			||||||
 | 
					import { RequestEmailTokenCallback, RequestEmailTokenResponse } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useRegisterEmail = (
 | 
				
			||||||
 | 
					  mx: MatrixClient
 | 
				
			||||||
 | 
					): [AsyncState<RequestEmailTokenResponse, MatrixError>, RequestEmailTokenCallback] => {
 | 
				
			||||||
 | 
					  const sendAttemptRef = useRef(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const registerEmailCallback: RequestEmailTokenCallback = useCallback(
 | 
				
			||||||
 | 
					    async (email, clientSecret, nextLink) => {
 | 
				
			||||||
 | 
					      const sendAttempt = sendAttemptRef.current;
 | 
				
			||||||
 | 
					      sendAttemptRef.current += 1;
 | 
				
			||||||
 | 
					      const result = await mx.requestRegisterEmailToken(email, clientSecret, sendAttempt, nextLink);
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        email,
 | 
				
			||||||
 | 
					        clientSecret,
 | 
				
			||||||
 | 
					        result,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [mx]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [registerEmailState, registerEmail] = useAsyncCallback<
 | 
				
			||||||
 | 
					    RequestEmailTokenResponse,
 | 
				
			||||||
 | 
					    MatrixError,
 | 
				
			||||||
 | 
					    Parameters<RequestEmailTokenCallback>
 | 
				
			||||||
 | 
					  >(registerEmailCallback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [registerEmailState, registerEmail];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/app/hooks/useSpecVersions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/app/hooks/useSpecVersions.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import { createContext, useContext } from 'react';
 | 
				
			||||||
 | 
					import { SpecVersions } from '../cs-api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SpecVersionsContext = createContext<SpecVersions | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SpecVersionsProvider = SpecVersionsContext.Provider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useSpecVersions(): SpecVersions {
 | 
				
			||||||
 | 
					  const versions = useContext(SpecVersionsContext);
 | 
				
			||||||
 | 
					  if (!versions) throw new Error('Server versions are not provided!');
 | 
				
			||||||
 | 
					  return versions;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										96
									
								
								src/app/hooks/useUIAFlows.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/app/hooks/useUIAFlows.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,96 @@
 | 
				
			||||||
 | 
					import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { useCallback, useMemo } from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getSupportedUIAFlows,
 | 
				
			||||||
 | 
					  getUIACompleted,
 | 
				
			||||||
 | 
					  getUIAError,
 | 
				
			||||||
 | 
					  getUIAErrorCode,
 | 
				
			||||||
 | 
					  getUIAParams,
 | 
				
			||||||
 | 
					  getUIASession,
 | 
				
			||||||
 | 
					} from '../utils/matrix-uia';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SUPPORTED_FLOW_TYPES = [
 | 
				
			||||||
 | 
					  AuthType.Dummy,
 | 
				
			||||||
 | 
					  AuthType.Password,
 | 
				
			||||||
 | 
					  AuthType.Email,
 | 
				
			||||||
 | 
					  AuthType.Terms,
 | 
				
			||||||
 | 
					  AuthType.Recaptcha,
 | 
				
			||||||
 | 
					  AuthType.RegistrationToken,
 | 
				
			||||||
 | 
					] as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useSupportedUIAFlows = (uiaFlows: UIAFlow[], supportedStages: string[]): UIAFlow[] =>
 | 
				
			||||||
 | 
					  useMemo(() => getSupportedUIAFlows(uiaFlows, supportedStages), [uiaFlows, supportedStages]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useUIACompleted = (authData: IAuthData): string[] =>
 | 
				
			||||||
 | 
					  useMemo(() => getUIACompleted(authData), [authData]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useUIAParams = (authData: IAuthData) =>
 | 
				
			||||||
 | 
					  useMemo(() => getUIAParams(authData), [authData]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useUIASession = (authData: IAuthData) =>
 | 
				
			||||||
 | 
					  useMemo(() => getUIASession(authData), [authData]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useUIAErrorCode = (authData: IAuthData) =>
 | 
				
			||||||
 | 
					  useMemo(() => getUIAErrorCode(authData), [authData]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useUIAError = (authData: IAuthData) =>
 | 
				
			||||||
 | 
					  useMemo(() => getUIAError(authData), [authData]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type StageInfo = Record<string, unknown>;
 | 
				
			||||||
 | 
					export type AuthStageData = {
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					  info?: StageInfo;
 | 
				
			||||||
 | 
					  session?: string;
 | 
				
			||||||
 | 
					  errorCode?: string;
 | 
				
			||||||
 | 
					  error?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export type AuthStageDataGetter = () => AuthStageData | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type UIAFlowInterface = {
 | 
				
			||||||
 | 
					  getStageToComplete: AuthStageDataGetter;
 | 
				
			||||||
 | 
					  hasStage: (stageType: string) => boolean;
 | 
				
			||||||
 | 
					  getStageInfo: (stageType: string) => StageInfo | undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const useUIAFlow = (authData: IAuthData, uiaFlow: UIAFlow): UIAFlowInterface => {
 | 
				
			||||||
 | 
					  const completed = useUIACompleted(authData);
 | 
				
			||||||
 | 
					  const params = useUIAParams(authData);
 | 
				
			||||||
 | 
					  const session = useUIASession(authData);
 | 
				
			||||||
 | 
					  const errorCode = useUIAErrorCode(authData);
 | 
				
			||||||
 | 
					  const error = useUIAError(authData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getStageToComplete: AuthStageDataGetter = useCallback(() => {
 | 
				
			||||||
 | 
					    const { stages } = uiaFlow;
 | 
				
			||||||
 | 
					    const nextStage = stages.find((stage) => !completed.includes(stage));
 | 
				
			||||||
 | 
					    if (!nextStage) return undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const info = params[nextStage];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      type: nextStage,
 | 
				
			||||||
 | 
					      info,
 | 
				
			||||||
 | 
					      session,
 | 
				
			||||||
 | 
					      errorCode,
 | 
				
			||||||
 | 
					      error,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [uiaFlow, completed, params, errorCode, error, session]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const hasStage = useCallback(
 | 
				
			||||||
 | 
					    (stageType: string): boolean => uiaFlow.stages.includes(stageType),
 | 
				
			||||||
 | 
					    [uiaFlow]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getStageInfo = useCallback(
 | 
				
			||||||
 | 
					    (stageType: string): StageInfo | undefined => {
 | 
				
			||||||
 | 
					      if (!hasStage(stageType)) return undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return params[stageType];
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [hasStage, params]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    getStageToComplete,
 | 
				
			||||||
 | 
					    hasStage,
 | 
				
			||||||
 | 
					    getStageInfo,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -110,7 +110,9 @@ function RoomAliases({ roomId }) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId);
 | 
					  const canPublishAlias = room.currentState.maySendStateEvent('m.room.canonical_alias', userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => isMountedStore.setItem(true), []);
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    isMountedStore.setItem(true)
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    let isUnmounted = false;
 | 
					    let isUnmounted = false;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,9 @@ function useVisibility(roomId) {
 | 
				
			||||||
  const room = mx.getRoom(roomId);
 | 
					  const room = mx.getRoom(roomId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [activeType, setActiveType] = useState(room.getHistoryVisibility());
 | 
					  const [activeType, setActiveType] = useState(room.getHistoryVisibility());
 | 
				
			||||||
  useEffect(() => setActiveType(room.getHistoryVisibility()), [roomId]);
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setActiveType(room.getHistoryVisibility());
 | 
				
			||||||
 | 
					  }, [roomId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const setVisibility = useCallback((item) => {
 | 
					  const setVisibility = useCallback((item) => {
 | 
				
			||||||
    if (item.type === activeType.type) return;
 | 
					    if (item.type === activeType.type) return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,7 +103,9 @@ function setRoomNotifType(roomId, newType) {
 | 
				
			||||||
function useNotifications(roomId) {
 | 
					function useNotifications(roomId) {
 | 
				
			||||||
  const { notifications } = initMatrix;
 | 
					  const { notifications } = initMatrix;
 | 
				
			||||||
  const [activeType, setActiveType] = useState(notifications.getNotiType(roomId));
 | 
					  const [activeType, setActiveType] = useState(notifications.getNotiType(roomId));
 | 
				
			||||||
  useEffect(() => setActiveType(notifications.getNotiType(roomId)), [roomId]);
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setActiveType(notifications.getNotiType(roomId));
 | 
				
			||||||
 | 
					  }, [roomId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const setNotification = useCallback((item) => {
 | 
					  const setNotification = useCallback((item) => {
 | 
				
			||||||
    if (item.type === activeType.type) return;
 | 
					    if (item.type === activeType.type) return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,7 +29,9 @@ function useRoomSearch(roomId) {
 | 
				
			||||||
  const mountStore = useStore(roomId);
 | 
					  const mountStore = useStore(roomId);
 | 
				
			||||||
  const mx = initMatrix.matrixClient;
 | 
					  const mx = initMatrix.matrixClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => mountStore.setItem(true), [roomId]);
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    mountStore.setItem(true)
 | 
				
			||||||
 | 
					  }, [roomId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (searchData?.results?.length > 0) {
 | 
					    if (searchData?.results?.length > 0) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,9 @@ function useVisibility(roomId) {
 | 
				
			||||||
  const room = mx.getRoom(roomId);
 | 
					  const room = mx.getRoom(roomId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [activeType, setActiveType] = useState(room.getJoinRule());
 | 
					  const [activeType, setActiveType] = useState(room.getJoinRule());
 | 
				
			||||||
  useEffect(() => setActiveType(room.getJoinRule()), [roomId]);
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setActiveType(room.getJoinRule());
 | 
				
			||||||
 | 
					  }, [roomId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const setNotification = useCallback((item) => {
 | 
					  const setNotification = useCallback((item) => {
 | 
				
			||||||
    if (item.type === activeType.type) return;
 | 
					    if (item.type === activeType.type) return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -80,7 +80,7 @@ function EmojiVerificationContent({ data, requestClose }) {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (request === null) return null;
 | 
					    if (request === null) return undefined;
 | 
				
			||||||
    const req = request;
 | 
					    const req = request;
 | 
				
			||||||
    req.on('change', handleChange);
 | 
					    req.on('change', handleChange);
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,10 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
 | 
				
			||||||
    const [previewStatus, loadPreview] = useAsyncCallback(
 | 
					    const [previewStatus, loadPreview] = useAsyncCallback(
 | 
				
			||||||
      useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
 | 
					      useCallback(() => mx.getUrlPreview(url, ts), [url, ts, mx])
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    if (previewStatus.status === AsyncStatus.Idle) loadPreview();
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      loadPreview();
 | 
				
			||||||
 | 
					    }, [loadPreview]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (previewStatus.status === AsyncStatus.Error) return null;
 | 
					    if (previewStatus.status === AsyncStatus.Error) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -302,7 +302,9 @@ function SpaceManageContent({ roomId, requestClose }) {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, [roomId]);
 | 
					  }, [roomId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => setSelected([]), [spacePath]);
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setSelected([]);
 | 
				
			||||||
 | 
					  }, [spacePath]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSelected = (selectedRoomId) => {
 | 
					  const handleSelected = (selectedRoomId) => {
 | 
				
			||||||
    const newSelected = [...selected];
 | 
					    const newSelected = [...selected];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,17 +0,0 @@
 | 
				
			||||||
import React, { StrictMode } from 'react';
 | 
					 | 
				
			||||||
import { Provider } from 'jotai';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { isAuthenticated } from '../../client/state/auth';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import Auth from '../templates/auth/Auth';
 | 
					 | 
				
			||||||
import Client from '../templates/client/Client';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function App() {
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <StrictMode>
 | 
					 | 
				
			||||||
      <Provider>{isAuthenticated() ? <Client /> : <Auth />}</Provider>
 | 
					 | 
				
			||||||
    </StrictMode>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default App;
 | 
					 | 
				
			||||||
							
								
								
									
										82
									
								
								src/app/pages/App.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/app/pages/App.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,82 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Provider as JotaiProvider } from 'jotai';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Route,
 | 
				
			||||||
 | 
					  RouterProvider,
 | 
				
			||||||
 | 
					  createBrowserRouter,
 | 
				
			||||||
 | 
					  createHashRouter,
 | 
				
			||||||
 | 
					  createRoutesFromElements,
 | 
				
			||||||
 | 
					  redirect,
 | 
				
			||||||
 | 
					} from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ClientConfigLoader } from '../components/ClientConfigLoader';
 | 
				
			||||||
 | 
					import { ClientConfig, ClientConfigProvider } from '../hooks/useClientConfig';
 | 
				
			||||||
 | 
					import { AuthLayout, Login, Register, ResetPassword, authLayoutLoader } from './auth';
 | 
				
			||||||
 | 
					import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, ROOT_PATH } from './paths';
 | 
				
			||||||
 | 
					import { isAuthenticated } from '../../client/state/auth';
 | 
				
			||||||
 | 
					import Client from '../templates/client/Client';
 | 
				
			||||||
 | 
					import { getLoginPath } from './pathUtils';
 | 
				
			||||||
 | 
					import { ConfigConfigError, ConfigConfigLoading } from './ConfigConfig';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createRouter = (clientConfig: ClientConfig) => {
 | 
				
			||||||
 | 
					  const { hashRouter } = clientConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const routes = createRoutesFromElements(
 | 
				
			||||||
 | 
					    <Route>
 | 
				
			||||||
 | 
					      <Route
 | 
				
			||||||
 | 
					        path={ROOT_PATH}
 | 
				
			||||||
 | 
					        loader={() => {
 | 
				
			||||||
 | 
					          if (isAuthenticated()) return redirect('/home');
 | 
				
			||||||
 | 
					          return redirect(getLoginPath());
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <Route loader={authLayoutLoader} element={<AuthLayout />}>
 | 
				
			||||||
 | 
					        <Route path={LOGIN_PATH} element={<Login />} />
 | 
				
			||||||
 | 
					        <Route path={REGISTER_PATH} element={<Register />} />
 | 
				
			||||||
 | 
					        <Route path={RESET_PASSWORD_PATH} element={<ResetPassword />} />
 | 
				
			||||||
 | 
					      </Route>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Route
 | 
				
			||||||
 | 
					        loader={() => {
 | 
				
			||||||
 | 
					          if (!isAuthenticated()) return redirect(getLoginPath());
 | 
				
			||||||
 | 
					          return null;
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Route path="/home" element={<Client />} />
 | 
				
			||||||
 | 
					        <Route path="/direct" element={<p>direct</p>} />
 | 
				
			||||||
 | 
					        <Route path="/:spaceIdOrAlias" element={<p>:spaceIdOrAlias</p>} />
 | 
				
			||||||
 | 
					        <Route path="/explore" element={<p>explore</p>} />
 | 
				
			||||||
 | 
					      </Route>
 | 
				
			||||||
 | 
					      <Route path="/*" element={<p>Page not found</p>} />
 | 
				
			||||||
 | 
					    </Route>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (hashRouter?.enabled) {
 | 
				
			||||||
 | 
					    return createHashRouter(routes, { basename: hashRouter.basename });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return createBrowserRouter(routes, {
 | 
				
			||||||
 | 
					    basename: import.meta.env.BASE_URL,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: app crash boundary
 | 
				
			||||||
 | 
					function App() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ClientConfigLoader
 | 
				
			||||||
 | 
					      fallback={() => <ConfigConfigLoading />}
 | 
				
			||||||
 | 
					      error={(err, retry, ignore) => (
 | 
				
			||||||
 | 
					        <ConfigConfigError error={err} retry={retry} ignore={ignore} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {(clientConfig) => (
 | 
				
			||||||
 | 
					        <ClientConfigProvider value={clientConfig}>
 | 
				
			||||||
 | 
					          <JotaiProvider>
 | 
				
			||||||
 | 
					            <RouterProvider router={createRouter(clientConfig)} />
 | 
				
			||||||
 | 
					          </JotaiProvider>
 | 
				
			||||||
 | 
					        </ClientConfigProvider>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </ClientConfigLoader>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default App;
 | 
				
			||||||
							
								
								
									
										53
									
								
								src/app/pages/ConfigConfig.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/app/pages/ConfigConfig.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					import { Box, Button, Dialog, Spinner, Text, color, config } from 'folds';
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { SplashScreen } from '../components/splash-screen';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ConfigConfigLoading() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <SplashScreen>
 | 
				
			||||||
 | 
					      <Box grow="Yes" direction="Column" gap="400" alignItems="Center" justifyContent="Center">
 | 
				
			||||||
 | 
					        <Spinner variant="Secondary" size="600" />
 | 
				
			||||||
 | 
					        <Text>Heating up</Text>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </SplashScreen>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ConfigConfigErrorProps = {
 | 
				
			||||||
 | 
					  error: unknown;
 | 
				
			||||||
 | 
					  retry: () => void;
 | 
				
			||||||
 | 
					  ignore: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function ConfigConfigError({ error, retry, ignore }: ConfigConfigErrorProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <SplashScreen>
 | 
				
			||||||
 | 
					      <Box grow="Yes" direction="Column" gap="400" alignItems="Center" justifyContent="Center">
 | 
				
			||||||
 | 
					        <Dialog>
 | 
				
			||||||
 | 
					          <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
 | 
				
			||||||
 | 
					            <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					              <Text>Failed to load client configuration file.</Text>
 | 
				
			||||||
 | 
					              {typeof error === 'object' &&
 | 
				
			||||||
 | 
					                error &&
 | 
				
			||||||
 | 
					                'message' in error &&
 | 
				
			||||||
 | 
					                typeof error.message === 'string' && (
 | 
				
			||||||
 | 
					                  <Text size="T300" style={{ color: color.Critical.Main }}>
 | 
				
			||||||
 | 
					                    {error.message}
 | 
				
			||||||
 | 
					                  </Text>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					            <Button variant="Critical" onClick={retry}>
 | 
				
			||||||
 | 
					              <Text as="span" size="B400">
 | 
				
			||||||
 | 
					                Retry
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					            <Button variant="Critical" onClick={ignore} fill="Soft">
 | 
				
			||||||
 | 
					              <Text as="span" size="B400">
 | 
				
			||||||
 | 
					                Continue
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        </Dialog>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </SplashScreen>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								src/app/pages/auth/AuthFooter.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/app/pages/auth/AuthFooter.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Box, Text } from 'folds';
 | 
				
			||||||
 | 
					import * as css from './styles.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function AuthFooter() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box className={css.AuthFooter} justifyContent="Center" gap="400" wrap="Wrap">
 | 
				
			||||||
 | 
					      <Text as="a" size="T300" href="https://cinny.in" target="_blank" rel="noreferrer">
 | 
				
			||||||
 | 
					        About
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      <Text
 | 
				
			||||||
 | 
					        as="a"
 | 
				
			||||||
 | 
					        size="T300"
 | 
				
			||||||
 | 
					        href="https://github.com/ajbura/cinny/releases"
 | 
				
			||||||
 | 
					        target="_blank"
 | 
				
			||||||
 | 
					        rel="noreferrer"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        v3.2.0
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      <Text as="a" size="T300" href="https://twitter.com/cinnyapp" target="_blank" rel="noreferrer">
 | 
				
			||||||
 | 
					        Twitter
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      <Text as="a" size="T300" href="https://matrix.org" target="_blank" rel="noreferrer">
 | 
				
			||||||
 | 
					        Powered by Matrix
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										215
									
								
								src/app/pages/auth/AuthLayout.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								src/app/pages/auth/AuthLayout.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,215 @@
 | 
				
			||||||
 | 
					import React, { useCallback, useEffect } from 'react';
 | 
				
			||||||
 | 
					import { Box, Header, Scroll, Spinner, Text, color } from 'folds';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  LoaderFunction,
 | 
				
			||||||
 | 
					  Outlet,
 | 
				
			||||||
 | 
					  generatePath,
 | 
				
			||||||
 | 
					  matchPath,
 | 
				
			||||||
 | 
					  redirect,
 | 
				
			||||||
 | 
					  useLocation,
 | 
				
			||||||
 | 
					  useNavigate,
 | 
				
			||||||
 | 
					  useParams,
 | 
				
			||||||
 | 
					} from 'react-router-dom';
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AuthFooter } from './AuthFooter';
 | 
				
			||||||
 | 
					import * as css from './styles.css';
 | 
				
			||||||
 | 
					import * as PatternsCss from '../../styles/Patterns.css';
 | 
				
			||||||
 | 
					import { isAuthenticated } from '../../../client/state/auth';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  clientAllowedServer,
 | 
				
			||||||
 | 
					  clientDefaultServer,
 | 
				
			||||||
 | 
					  useClientConfig,
 | 
				
			||||||
 | 
					} from '../../hooks/useClientConfig';
 | 
				
			||||||
 | 
					import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { LOGIN_PATH, REGISTER_PATH } from '../paths';
 | 
				
			||||||
 | 
					import CinnySVG from '../../../../public/res/svg/cinny.svg';
 | 
				
			||||||
 | 
					import { ServerPicker } from './ServerPicker';
 | 
				
			||||||
 | 
					import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api';
 | 
				
			||||||
 | 
					import { SpecVersionsLoader } from '../../components/SpecVersionsLoader';
 | 
				
			||||||
 | 
					import { SpecVersionsProvider } from '../../hooks/useSpecVersions';
 | 
				
			||||||
 | 
					import { AutoDiscoveryInfoProvider } from '../../hooks/useAutoDiscoveryInfo';
 | 
				
			||||||
 | 
					import { AuthFlowsLoader } from '../../components/AuthFlowsLoader';
 | 
				
			||||||
 | 
					import { AuthFlowsProvider } from '../../hooks/useAuthFlows';
 | 
				
			||||||
 | 
					import { AuthServerProvider } from '../../hooks/useAuthServer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const authLayoutLoader: LoaderFunction = () => {
 | 
				
			||||||
 | 
					  if (isAuthenticated()) {
 | 
				
			||||||
 | 
					    return redirect('/');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const currentAuthPath = (pathname: string): string => {
 | 
				
			||||||
 | 
					  if (matchPath(LOGIN_PATH, pathname)) {
 | 
				
			||||||
 | 
					    return LOGIN_PATH;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (matchPath(REGISTER_PATH, pathname)) {
 | 
				
			||||||
 | 
					    return REGISTER_PATH;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return LOGIN_PATH;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function AuthLayoutLoading({ message }: { message: string }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box justifyContent="Center" alignItems="Center" gap="200">
 | 
				
			||||||
 | 
					      <Spinner size="100" variant="Secondary" />
 | 
				
			||||||
 | 
					      <Text align="Center" size="T300">
 | 
				
			||||||
 | 
					        {message}
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function AuthLayoutError({ message }: { message: string }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box justifyContent="Center" alignItems="Center" gap="200">
 | 
				
			||||||
 | 
					      <Text align="Center" style={{ color: color.Critical.Main }} size="T300">
 | 
				
			||||||
 | 
					        {message}
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function AuthLayout() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					  const location = useLocation();
 | 
				
			||||||
 | 
					  const { server: urlEncodedServer } = useParams();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const clientConfig = useClientConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const defaultServer = clientDefaultServer(clientConfig);
 | 
				
			||||||
 | 
					  let server: string = urlEncodedServer ? decodeURIComponent(urlEncodedServer) : defaultServer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!clientAllowedServer(clientConfig, server)) {
 | 
				
			||||||
 | 
					    server = defaultServer;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [discoveryState, discoverServer] = useAsyncCallback(
 | 
				
			||||||
 | 
					    useCallback(async (serverName: string) => {
 | 
				
			||||||
 | 
					      const response = await autoDiscovery(fetch, serverName);
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        serverName,
 | 
				
			||||||
 | 
					        response,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }, [])
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (server) discoverServer(server);
 | 
				
			||||||
 | 
					  }, [discoverServer, server]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // if server is mismatches with path server, update path
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (!urlEncodedServer || decodeURIComponent(urlEncodedServer) !== server) {
 | 
				
			||||||
 | 
					      navigate(
 | 
				
			||||||
 | 
					        generatePath(currentAuthPath(location.pathname), {
 | 
				
			||||||
 | 
					          server: encodeURIComponent(server),
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        { replace: true }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [urlEncodedServer, navigate, location, server]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const selectServer = useCallback(
 | 
				
			||||||
 | 
					    (newServer: string) => {
 | 
				
			||||||
 | 
					      if (newServer === server) {
 | 
				
			||||||
 | 
					        if (discoveryState.status === AsyncStatus.Loading) return;
 | 
				
			||||||
 | 
					        discoverServer(server);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      navigate(
 | 
				
			||||||
 | 
					        generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [navigate, location, discoveryState, server, discoverServer]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [autoDiscoveryError, autoDiscoveryInfo] =
 | 
				
			||||||
 | 
					    discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Scroll variant="Background" visibility="Hover" size="300" hideTrack>
 | 
				
			||||||
 | 
					      <Box
 | 
				
			||||||
 | 
					        className={classNames(css.AuthLayout, PatternsCss.BackgroundDotPattern)}
 | 
				
			||||||
 | 
					        direction="Column"
 | 
				
			||||||
 | 
					        alignItems="Center"
 | 
				
			||||||
 | 
					        justifyContent="SpaceBetween"
 | 
				
			||||||
 | 
					        gap="400"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Box direction="Column" className={css.AuthCard}>
 | 
				
			||||||
 | 
					          <Header className={css.AuthHeader} size="600" variant="Surface">
 | 
				
			||||||
 | 
					            <Box grow="Yes" direction="Row" gap="300" alignItems="Center">
 | 
				
			||||||
 | 
					              <img className={css.AuthLogo} src={CinnySVG} alt="Cinny Logo" />
 | 
				
			||||||
 | 
					              <Text size="H3">Cinny</Text>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					          </Header>
 | 
				
			||||||
 | 
					          <Box className={css.AuthCardContent} direction="Column">
 | 
				
			||||||
 | 
					            <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					              <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					                Homeserver
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					              <ServerPicker
 | 
				
			||||||
 | 
					                server={server}
 | 
				
			||||||
 | 
					                serverList={clientConfig.homeserverList ?? []}
 | 
				
			||||||
 | 
					                allowCustomServer={clientConfig.allowCustomHomeservers}
 | 
				
			||||||
 | 
					                onServerChange={selectServer}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					            {discoveryState.status === AsyncStatus.Loading && (
 | 
				
			||||||
 | 
					              <AuthLayoutLoading message="Looking for homeserver..." />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {discoveryState.status === AsyncStatus.Error && (
 | 
				
			||||||
 | 
					              <AuthLayoutError message="Failed to find homeserver." />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
 | 
				
			||||||
 | 
					              <AuthLayoutError
 | 
				
			||||||
 | 
					                message={`Failed to connect. Homeserver configuration found with ${autoDiscoveryError.host} appears unusable.`}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
 | 
				
			||||||
 | 
					              <AuthLayoutError message="Failed to connect. Homeserver configuration base_url appears invalid." />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
 | 
				
			||||||
 | 
					              <AuthServerProvider value={discoveryState.data.serverName}>
 | 
				
			||||||
 | 
					                <AutoDiscoveryInfoProvider value={autoDiscoveryInfo}>
 | 
				
			||||||
 | 
					                  <SpecVersionsLoader
 | 
				
			||||||
 | 
					                    fallback={() => (
 | 
				
			||||||
 | 
					                      <AuthLayoutLoading
 | 
				
			||||||
 | 
					                        message={`Connecting to ${autoDiscoveryInfo['m.homeserver'].base_url}`}
 | 
				
			||||||
 | 
					                      />
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                    error={() => (
 | 
				
			||||||
 | 
					                      <AuthLayoutError message="Failed to connect. Either homeserver is unavailable at this moment or does not exist." />
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    {(specVersions) => (
 | 
				
			||||||
 | 
					                      <SpecVersionsProvider value={specVersions}>
 | 
				
			||||||
 | 
					                        <AuthFlowsLoader
 | 
				
			||||||
 | 
					                          fallback={() => (
 | 
				
			||||||
 | 
					                            <AuthLayoutLoading message="Loading authentication flow..." />
 | 
				
			||||||
 | 
					                          )}
 | 
				
			||||||
 | 
					                          error={() => (
 | 
				
			||||||
 | 
					                            <AuthLayoutError message="Failed to get authentication flow information." />
 | 
				
			||||||
 | 
					                          )}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                          {(authFlows) => (
 | 
				
			||||||
 | 
					                            <AuthFlowsProvider value={authFlows}>
 | 
				
			||||||
 | 
					                              <Outlet />
 | 
				
			||||||
 | 
					                            </AuthFlowsProvider>
 | 
				
			||||||
 | 
					                          )}
 | 
				
			||||||
 | 
					                        </AuthFlowsLoader>
 | 
				
			||||||
 | 
					                      </SpecVersionsProvider>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  </SpecVersionsLoader>
 | 
				
			||||||
 | 
					                </AutoDiscoveryInfoProvider>
 | 
				
			||||||
 | 
					              </AuthServerProvider>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        <AuthFooter />
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Scroll>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/app/pages/auth/FiledError.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/app/pages/auth/FiledError.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Box, Icon, Icons, color, Text } from 'folds';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function FieldError({ message }: { message: string }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box style={{ color: color.Critical.Main }} alignItems="Center" gap="100">
 | 
				
			||||||
 | 
					      <Icon size="50" filled src={Icons.Warning} />
 | 
				
			||||||
 | 
					      <Text size="T200">
 | 
				
			||||||
 | 
					        <b>{message}</b>
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/app/pages/auth/OrDivider.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/app/pages/auth/OrDivider.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Box, Line, Text } from 'folds';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function OrDivider() {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box gap="400" alignItems="Center">
 | 
				
			||||||
 | 
					      <Line style={{ flexGrow: 1 }} direction="Horizontal" size="300" variant="Surface" />
 | 
				
			||||||
 | 
					      <Text>OR</Text>
 | 
				
			||||||
 | 
					      <Line style={{ flexGrow: 1 }} direction="Horizontal" size="300" variant="Surface" />
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										68
									
								
								src/app/pages/auth/SSOLogin.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/app/pages/auth/SSOLogin.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					import { Avatar, AvatarImage, Box, Button, Text } from 'folds';
 | 
				
			||||||
 | 
					import { IIdentityProvider, createClient } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import React, { useMemo } from 'react';
 | 
				
			||||||
 | 
					import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SSOLoginProps = {
 | 
				
			||||||
 | 
					  providers: IIdentityProvider[];
 | 
				
			||||||
 | 
					  asIcons?: boolean;
 | 
				
			||||||
 | 
					  redirectUrl: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function SSOLogin({ providers, redirectUrl, asIcons }: SSOLoginProps) {
 | 
				
			||||||
 | 
					  const discovery = useAutoDiscoveryInfo();
 | 
				
			||||||
 | 
					  const baseUrl = discovery['m.homeserver'].base_url;
 | 
				
			||||||
 | 
					  const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getSSOIdUrl = (ssoId: string): string => mx.getSsoLoginUrl(redirectUrl, 'sso', ssoId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box justifyContent="Center" gap="600" wrap="Wrap">
 | 
				
			||||||
 | 
					      {providers.map((provider) => {
 | 
				
			||||||
 | 
					        const { id, name, icon } = provider;
 | 
				
			||||||
 | 
					        const iconUrl = icon && mx.mxcUrlToHttp(icon, 96, 96, 'crop', false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const buttonTitle = `Continue with ${name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (iconUrl && asIcons) {
 | 
				
			||||||
 | 
					          return (
 | 
				
			||||||
 | 
					            <Avatar
 | 
				
			||||||
 | 
					              style={{ cursor: 'pointer' }}
 | 
				
			||||||
 | 
					              key={id}
 | 
				
			||||||
 | 
					              as="a"
 | 
				
			||||||
 | 
					              href={getSSOIdUrl(id)}
 | 
				
			||||||
 | 
					              aria-label={buttonTitle}
 | 
				
			||||||
 | 
					              size="300"
 | 
				
			||||||
 | 
					              radii="300"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <AvatarImage src={iconUrl} alt={name} title={buttonTitle} />
 | 
				
			||||||
 | 
					            </Avatar>
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          <Button
 | 
				
			||||||
 | 
					            style={{ width: '100%' }}
 | 
				
			||||||
 | 
					            key={id}
 | 
				
			||||||
 | 
					            as="a"
 | 
				
			||||||
 | 
					            href={getSSOIdUrl(id)}
 | 
				
			||||||
 | 
					            size="500"
 | 
				
			||||||
 | 
					            variant="Secondary"
 | 
				
			||||||
 | 
					            fill="Soft"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            before={
 | 
				
			||||||
 | 
					              iconUrl && (
 | 
				
			||||||
 | 
					                <Avatar size="200" radii="300">
 | 
				
			||||||
 | 
					                  <AvatarImage src={iconUrl} alt={name} />
 | 
				
			||||||
 | 
					                </Avatar>
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Text align="Center" size="B500" truncate>
 | 
				
			||||||
 | 
					              {buttonTitle}
 | 
				
			||||||
 | 
					            </Text>
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      })}
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										140
									
								
								src/app/pages/auth/ServerPicker.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/app/pages/auth/ServerPicker.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,140 @@
 | 
				
			||||||
 | 
					import React, {
 | 
				
			||||||
 | 
					  ChangeEventHandler,
 | 
				
			||||||
 | 
					  KeyboardEventHandler,
 | 
				
			||||||
 | 
					  MouseEventHandler,
 | 
				
			||||||
 | 
					  useEffect,
 | 
				
			||||||
 | 
					  useRef,
 | 
				
			||||||
 | 
					  useState,
 | 
				
			||||||
 | 
					} from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Header,
 | 
				
			||||||
 | 
					  Icon,
 | 
				
			||||||
 | 
					  IconButton,
 | 
				
			||||||
 | 
					  Icons,
 | 
				
			||||||
 | 
					  Input,
 | 
				
			||||||
 | 
					  Menu,
 | 
				
			||||||
 | 
					  MenuItem,
 | 
				
			||||||
 | 
					  PopOut,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  config,
 | 
				
			||||||
 | 
					} from 'folds';
 | 
				
			||||||
 | 
					import FocusTrap from 'focus-trap-react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useDebounce } from '../../hooks/useDebounce';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ServerPicker({
 | 
				
			||||||
 | 
					  server,
 | 
				
			||||||
 | 
					  serverList,
 | 
				
			||||||
 | 
					  allowCustomServer,
 | 
				
			||||||
 | 
					  onServerChange,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  server: string;
 | 
				
			||||||
 | 
					  serverList: string[];
 | 
				
			||||||
 | 
					  allowCustomServer?: boolean;
 | 
				
			||||||
 | 
					  onServerChange: (server: string) => void;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					  const [serverMenu, setServerMenu] = useState(false);
 | 
				
			||||||
 | 
					  const serverInputRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // sync input with it outside server changes
 | 
				
			||||||
 | 
					    if (serverInputRef.current && serverInputRef.current.value !== server) {
 | 
				
			||||||
 | 
					      serverInputRef.current.value = server;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [server]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const debounceServerSelect = useDebounce(onServerChange, { wait: 700 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleServerChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
 | 
				
			||||||
 | 
					    const inputServer = evt.target.value.trim();
 | 
				
			||||||
 | 
					    if (inputServer) debounceServerSelect(inputServer);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (evt) => {
 | 
				
			||||||
 | 
					    if (evt.key === 'ArrowDown') {
 | 
				
			||||||
 | 
					      evt.preventDefault();
 | 
				
			||||||
 | 
					      setServerMenu(true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (evt.key === 'Enter') {
 | 
				
			||||||
 | 
					      evt.preventDefault();
 | 
				
			||||||
 | 
					      const inputServer = evt.currentTarget.value.trim();
 | 
				
			||||||
 | 
					      if (inputServer) onServerChange(inputServer);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleServerSelect: MouseEventHandler<HTMLButtonElement> = (evt) => {
 | 
				
			||||||
 | 
					    const selectedServer = evt.currentTarget.getAttribute('data-server');
 | 
				
			||||||
 | 
					    if (selectedServer) {
 | 
				
			||||||
 | 
					      onServerChange(selectedServer);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setServerMenu(false);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Input
 | 
				
			||||||
 | 
					      ref={serverInputRef}
 | 
				
			||||||
 | 
					      style={{ paddingRight: config.space.S200 }}
 | 
				
			||||||
 | 
					      variant={allowCustomServer ? 'Background' : 'Surface'}
 | 
				
			||||||
 | 
					      outlined
 | 
				
			||||||
 | 
					      defaultValue={server}
 | 
				
			||||||
 | 
					      onChange={handleServerChange}
 | 
				
			||||||
 | 
					      onKeyDown={handleKeyDown}
 | 
				
			||||||
 | 
					      size="500"
 | 
				
			||||||
 | 
					      readOnly={!allowCustomServer}
 | 
				
			||||||
 | 
					      onClick={allowCustomServer ? undefined : () => setServerMenu(true)}
 | 
				
			||||||
 | 
					      after={
 | 
				
			||||||
 | 
					        serverList.length === 0 || (serverList.length === 1 && !allowCustomServer) ? undefined : (
 | 
				
			||||||
 | 
					          <PopOut
 | 
				
			||||||
 | 
					            open={serverMenu}
 | 
				
			||||||
 | 
					            position="Bottom"
 | 
				
			||||||
 | 
					            align="End"
 | 
				
			||||||
 | 
					            offset={4}
 | 
				
			||||||
 | 
					            content={
 | 
				
			||||||
 | 
					              <FocusTrap
 | 
				
			||||||
 | 
					                focusTrapOptions={{
 | 
				
			||||||
 | 
					                  initialFocus: false,
 | 
				
			||||||
 | 
					                  onDeactivate: () => setServerMenu(false),
 | 
				
			||||||
 | 
					                  clickOutsideDeactivates: true,
 | 
				
			||||||
 | 
					                  isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
 | 
				
			||||||
 | 
					                  isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Menu>
 | 
				
			||||||
 | 
					                  <Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
 | 
				
			||||||
 | 
					                    <Text size="L400">Homeserver List</Text>
 | 
				
			||||||
 | 
					                  </Header>
 | 
				
			||||||
 | 
					                  <div style={{ padding: config.space.S100, paddingTop: 0 }}>
 | 
				
			||||||
 | 
					                    {serverList?.map((serverName) => (
 | 
				
			||||||
 | 
					                      <MenuItem
 | 
				
			||||||
 | 
					                        key={serverName}
 | 
				
			||||||
 | 
					                        radii="300"
 | 
				
			||||||
 | 
					                        aria-pressed={serverName === server}
 | 
				
			||||||
 | 
					                        data-server={serverName}
 | 
				
			||||||
 | 
					                        onClick={handleServerSelect}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <Text>{serverName}</Text>
 | 
				
			||||||
 | 
					                      </MenuItem>
 | 
				
			||||||
 | 
					                    ))}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </Menu>
 | 
				
			||||||
 | 
					              </FocusTrap>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {(anchorRef) => (
 | 
				
			||||||
 | 
					              <IconButton
 | 
				
			||||||
 | 
					                ref={anchorRef}
 | 
				
			||||||
 | 
					                onClick={() => setServerMenu(true)}
 | 
				
			||||||
 | 
					                variant={allowCustomServer ? 'Background' : 'Surface'}
 | 
				
			||||||
 | 
					                size="300"
 | 
				
			||||||
 | 
					                aria-pressed={serverMenu}
 | 
				
			||||||
 | 
					                radii="300"
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <Icon src={Icons.ChevronBottom} />
 | 
				
			||||||
 | 
					              </IconButton>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </PopOut>
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/app/pages/auth/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/app/pages/auth/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					export * from './AuthLayout';
 | 
				
			||||||
 | 
					export * from './login';
 | 
				
			||||||
 | 
					export * from './register';
 | 
				
			||||||
 | 
					export * from './reset-password';
 | 
				
			||||||
							
								
								
									
										73
									
								
								src/app/pages/auth/login/Login.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/app/pages/auth/login/Login.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,73 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Box, Text, color } from 'folds';
 | 
				
			||||||
 | 
					import { Link, useSearchParams } from 'react-router-dom';
 | 
				
			||||||
 | 
					import { useAuthFlows } from '../../../hooks/useAuthFlows';
 | 
				
			||||||
 | 
					import { useAuthServer } from '../../../hooks/useAuthServer';
 | 
				
			||||||
 | 
					import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
 | 
				
			||||||
 | 
					import { PasswordLoginForm } from './PasswordLoginForm';
 | 
				
			||||||
 | 
					import { SSOLogin } from '../SSOLogin';
 | 
				
			||||||
 | 
					import { TokenLogin } from './TokenLogin';
 | 
				
			||||||
 | 
					import { OrDivider } from '../OrDivider';
 | 
				
			||||||
 | 
					import { getLoginPath, getRegisterPath } from '../../pathUtils';
 | 
				
			||||||
 | 
					import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin';
 | 
				
			||||||
 | 
					import { LoginPathSearchParams } from '../../paths';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getLoginSearchParams = (searchParams: URLSearchParams): LoginPathSearchParams => ({
 | 
				
			||||||
 | 
					  username: searchParams.get('username') ?? undefined,
 | 
				
			||||||
 | 
					  email: searchParams.get('email') ?? undefined,
 | 
				
			||||||
 | 
					  loginToken: searchParams.get('loginToken') ?? undefined,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Login() {
 | 
				
			||||||
 | 
					  const server = useAuthServer();
 | 
				
			||||||
 | 
					  const { loginFlows } = useAuthFlows();
 | 
				
			||||||
 | 
					  const [searchParams] = useSearchParams();
 | 
				
			||||||
 | 
					  const loginSearchParams = getLoginSearchParams(searchParams);
 | 
				
			||||||
 | 
					  const ssoRedirectUrl = usePathWithOrigin(getLoginPath(server));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const parsedFlows = useParsedLoginFlows(loginFlows.flows);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box direction="Column" gap="500">
 | 
				
			||||||
 | 
					      <Text size="H2" priority="400">
 | 
				
			||||||
 | 
					        Login
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      {parsedFlows.token && loginSearchParams.loginToken && (
 | 
				
			||||||
 | 
					        <TokenLogin token={loginSearchParams.loginToken} />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {parsedFlows.password && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <PasswordLoginForm
 | 
				
			||||||
 | 
					            defaultUsername={loginSearchParams.username}
 | 
				
			||||||
 | 
					            defaultEmail={loginSearchParams.email}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <span data-spacing-node />
 | 
				
			||||||
 | 
					          {parsedFlows.sso && <OrDivider />}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {parsedFlows.sso && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <SSOLogin
 | 
				
			||||||
 | 
					            providers={parsedFlows.sso.identity_providers}
 | 
				
			||||||
 | 
					            redirectUrl={ssoRedirectUrl}
 | 
				
			||||||
 | 
					            asIcons={
 | 
				
			||||||
 | 
					              parsedFlows.password !== undefined && parsedFlows.sso.identity_providers.length > 2
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <span data-spacing-node />
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {!parsedFlows.password && !parsedFlows.sso && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <Text style={{ color: color.Critical.Main }}>
 | 
				
			||||||
 | 
					            {`This client does not support login on "${server}" homeserver. Password and SSO based login method not found.`}
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					          <span data-spacing-node />
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      <Text align="Center">
 | 
				
			||||||
 | 
					        Do not have an account? <Link to={getRegisterPath(server)}>Register</Link>
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										272
									
								
								src/app/pages/auth/login/PasswordLoginForm.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								src/app/pages/auth/login/PasswordLoginForm.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,272 @@
 | 
				
			||||||
 | 
					import React, { FormEventHandler, useCallback, useState } from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Header,
 | 
				
			||||||
 | 
					  Icon,
 | 
				
			||||||
 | 
					  IconButton,
 | 
				
			||||||
 | 
					  Icons,
 | 
				
			||||||
 | 
					  Input,
 | 
				
			||||||
 | 
					  Menu,
 | 
				
			||||||
 | 
					  Overlay,
 | 
				
			||||||
 | 
					  OverlayBackdrop,
 | 
				
			||||||
 | 
					  OverlayCenter,
 | 
				
			||||||
 | 
					  PopOut,
 | 
				
			||||||
 | 
					  Spinner,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  config,
 | 
				
			||||||
 | 
					} from 'folds';
 | 
				
			||||||
 | 
					import FocusTrap from 'focus-trap-react';
 | 
				
			||||||
 | 
					import { Link } from 'react-router-dom';
 | 
				
			||||||
 | 
					import { MatrixError } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix';
 | 
				
			||||||
 | 
					import { EMAIL_REGEX } from '../../../utils/regex';
 | 
				
			||||||
 | 
					import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
 | 
				
			||||||
 | 
					import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { useAuthServer } from '../../../hooks/useAuthServer';
 | 
				
			||||||
 | 
					import { useClientConfig } from '../../../hooks/useClientConfig';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  CustomLoginResponse,
 | 
				
			||||||
 | 
					  LoginError,
 | 
				
			||||||
 | 
					  factoryGetBaseUrl,
 | 
				
			||||||
 | 
					  login,
 | 
				
			||||||
 | 
					  useLoginComplete,
 | 
				
			||||||
 | 
					} from './loginUtil';
 | 
				
			||||||
 | 
					import { PasswordInput } from '../../../components/password-input/PasswordInput';
 | 
				
			||||||
 | 
					import { FieldError } from '../FiledError';
 | 
				
			||||||
 | 
					import { getResetPasswordPath } from '../../pathUtils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function UsernameHint({ server }: { server: string }) {
 | 
				
			||||||
 | 
					  const [open, setOpen] = useState(false);
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <PopOut
 | 
				
			||||||
 | 
					      open={open}
 | 
				
			||||||
 | 
					      position="Top"
 | 
				
			||||||
 | 
					      align="End"
 | 
				
			||||||
 | 
					      content={
 | 
				
			||||||
 | 
					        <FocusTrap
 | 
				
			||||||
 | 
					          focusTrapOptions={{
 | 
				
			||||||
 | 
					            initialFocus: false,
 | 
				
			||||||
 | 
					            onDeactivate: () => setOpen(false),
 | 
				
			||||||
 | 
					            clickOutsideDeactivates: true,
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Menu>
 | 
				
			||||||
 | 
					            <Header size="300" style={{ padding: `0 ${config.space.S200}` }}>
 | 
				
			||||||
 | 
					              <Text size="L400">Hint</Text>
 | 
				
			||||||
 | 
					            </Header>
 | 
				
			||||||
 | 
					            <Box
 | 
				
			||||||
 | 
					              style={{ padding: config.space.S200, paddingTop: 0 }}
 | 
				
			||||||
 | 
					              direction="Column"
 | 
				
			||||||
 | 
					              tabIndex={0}
 | 
				
			||||||
 | 
					              gap="100"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <Text size="T300">
 | 
				
			||||||
 | 
					                <Text as="span" size="Inherit" priority="300">
 | 
				
			||||||
 | 
					                  Username:
 | 
				
			||||||
 | 
					                </Text>{' '}
 | 
				
			||||||
 | 
					                johndoe
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					              <Text size="T300">
 | 
				
			||||||
 | 
					                <Text as="span" size="Inherit" priority="300">
 | 
				
			||||||
 | 
					                  Matrix ID:
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					                {` @johndoe:${server}`}
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					              <Text size="T300">
 | 
				
			||||||
 | 
					                <Text as="span" size="Inherit" priority="300">
 | 
				
			||||||
 | 
					                  Email:
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					                {` johndoe@${server}`}
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					          </Menu>
 | 
				
			||||||
 | 
					        </FocusTrap>
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {(targetRef) => (
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          tabIndex={-1}
 | 
				
			||||||
 | 
					          onClick={() => setOpen(true)}
 | 
				
			||||||
 | 
					          ref={targetRef}
 | 
				
			||||||
 | 
					          type="button"
 | 
				
			||||||
 | 
					          variant="Background"
 | 
				
			||||||
 | 
					          size="300"
 | 
				
			||||||
 | 
					          radii="300"
 | 
				
			||||||
 | 
					          aria-pressed={open}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
 | 
				
			||||||
 | 
					        </IconButton>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </PopOut>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PasswordLoginFormProps = {
 | 
				
			||||||
 | 
					  defaultUsername?: string;
 | 
				
			||||||
 | 
					  defaultEmail?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) {
 | 
				
			||||||
 | 
					  const server = useAuthServer();
 | 
				
			||||||
 | 
					  const clientConfig = useClientConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const serverDiscovery = useAutoDiscoveryInfo();
 | 
				
			||||||
 | 
					  const baseUrl = serverDiscovery['m.homeserver'].base_url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [loginState, startLogin] = useAsyncCallback<
 | 
				
			||||||
 | 
					    CustomLoginResponse,
 | 
				
			||||||
 | 
					    MatrixError,
 | 
				
			||||||
 | 
					    Parameters<typeof login>
 | 
				
			||||||
 | 
					  >(useCallback(login, []));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleUsernameLogin = (username: string, password: string) => {
 | 
				
			||||||
 | 
					    startLogin(baseUrl, {
 | 
				
			||||||
 | 
					      type: 'm.login.password',
 | 
				
			||||||
 | 
					      identifier: {
 | 
				
			||||||
 | 
					        type: 'm.id.user',
 | 
				
			||||||
 | 
					        user: username,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      password,
 | 
				
			||||||
 | 
					      initial_device_display_name: 'Cinny Web',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleMxIdLogin = async (mxId: string, password: string) => {
 | 
				
			||||||
 | 
					    const mxIdServer = getMxIdServer(mxId);
 | 
				
			||||||
 | 
					    const mxIdUsername = getMxIdLocalPart(mxId);
 | 
				
			||||||
 | 
					    if (!mxIdServer || !mxIdUsername) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getBaseUrl = factoryGetBaseUrl(clientConfig, mxIdServer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    startLogin(getBaseUrl, {
 | 
				
			||||||
 | 
					      type: 'm.login.password',
 | 
				
			||||||
 | 
					      identifier: {
 | 
				
			||||||
 | 
					        type: 'm.id.user',
 | 
				
			||||||
 | 
					        user: mxIdUsername,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      password,
 | 
				
			||||||
 | 
					      initial_device_display_name: 'Cinny Web',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  const handleEmailLogin = (email: string, password: string) => {
 | 
				
			||||||
 | 
					    startLogin(baseUrl, {
 | 
				
			||||||
 | 
					      type: 'm.login.password',
 | 
				
			||||||
 | 
					      identifier: {
 | 
				
			||||||
 | 
					        type: 'm.id.thirdparty',
 | 
				
			||||||
 | 
					        medium: 'email',
 | 
				
			||||||
 | 
					        address: email,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      password,
 | 
				
			||||||
 | 
					      initial_device_display_name: 'Cinny Web',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
 | 
				
			||||||
 | 
					    evt.preventDefault();
 | 
				
			||||||
 | 
					    const { usernameInput, passwordInput } = evt.target as HTMLFormElement & {
 | 
				
			||||||
 | 
					      usernameInput: HTMLInputElement;
 | 
				
			||||||
 | 
					      passwordInput: HTMLInputElement;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const username = usernameInput.value.trim();
 | 
				
			||||||
 | 
					    const password = passwordInput.value;
 | 
				
			||||||
 | 
					    if (!username) {
 | 
				
			||||||
 | 
					      usernameInput.focus();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!password) {
 | 
				
			||||||
 | 
					      passwordInput.focus();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isUserId(username)) {
 | 
				
			||||||
 | 
					      handleMxIdLogin(username, password);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (EMAIL_REGEX.test(username)) {
 | 
				
			||||||
 | 
					      handleEmailLogin(username, password);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    handleUsernameLogin(username, password);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
 | 
				
			||||||
 | 
					      <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					        <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					          Username
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					        <Input
 | 
				
			||||||
 | 
					          defaultValue={defaultUsername ?? defaultEmail}
 | 
				
			||||||
 | 
					          style={{ paddingRight: config.space.S300 }}
 | 
				
			||||||
 | 
					          name="usernameInput"
 | 
				
			||||||
 | 
					          variant="Background"
 | 
				
			||||||
 | 
					          size="500"
 | 
				
			||||||
 | 
					          required
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					          after={<UsernameHint server={server} />}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        {loginState.status === AsyncStatus.Error && (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            {loginState.error.errcode === LoginError.ServerNotAllowed && (
 | 
				
			||||||
 | 
					              <FieldError message="Login with custom server not allowed by your client instance." />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {loginState.error.errcode === LoginError.InvalidServer && (
 | 
				
			||||||
 | 
					              <FieldError message="Failed to find your Matrix ID server." />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					      <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					        <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					          Password
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					        <PasswordInput name="passwordInput" variant="Background" size="500" outlined required />
 | 
				
			||||||
 | 
					        <Box alignItems="Start" justifyContent="SpaceBetween" gap="200">
 | 
				
			||||||
 | 
					          {loginState.status === AsyncStatus.Error && (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              {loginState.error.errcode === LoginError.Forbidden && (
 | 
				
			||||||
 | 
					                <FieldError message="Invalid Username or Password." />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {loginState.error.errcode === LoginError.UserDeactivated && (
 | 
				
			||||||
 | 
					                <FieldError message="This account has been deactivated." />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {loginState.error.errcode === LoginError.InvalidRequest && (
 | 
				
			||||||
 | 
					                <FieldError message="Failed to login. Part of your request data is invalid." />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {loginState.error.errcode === LoginError.RateLimited && (
 | 
				
			||||||
 | 
					                <FieldError message="Failed to login. Your login request has been rate-limited by server, Please try after some time." />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              {loginState.error.errcode === LoginError.Unknown && (
 | 
				
			||||||
 | 
					                <FieldError message="Failed to login. Unknown reason." />
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          <Box grow="Yes" shrink="No" justifyContent="End">
 | 
				
			||||||
 | 
					            <Text as="span" size="T200" priority="400" align="Right">
 | 
				
			||||||
 | 
					              <Link to={getResetPasswordPath(server)}>Forget Password?</Link>
 | 
				
			||||||
 | 
					            </Text>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					      <Button type="submit" variant="Primary" size="500">
 | 
				
			||||||
 | 
					        <Text as="span" size="B500">
 | 
				
			||||||
 | 
					          Login
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Overlay
 | 
				
			||||||
 | 
					        open={
 | 
				
			||||||
 | 
					          loginState.status === AsyncStatus.Loading || loginState.status === AsyncStatus.Success
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        backdrop={<OverlayBackdrop />}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <OverlayCenter>
 | 
				
			||||||
 | 
					          <Spinner variant="Secondary" size="600" />
 | 
				
			||||||
 | 
					        </OverlayCenter>
 | 
				
			||||||
 | 
					      </Overlay>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										94
									
								
								src/app/pages/auth/login/TokenLogin.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/app/pages/auth/login/TokenLogin.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,94 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Icon,
 | 
				
			||||||
 | 
					  Icons,
 | 
				
			||||||
 | 
					  Overlay,
 | 
				
			||||||
 | 
					  OverlayBackdrop,
 | 
				
			||||||
 | 
					  OverlayCenter,
 | 
				
			||||||
 | 
					  Spinner,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  color,
 | 
				
			||||||
 | 
					  config,
 | 
				
			||||||
 | 
					} from 'folds';
 | 
				
			||||||
 | 
					import React, { useCallback, useEffect } from 'react';
 | 
				
			||||||
 | 
					import { MatrixError } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
 | 
				
			||||||
 | 
					import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { CustomLoginResponse, LoginError, login, useLoginComplete } from './loginUtil';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function LoginTokenError({ message }: { message: string }) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        backgroundColor: color.Critical.Container,
 | 
				
			||||||
 | 
					        color: color.Critical.OnContainer,
 | 
				
			||||||
 | 
					        padding: config.space.S300,
 | 
				
			||||||
 | 
					        borderRadius: config.radii.R400,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      justifyContent="Start"
 | 
				
			||||||
 | 
					      alignItems="Start"
 | 
				
			||||||
 | 
					      gap="300"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <Icon size="300" filled src={Icons.Warning} />
 | 
				
			||||||
 | 
					      <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					        <Text size="L400">Token Login</Text>
 | 
				
			||||||
 | 
					        <Text size="T300">
 | 
				
			||||||
 | 
					          <b>{message}</b>
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TokenLoginProps = {
 | 
				
			||||||
 | 
					  token: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function TokenLogin({ token }: TokenLoginProps) {
 | 
				
			||||||
 | 
					  const discovery = useAutoDiscoveryInfo();
 | 
				
			||||||
 | 
					  const baseUrl = discovery['m.homeserver'].base_url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [loginState, startLogin] = useAsyncCallback<
 | 
				
			||||||
 | 
					    CustomLoginResponse,
 | 
				
			||||||
 | 
					    MatrixError,
 | 
				
			||||||
 | 
					    Parameters<typeof login>
 | 
				
			||||||
 | 
					  >(useCallback(login, []));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    startLogin(baseUrl, {
 | 
				
			||||||
 | 
					      type: 'm.login.token',
 | 
				
			||||||
 | 
					      token,
 | 
				
			||||||
 | 
					      initial_device_display_name: 'Cinny Web',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, [baseUrl, token, startLogin]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      {loginState.status === AsyncStatus.Error && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          {loginState.error.errcode === LoginError.Forbidden && (
 | 
				
			||||||
 | 
					            <LoginTokenError message="Invalid login token." />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          {loginState.error.errcode === LoginError.UserDeactivated && (
 | 
				
			||||||
 | 
					            <LoginTokenError message="This account has been deactivated." />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          {loginState.error.errcode === LoginError.InvalidRequest && (
 | 
				
			||||||
 | 
					            <LoginTokenError message="Failed to login. Part of your request data is invalid." />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          {loginState.error.errcode === LoginError.RateLimited && (
 | 
				
			||||||
 | 
					            <LoginTokenError message="Failed to login. Your login request has been rate-limited by server, Please try after some time." />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          {loginState.error.errcode === LoginError.Unknown && (
 | 
				
			||||||
 | 
					            <LoginTokenError message="Failed to login. Unknown reason." />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      <Overlay open={loginState.status !== AsyncStatus.Error} backdrop={<OverlayBackdrop />}>
 | 
				
			||||||
 | 
					        <OverlayCenter>
 | 
				
			||||||
 | 
					          <Spinner size="600" variant="Secondary" />
 | 
				
			||||||
 | 
					        </OverlayCenter>
 | 
				
			||||||
 | 
					      </Overlay>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/app/pages/auth/login/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/pages/auth/login/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export * from './Login';
 | 
				
			||||||
							
								
								
									
										118
									
								
								src/app/pages/auth/login/loginUtil.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/app/pages/auth/login/loginUtil.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,118 @@
 | 
				
			||||||
 | 
					import to from 'await-to-js';
 | 
				
			||||||
 | 
					import { LoginRequest, LoginResponse, MatrixError, createClient } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
 | 
					import { useNavigate } from 'react-router-dom';
 | 
				
			||||||
 | 
					import { ClientConfig, clientAllowedServer } from '../../../hooks/useClientConfig';
 | 
				
			||||||
 | 
					import { autoDiscovery, specVersions } from '../../../cs-api';
 | 
				
			||||||
 | 
					import { updateLocalStore } from '../../../../client/action/auth';
 | 
				
			||||||
 | 
					import { ROOT_PATH } from '../../paths';
 | 
				
			||||||
 | 
					import { ErrorCode } from '../../../cs-errorcode';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum GetBaseUrlError {
 | 
				
			||||||
 | 
					  NotAllow = 'NotAllow',
 | 
				
			||||||
 | 
					  NotFound = 'NotFound',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export const factoryGetBaseUrl = (clientConfig: ClientConfig, server: string) => {
 | 
				
			||||||
 | 
					  const getBaseUrl = async (): Promise<string> => {
 | 
				
			||||||
 | 
					    if (!clientAllowedServer(clientConfig, server)) {
 | 
				
			||||||
 | 
					      throw new Error(GetBaseUrlError.NotAllow);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [, discovery] = await to(autoDiscovery(fetch, server));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mxIdBaseUrl: string | undefined;
 | 
				
			||||||
 | 
					    const [, discoveryInfo] = discovery ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (discoveryInfo) {
 | 
				
			||||||
 | 
					      mxIdBaseUrl = discoveryInfo['m.homeserver'].base_url;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!mxIdBaseUrl) {
 | 
				
			||||||
 | 
					      throw new Error(GetBaseUrlError.NotFound);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const [, versions] = await to(specVersions(fetch, mxIdBaseUrl));
 | 
				
			||||||
 | 
					    if (!versions) {
 | 
				
			||||||
 | 
					      throw new Error(GetBaseUrlError.NotFound);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return mxIdBaseUrl;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  return getBaseUrl;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum LoginError {
 | 
				
			||||||
 | 
					  ServerNotAllowed = 'ServerNotAllowed',
 | 
				
			||||||
 | 
					  InvalidServer = 'InvalidServer',
 | 
				
			||||||
 | 
					  Forbidden = 'Forbidden',
 | 
				
			||||||
 | 
					  UserDeactivated = 'UserDeactivated',
 | 
				
			||||||
 | 
					  InvalidRequest = 'InvalidRequest',
 | 
				
			||||||
 | 
					  RateLimited = 'RateLimited',
 | 
				
			||||||
 | 
					  Unknown = 'Unknown',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CustomLoginResponse = {
 | 
				
			||||||
 | 
					  baseUrl: string;
 | 
				
			||||||
 | 
					  response: LoginResponse;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const login = async (
 | 
				
			||||||
 | 
					  serverBaseUrl: string | (() => Promise<string>),
 | 
				
			||||||
 | 
					  data: LoginRequest
 | 
				
			||||||
 | 
					): Promise<CustomLoginResponse> => {
 | 
				
			||||||
 | 
					  const [urlError, url] =
 | 
				
			||||||
 | 
					    typeof serverBaseUrl === 'function' ? await to(serverBaseUrl()) : [undefined, serverBaseUrl];
 | 
				
			||||||
 | 
					  if (urlError) {
 | 
				
			||||||
 | 
					    throw new MatrixError({
 | 
				
			||||||
 | 
					      errcode:
 | 
				
			||||||
 | 
					        urlError.message === GetBaseUrlError.NotAllow
 | 
				
			||||||
 | 
					          ? LoginError.ServerNotAllowed
 | 
				
			||||||
 | 
					          : LoginError.InvalidServer,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mx = createClient({ baseUrl: url });
 | 
				
			||||||
 | 
					  const [err, res] = await to<LoginResponse, MatrixError>(mx.login(data.type, data));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (err) {
 | 
				
			||||||
 | 
					    if (err.httpStatus === 400) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: LoginError.InvalidRequest,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (err.httpStatus === 429) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: LoginError.RateLimited,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (err.errcode === ErrorCode.M_USER_DEACTIVATED) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: LoginError.UserDeactivated,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (err.httpStatus === 403) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: LoginError.Forbidden,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    throw new MatrixError({
 | 
				
			||||||
 | 
					      errcode: LoginError.Unknown,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    baseUrl: url,
 | 
				
			||||||
 | 
					    response: res,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useLoginComplete = (data?: CustomLoginResponse) => {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (data) {
 | 
				
			||||||
 | 
					      const { response: loginRes, baseUrl: loginBaseUrl } = data;
 | 
				
			||||||
 | 
					      updateLocalStore(loginRes.access_token, loginRes.device_id, loginRes.user_id, loginBaseUrl);
 | 
				
			||||||
 | 
					      // TODO: add after login redirect url
 | 
				
			||||||
 | 
					      navigate(ROOT_PATH, { replace: true });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [data, navigate]);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										420
									
								
								src/app/pages/auth/register/PasswordRegisterForm.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								src/app/pages/auth/register/PasswordRegisterForm.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,420 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Checkbox,
 | 
				
			||||||
 | 
					  Input,
 | 
				
			||||||
 | 
					  Overlay,
 | 
				
			||||||
 | 
					  OverlayBackdrop,
 | 
				
			||||||
 | 
					  OverlayCenter,
 | 
				
			||||||
 | 
					  Spinner,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  color,
 | 
				
			||||||
 | 
					} from 'folds';
 | 
				
			||||||
 | 
					import React, { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AuthDict,
 | 
				
			||||||
 | 
					  AuthType,
 | 
				
			||||||
 | 
					  IAuthData,
 | 
				
			||||||
 | 
					  MatrixError,
 | 
				
			||||||
 | 
					  RegisterRequest,
 | 
				
			||||||
 | 
					  UIAFlow,
 | 
				
			||||||
 | 
					  createClient,
 | 
				
			||||||
 | 
					} from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { PasswordInput } from '../../../components/password-input/PasswordInput';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  getLoginTermUrl,
 | 
				
			||||||
 | 
					  getUIAFlowForStages,
 | 
				
			||||||
 | 
					  hasStageInFlows,
 | 
				
			||||||
 | 
					  requiredStageInFlows,
 | 
				
			||||||
 | 
					} from '../../../utils/matrix-uia';
 | 
				
			||||||
 | 
					import { useUIACompleted, useUIAFlow, useUIAParams } from '../../../hooks/useUIAFlows';
 | 
				
			||||||
 | 
					import { AsyncState, AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
 | 
				
			||||||
 | 
					import { RegisterError, RegisterResult, register, useRegisterComplete } from './registerUtil';
 | 
				
			||||||
 | 
					import { FieldError } from '../FiledError';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AutoDummyStageDialog,
 | 
				
			||||||
 | 
					  AutoTermsStageDialog,
 | 
				
			||||||
 | 
					  EmailStageDialog,
 | 
				
			||||||
 | 
					  ReCaptchaStageDialog,
 | 
				
			||||||
 | 
					  RegistrationTokenStageDialog,
 | 
				
			||||||
 | 
					} from '../../../components/uia-stages';
 | 
				
			||||||
 | 
					import { useRegisterEmail } from '../../../hooks/useRegisterEmail';
 | 
				
			||||||
 | 
					import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
 | 
				
			||||||
 | 
					import { UIAFlowOverlay } from '../../../components/UIAFlowOverlay';
 | 
				
			||||||
 | 
					import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../../hooks/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SUPPORTED_REGISTER_STAGES = [
 | 
				
			||||||
 | 
					  AuthType.RegistrationToken,
 | 
				
			||||||
 | 
					  AuthType.Terms,
 | 
				
			||||||
 | 
					  AuthType.Recaptcha,
 | 
				
			||||||
 | 
					  AuthType.Email,
 | 
				
			||||||
 | 
					  AuthType.Dummy,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					type RegisterFormInputs = {
 | 
				
			||||||
 | 
					  usernameInput: HTMLInputElement;
 | 
				
			||||||
 | 
					  passwordInput: HTMLInputElement;
 | 
				
			||||||
 | 
					  confirmPasswordInput: HTMLInputElement;
 | 
				
			||||||
 | 
					  tokenInput?: HTMLInputElement;
 | 
				
			||||||
 | 
					  emailInput?: HTMLInputElement;
 | 
				
			||||||
 | 
					  termsInput?: HTMLInputElement;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FormData = {
 | 
				
			||||||
 | 
					  username: string;
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					  token?: string;
 | 
				
			||||||
 | 
					  email?: string;
 | 
				
			||||||
 | 
					  terms?: boolean;
 | 
				
			||||||
 | 
					  clientSecret: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const pickStages = (uiaFlows: UIAFlow[], formData: FormData): string[] => {
 | 
				
			||||||
 | 
					  const pickedStages: string[] = [];
 | 
				
			||||||
 | 
					  if (formData.token) pickedStages.push(AuthType.RegistrationToken);
 | 
				
			||||||
 | 
					  if (formData.email) pickedStages.push(AuthType.Email);
 | 
				
			||||||
 | 
					  if (formData.terms) pickedStages.push(AuthType.Terms);
 | 
				
			||||||
 | 
					  if (hasStageInFlows(uiaFlows, AuthType.Recaptcha)) {
 | 
				
			||||||
 | 
					    pickedStages.push(AuthType.Recaptcha);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return pickedStages;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RegisterUIAFlowProps = {
 | 
				
			||||||
 | 
					  formData: FormData;
 | 
				
			||||||
 | 
					  flow: UIAFlow;
 | 
				
			||||||
 | 
					  authData: IAuthData;
 | 
				
			||||||
 | 
					  registerEmailState: AsyncState<RequestEmailTokenResponse, MatrixError>;
 | 
				
			||||||
 | 
					  registerEmail: RequestEmailTokenCallback;
 | 
				
			||||||
 | 
					  onRegister: (registerReqData: RegisterRequest) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					function RegisterUIAFlow({
 | 
				
			||||||
 | 
					  formData,
 | 
				
			||||||
 | 
					  flow,
 | 
				
			||||||
 | 
					  authData,
 | 
				
			||||||
 | 
					  registerEmailState,
 | 
				
			||||||
 | 
					  registerEmail,
 | 
				
			||||||
 | 
					  onRegister,
 | 
				
			||||||
 | 
					}: RegisterUIAFlowProps) {
 | 
				
			||||||
 | 
					  const completed = useUIACompleted(authData);
 | 
				
			||||||
 | 
					  const { getStageToComplete } = useUIAFlow(authData, flow);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const stageToComplete = getStageToComplete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleAuthDict = useCallback(
 | 
				
			||||||
 | 
					    (authDict: AuthDict) => {
 | 
				
			||||||
 | 
					      const { password, username } = formData;
 | 
				
			||||||
 | 
					      onRegister({
 | 
				
			||||||
 | 
					        auth: authDict,
 | 
				
			||||||
 | 
					        password,
 | 
				
			||||||
 | 
					        username,
 | 
				
			||||||
 | 
					        initial_device_display_name: 'Cinny Web',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [onRegister, formData]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleCancel = useCallback(() => {
 | 
				
			||||||
 | 
					    window.location.reload();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!stageToComplete) return null;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <UIAFlowOverlay
 | 
				
			||||||
 | 
					      currentStep={completed.length + 1}
 | 
				
			||||||
 | 
					      stepCount={flow.stages.length}
 | 
				
			||||||
 | 
					      onCancel={handleCancel}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {stageToComplete.type === AuthType.RegistrationToken && (
 | 
				
			||||||
 | 
					        <RegistrationTokenStageDialog
 | 
				
			||||||
 | 
					          token={formData.token}
 | 
				
			||||||
 | 
					          stageData={stageToComplete}
 | 
				
			||||||
 | 
					          submitAuthDict={handleAuthDict}
 | 
				
			||||||
 | 
					          onCancel={handleCancel}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {stageToComplete.type === AuthType.Terms && (
 | 
				
			||||||
 | 
					        <AutoTermsStageDialog
 | 
				
			||||||
 | 
					          stageData={stageToComplete}
 | 
				
			||||||
 | 
					          submitAuthDict={handleAuthDict}
 | 
				
			||||||
 | 
					          onCancel={handleCancel}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {stageToComplete.type === AuthType.Recaptcha && (
 | 
				
			||||||
 | 
					        <ReCaptchaStageDialog
 | 
				
			||||||
 | 
					          stageData={stageToComplete}
 | 
				
			||||||
 | 
					          submitAuthDict={handleAuthDict}
 | 
				
			||||||
 | 
					          onCancel={handleCancel}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {stageToComplete.type === AuthType.Email && (
 | 
				
			||||||
 | 
					        <EmailStageDialog
 | 
				
			||||||
 | 
					          email={formData.email}
 | 
				
			||||||
 | 
					          clientSecret={formData.clientSecret}
 | 
				
			||||||
 | 
					          stageData={stageToComplete}
 | 
				
			||||||
 | 
					          requestEmailToken={registerEmail}
 | 
				
			||||||
 | 
					          emailTokenState={registerEmailState}
 | 
				
			||||||
 | 
					          submitAuthDict={handleAuthDict}
 | 
				
			||||||
 | 
					          onCancel={handleCancel}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {stageToComplete.type === AuthType.Dummy && (
 | 
				
			||||||
 | 
					        <AutoDummyStageDialog
 | 
				
			||||||
 | 
					          stageData={stageToComplete}
 | 
				
			||||||
 | 
					          submitAuthDict={handleAuthDict}
 | 
				
			||||||
 | 
					          onCancel={handleCancel}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </UIAFlowOverlay>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PasswordRegisterFormProps = {
 | 
				
			||||||
 | 
					  authData: IAuthData;
 | 
				
			||||||
 | 
					  uiaFlows: UIAFlow[];
 | 
				
			||||||
 | 
					  defaultUsername?: string;
 | 
				
			||||||
 | 
					  defaultEmail?: string;
 | 
				
			||||||
 | 
					  defaultRegisterToken?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function PasswordRegisterForm({
 | 
				
			||||||
 | 
					  authData,
 | 
				
			||||||
 | 
					  uiaFlows,
 | 
				
			||||||
 | 
					  defaultUsername,
 | 
				
			||||||
 | 
					  defaultEmail,
 | 
				
			||||||
 | 
					  defaultRegisterToken,
 | 
				
			||||||
 | 
					}: PasswordRegisterFormProps) {
 | 
				
			||||||
 | 
					  const serverDiscovery = useAutoDiscoveryInfo();
 | 
				
			||||||
 | 
					  const baseUrl = serverDiscovery['m.homeserver'].base_url;
 | 
				
			||||||
 | 
					  const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
 | 
				
			||||||
 | 
					  const params = useUIAParams(authData);
 | 
				
			||||||
 | 
					  const termUrl = getLoginTermUrl(params);
 | 
				
			||||||
 | 
					  const [formData, setFormData] = useState<FormData>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [ongoingFlow, setOngoingFlow] = useState<UIAFlow>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [registerEmailState, registerEmail] = useRegisterEmail(mx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [registerState, handleRegister] = useAsyncCallback<
 | 
				
			||||||
 | 
					    RegisterResult,
 | 
				
			||||||
 | 
					    MatrixError,
 | 
				
			||||||
 | 
					    [RegisterRequest]
 | 
				
			||||||
 | 
					  >(useCallback(async (registerReqData) => register(mx, registerReqData), [mx]));
 | 
				
			||||||
 | 
					  const [ongoingAuthData, customRegisterResp] =
 | 
				
			||||||
 | 
					    registerState.status === AsyncStatus.Success ? registerState.data : [];
 | 
				
			||||||
 | 
					  const registerError =
 | 
				
			||||||
 | 
					    registerState.status === AsyncStatus.Error ? registerState.error : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useRegisterComplete(customRegisterResp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit: ChangeEventHandler<HTMLFormElement> = (evt) => {
 | 
				
			||||||
 | 
					    evt.preventDefault();
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      usernameInput,
 | 
				
			||||||
 | 
					      passwordInput,
 | 
				
			||||||
 | 
					      confirmPasswordInput,
 | 
				
			||||||
 | 
					      emailInput,
 | 
				
			||||||
 | 
					      tokenInput,
 | 
				
			||||||
 | 
					      termsInput,
 | 
				
			||||||
 | 
					    } = evt.target as HTMLFormElement & RegisterFormInputs;
 | 
				
			||||||
 | 
					    const token = tokenInput?.value.trim();
 | 
				
			||||||
 | 
					    const username = usernameInput.value.trim();
 | 
				
			||||||
 | 
					    const password = passwordInput.value;
 | 
				
			||||||
 | 
					    const confirmPassword = confirmPasswordInput.value;
 | 
				
			||||||
 | 
					    if (password !== confirmPassword) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const email = emailInput?.value.trim();
 | 
				
			||||||
 | 
					    const terms = termsInput?.value === 'on';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!username) {
 | 
				
			||||||
 | 
					      usernameInput.focus();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fData: FormData = {
 | 
				
			||||||
 | 
					      username,
 | 
				
			||||||
 | 
					      password,
 | 
				
			||||||
 | 
					      token,
 | 
				
			||||||
 | 
					      email,
 | 
				
			||||||
 | 
					      terms,
 | 
				
			||||||
 | 
					      clientSecret: mx.generateClientSecret(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    const pickedStages = pickStages(uiaFlows, fData);
 | 
				
			||||||
 | 
					    const pickedFlow = getUIAFlowForStages(uiaFlows, pickedStages);
 | 
				
			||||||
 | 
					    setOngoingFlow(pickedFlow);
 | 
				
			||||||
 | 
					    setFormData(fData);
 | 
				
			||||||
 | 
					    handleRegister({
 | 
				
			||||||
 | 
					      username,
 | 
				
			||||||
 | 
					      password,
 | 
				
			||||||
 | 
					      auth: {
 | 
				
			||||||
 | 
					        session: authData.session,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      initial_device_display_name: 'Cinny Web',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
 | 
				
			||||||
 | 
					        <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					          <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					            Username
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					          <Input
 | 
				
			||||||
 | 
					            variant="Background"
 | 
				
			||||||
 | 
					            defaultValue={defaultUsername}
 | 
				
			||||||
 | 
					            name="usernameInput"
 | 
				
			||||||
 | 
					            size="500"
 | 
				
			||||||
 | 
					            outlined
 | 
				
			||||||
 | 
					            required
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          {registerError?.errcode === RegisterError.UserTaken && (
 | 
				
			||||||
 | 
					            <FieldError message="This username is already taken." />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          {registerError?.errcode === RegisterError.UserInvalid && (
 | 
				
			||||||
 | 
					            <FieldError message="This username contains invalid characters." />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          {registerError?.errcode === RegisterError.UserExclusive && (
 | 
				
			||||||
 | 
					            <FieldError message="This username is reserved." />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					        <ConfirmPasswordMatch initialValue>
 | 
				
			||||||
 | 
					          {(match, doMatch, passRef, confPassRef) => (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					                <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					                  Password
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					                <PasswordInput
 | 
				
			||||||
 | 
					                  ref={passRef}
 | 
				
			||||||
 | 
					                  onChange={doMatch}
 | 
				
			||||||
 | 
					                  name="passwordInput"
 | 
				
			||||||
 | 
					                  variant="Background"
 | 
				
			||||||
 | 
					                  size="500"
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  required
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                {registerError?.errcode === RegisterError.PasswordWeak && (
 | 
				
			||||||
 | 
					                  <FieldError
 | 
				
			||||||
 | 
					                    message={
 | 
				
			||||||
 | 
					                      registerError.data.error ??
 | 
				
			||||||
 | 
					                      'Weak Password. Password rejected by server please choosing more strong Password.'
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                {registerError?.errcode === RegisterError.PasswordShort && (
 | 
				
			||||||
 | 
					                  <FieldError
 | 
				
			||||||
 | 
					                    message={
 | 
				
			||||||
 | 
					                      registerError.data.error ??
 | 
				
			||||||
 | 
					                      'Short Password. Password rejected by server please choosing more long Password.'
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </Box>
 | 
				
			||||||
 | 
					              <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					                <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					                  Confirm Password
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					                <PasswordInput
 | 
				
			||||||
 | 
					                  ref={confPassRef}
 | 
				
			||||||
 | 
					                  onChange={doMatch}
 | 
				
			||||||
 | 
					                  name="confirmPasswordInput"
 | 
				
			||||||
 | 
					                  variant="Background"
 | 
				
			||||||
 | 
					                  size="500"
 | 
				
			||||||
 | 
					                  style={{ color: match ? undefined : color.Critical.Main }}
 | 
				
			||||||
 | 
					                  outlined
 | 
				
			||||||
 | 
					                  required
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </Box>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </ConfirmPasswordMatch>
 | 
				
			||||||
 | 
					        {hasStageInFlows(uiaFlows, AuthType.RegistrationToken) && (
 | 
				
			||||||
 | 
					          <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					            <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					              {requiredStageInFlows(uiaFlows, AuthType.RegistrationToken)
 | 
				
			||||||
 | 
					                ? 'Registration Token'
 | 
				
			||||||
 | 
					                : 'Registration Token (Optional)'}
 | 
				
			||||||
 | 
					            </Text>
 | 
				
			||||||
 | 
					            <Input
 | 
				
			||||||
 | 
					              variant="Background"
 | 
				
			||||||
 | 
					              defaultValue={defaultRegisterToken}
 | 
				
			||||||
 | 
					              name="tokenInput"
 | 
				
			||||||
 | 
					              size="500"
 | 
				
			||||||
 | 
					              required={requiredStageInFlows(uiaFlows, AuthType.RegistrationToken)}
 | 
				
			||||||
 | 
					              outlined
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {hasStageInFlows(uiaFlows, AuthType.Email) && (
 | 
				
			||||||
 | 
					          <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					            <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					              {requiredStageInFlows(uiaFlows, AuthType.Email) ? 'Email' : 'Email (Optional)'}
 | 
				
			||||||
 | 
					            </Text>
 | 
				
			||||||
 | 
					            <Input
 | 
				
			||||||
 | 
					              variant="Background"
 | 
				
			||||||
 | 
					              defaultValue={defaultEmail}
 | 
				
			||||||
 | 
					              name="emailInput"
 | 
				
			||||||
 | 
					              type="email"
 | 
				
			||||||
 | 
					              size="500"
 | 
				
			||||||
 | 
					              required={requiredStageInFlows(uiaFlows, AuthType.Email)}
 | 
				
			||||||
 | 
					              outlined
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {hasStageInFlows(uiaFlows, AuthType.Terms) && termUrl && (
 | 
				
			||||||
 | 
					          <Box alignItems="Center" gap="200">
 | 
				
			||||||
 | 
					            <Checkbox name="termsInput" size="300" variant="Primary" required />
 | 
				
			||||||
 | 
					            <Text size="T300">
 | 
				
			||||||
 | 
					              I accept server{' '}
 | 
				
			||||||
 | 
					              <a href={termUrl} target="_blank" rel="noreferrer">
 | 
				
			||||||
 | 
					                Terms and Conditions
 | 
				
			||||||
 | 
					              </a>
 | 
				
			||||||
 | 
					              .
 | 
				
			||||||
 | 
					            </Text>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {registerError?.errcode === RegisterError.RateLimited && (
 | 
				
			||||||
 | 
					          <FieldError message="Failed to register. Your register request has been rate-limited by server, Please try after some time." />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {registerError?.errcode === RegisterError.Forbidden && (
 | 
				
			||||||
 | 
					          <FieldError message="Failed to register. The homeserver does not permit registration." />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {registerError?.errcode === RegisterError.InvalidRequest && (
 | 
				
			||||||
 | 
					          <FieldError message="Failed to register. Invalid request." />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {registerError?.errcode === RegisterError.Unknown && (
 | 
				
			||||||
 | 
					          <FieldError message={registerError.data.error ?? 'Failed to register. Unknown Reason.'} />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        <span data-spacing-node />
 | 
				
			||||||
 | 
					        <Button variant="Primary" size="500" type="submit">
 | 
				
			||||||
 | 
					          <Text as="span" size="B500">
 | 
				
			||||||
 | 
					            Register
 | 
				
			||||||
 | 
					          </Text>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					      {registerState.status === AsyncStatus.Success &&
 | 
				
			||||||
 | 
					        formData &&
 | 
				
			||||||
 | 
					        ongoingFlow &&
 | 
				
			||||||
 | 
					        ongoingAuthData && (
 | 
				
			||||||
 | 
					          <RegisterUIAFlow
 | 
				
			||||||
 | 
					            formData={formData}
 | 
				
			||||||
 | 
					            flow={ongoingFlow}
 | 
				
			||||||
 | 
					            authData={ongoingAuthData}
 | 
				
			||||||
 | 
					            registerEmail={registerEmail}
 | 
				
			||||||
 | 
					            registerEmailState={registerEmailState}
 | 
				
			||||||
 | 
					            onRegister={handleRegister}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      {registerState.status === AsyncStatus.Loading && (
 | 
				
			||||||
 | 
					        <Overlay open backdrop={<OverlayBackdrop />}>
 | 
				
			||||||
 | 
					          <OverlayCenter>
 | 
				
			||||||
 | 
					            <Spinner variant="Secondary" size="600" />
 | 
				
			||||||
 | 
					          </OverlayCenter>
 | 
				
			||||||
 | 
					        </Overlay>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										95
									
								
								src/app/pages/auth/register/Register.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/app/pages/auth/register/Register.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,95 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Box, Text, color } from 'folds';
 | 
				
			||||||
 | 
					import { Link, useSearchParams } from 'react-router-dom';
 | 
				
			||||||
 | 
					import { useAuthServer } from '../../../hooks/useAuthServer';
 | 
				
			||||||
 | 
					import { RegisterFlowStatus, useAuthFlows } from '../../../hooks/useAuthFlows';
 | 
				
			||||||
 | 
					import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows';
 | 
				
			||||||
 | 
					import { PasswordRegisterForm, SUPPORTED_REGISTER_STAGES } from '../register/PasswordRegisterForm';
 | 
				
			||||||
 | 
					import { OrDivider } from '../OrDivider';
 | 
				
			||||||
 | 
					import { SSOLogin } from '../SSOLogin';
 | 
				
			||||||
 | 
					import { SupportedUIAFlowsLoader } from '../../../components/SupportedUIAFlowsLoader';
 | 
				
			||||||
 | 
					import { getLoginPath } from '../../pathUtils';
 | 
				
			||||||
 | 
					import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin';
 | 
				
			||||||
 | 
					import { RegisterPathSearchParams } from '../../paths';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getRegisterSearchParams = (searchParams: URLSearchParams): RegisterPathSearchParams => ({
 | 
				
			||||||
 | 
					  username: searchParams.get('username') ?? undefined,
 | 
				
			||||||
 | 
					  email: searchParams.get('email') ?? undefined,
 | 
				
			||||||
 | 
					  token: searchParams.get('token') ?? undefined,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Register() {
 | 
				
			||||||
 | 
					  const server = useAuthServer();
 | 
				
			||||||
 | 
					  const { loginFlows, registerFlows } = useAuthFlows();
 | 
				
			||||||
 | 
					  const [searchParams] = useSearchParams();
 | 
				
			||||||
 | 
					  const registerSearchParams = getRegisterSearchParams(searchParams);
 | 
				
			||||||
 | 
					  const { sso } = useParsedLoginFlows(loginFlows.flows);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // redirect to /login because only that path handle m.login.token
 | 
				
			||||||
 | 
					  const ssoRedirectUrl = usePathWithOrigin(getLoginPath(server));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box direction="Column" gap="500">
 | 
				
			||||||
 | 
					      <Text size="H2" priority="400">
 | 
				
			||||||
 | 
					        Register
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      {registerFlows.status === RegisterFlowStatus.RegistrationDisabled && !sso && (
 | 
				
			||||||
 | 
					        <Text style={{ color: color.Critical.Main }} size="T300">
 | 
				
			||||||
 | 
					          Registration has been disabled on this homeserver.
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {registerFlows.status === RegisterFlowStatus.RateLimited && !sso && (
 | 
				
			||||||
 | 
					        <Text style={{ color: color.Critical.Main }} size="T300">
 | 
				
			||||||
 | 
					          You have been rate-limited! Please try after some time.
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {registerFlows.status === RegisterFlowStatus.InvalidRequest && !sso && (
 | 
				
			||||||
 | 
					        <Text style={{ color: color.Critical.Main }} size="T300">
 | 
				
			||||||
 | 
					          Invalid Request! Failed to get any registration options.
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {registerFlows.status === RegisterFlowStatus.FlowRequired && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <SupportedUIAFlowsLoader
 | 
				
			||||||
 | 
					            flows={registerFlows.data.flows ?? []}
 | 
				
			||||||
 | 
					            supportedStages={SUPPORTED_REGISTER_STAGES}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {(supportedFlows) =>
 | 
				
			||||||
 | 
					              supportedFlows.length === 0 ? (
 | 
				
			||||||
 | 
					                <Text style={{ color: color.Critical.Main }} size="T300">
 | 
				
			||||||
 | 
					                  This application does not support registration on this homeserver.
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					              ) : (
 | 
				
			||||||
 | 
					                <PasswordRegisterForm
 | 
				
			||||||
 | 
					                  authData={registerFlows.data}
 | 
				
			||||||
 | 
					                  uiaFlows={supportedFlows}
 | 
				
			||||||
 | 
					                  defaultUsername={registerSearchParams.username}
 | 
				
			||||||
 | 
					                  defaultEmail={registerSearchParams.email}
 | 
				
			||||||
 | 
					                  defaultRegisterToken={registerSearchParams.token}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          </SupportedUIAFlowsLoader>
 | 
				
			||||||
 | 
					          <span data-spacing-node />
 | 
				
			||||||
 | 
					          {sso && <OrDivider />}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      {sso && (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <SSOLogin
 | 
				
			||||||
 | 
					            providers={sso.identity_providers}
 | 
				
			||||||
 | 
					            redirectUrl={ssoRedirectUrl}
 | 
				
			||||||
 | 
					            asIcons={
 | 
				
			||||||
 | 
					              registerFlows.status === RegisterFlowStatus.FlowRequired &&
 | 
				
			||||||
 | 
					              sso.identity_providers.length > 2
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					          <span data-spacing-node />
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      <Text align="Center">
 | 
				
			||||||
 | 
					        Already have an account? <Link to={getLoginPath(server)}>Login</Link>
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/app/pages/auth/register/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/pages/auth/register/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export * from './Register';
 | 
				
			||||||
							
								
								
									
										125
									
								
								src/app/pages/auth/register/registerUtil.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/app/pages/auth/register/registerUtil.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,125 @@
 | 
				
			||||||
 | 
					import to from 'await-to-js';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  IAuthData,
 | 
				
			||||||
 | 
					  MatrixClient,
 | 
				
			||||||
 | 
					  MatrixError,
 | 
				
			||||||
 | 
					  RegisterRequest,
 | 
				
			||||||
 | 
					  RegisterResponse,
 | 
				
			||||||
 | 
					} from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
 | 
					import { useNavigate } from 'react-router-dom';
 | 
				
			||||||
 | 
					import { updateLocalStore } from '../../../../client/action/auth';
 | 
				
			||||||
 | 
					import { ROOT_PATH } from '../../paths';
 | 
				
			||||||
 | 
					import { ErrorCode } from '../../../cs-errorcode';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum RegisterError {
 | 
				
			||||||
 | 
					  UserTaken = 'UserTaken',
 | 
				
			||||||
 | 
					  UserInvalid = 'UserInvalid',
 | 
				
			||||||
 | 
					  UserExclusive = 'UserExclusive',
 | 
				
			||||||
 | 
					  PasswordWeak = 'PasswordWeak',
 | 
				
			||||||
 | 
					  PasswordShort = 'PasswordShort',
 | 
				
			||||||
 | 
					  InvalidRequest = 'InvalidRequest',
 | 
				
			||||||
 | 
					  Forbidden = 'Forbidden',
 | 
				
			||||||
 | 
					  RateLimited = 'RateLimited',
 | 
				
			||||||
 | 
					  Unknown = 'Unknown',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CustomRegisterResponse = {
 | 
				
			||||||
 | 
					  baseUrl: string;
 | 
				
			||||||
 | 
					  response: RegisterResponse;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export type RegisterResult = [IAuthData, undefined] | [undefined, CustomRegisterResponse];
 | 
				
			||||||
 | 
					export const register = async (
 | 
				
			||||||
 | 
					  mx: MatrixClient,
 | 
				
			||||||
 | 
					  requestData: RegisterRequest
 | 
				
			||||||
 | 
					): Promise<RegisterResult> => {
 | 
				
			||||||
 | 
					  const [err, res] = await to<RegisterResponse, MatrixError>(mx.registerRequest(requestData));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (err) {
 | 
				
			||||||
 | 
					    if (err.httpStatus === 401) {
 | 
				
			||||||
 | 
					      const authData = err.data as IAuthData;
 | 
				
			||||||
 | 
					      return [authData, undefined];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (err.errcode === ErrorCode.M_USER_IN_USE) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: RegisterError.UserTaken,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (err.errcode === ErrorCode.M_INVALID_USERNAME) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: RegisterError.UserInvalid,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (err.errcode === ErrorCode.M_EXCLUSIVE) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: RegisterError.UserExclusive,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (err.errcode === ErrorCode.M_WEAK_PASSWORD) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: RegisterError.PasswordWeak,
 | 
				
			||||||
 | 
					        error: err.data.error,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (err.errcode === ErrorCode.M_PASSWORD_TOO_SHORT) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: RegisterError.PasswordShort,
 | 
				
			||||||
 | 
					        error: err.data.error,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (err.httpStatus === 429) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: RegisterError.RateLimited,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (err.httpStatus === 400) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: RegisterError.InvalidRequest,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (err.httpStatus === 403) {
 | 
				
			||||||
 | 
					      throw new MatrixError({
 | 
				
			||||||
 | 
					        errcode: RegisterError.Forbidden,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    throw new MatrixError({
 | 
				
			||||||
 | 
					      errcode: RegisterError.Unknown,
 | 
				
			||||||
 | 
					      error: err.data.error,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return [
 | 
				
			||||||
 | 
					    undefined,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      baseUrl: mx.baseUrl,
 | 
				
			||||||
 | 
					      response: res,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useRegisterComplete = (data?: CustomRegisterResponse) => {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (data) {
 | 
				
			||||||
 | 
					      const { response, baseUrl } = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const userId = response.user_id;
 | 
				
			||||||
 | 
					      const accessToken = response.access_token;
 | 
				
			||||||
 | 
					      const deviceId = response.device_id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (accessToken && deviceId) {
 | 
				
			||||||
 | 
					        updateLocalStore(accessToken, deviceId, userId, baseUrl);
 | 
				
			||||||
 | 
					        // TODO: add after register redirect url
 | 
				
			||||||
 | 
					        navigate(ROOT_PATH, { replace: true });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // TODO: navigate to login with userId
 | 
				
			||||||
 | 
					        navigate(ROOT_PATH, { replace: true });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [data, navigate]);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										274
									
								
								src/app/pages/auth/reset-password/PasswordResetForm.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								src/app/pages/auth/reset-password/PasswordResetForm.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,274 @@
 | 
				
			||||||
 | 
					import React, { FormEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Box,
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  Input,
 | 
				
			||||||
 | 
					  Overlay,
 | 
				
			||||||
 | 
					  OverlayBackdrop,
 | 
				
			||||||
 | 
					  OverlayCenter,
 | 
				
			||||||
 | 
					  Spinner,
 | 
				
			||||||
 | 
					  Text,
 | 
				
			||||||
 | 
					  color,
 | 
				
			||||||
 | 
					  config,
 | 
				
			||||||
 | 
					} from 'folds';
 | 
				
			||||||
 | 
					import { useNavigate } from 'react-router-dom';
 | 
				
			||||||
 | 
					import FocusTrap from 'focus-trap-react';
 | 
				
			||||||
 | 
					import { AuthDict, AuthType, MatrixError, createClient } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo';
 | 
				
			||||||
 | 
					import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
 | 
				
			||||||
 | 
					import { useAuthServer } from '../../../hooks/useAuthServer';
 | 
				
			||||||
 | 
					import { usePasswordEmail } from '../../../hooks/usePasswordEmail';
 | 
				
			||||||
 | 
					import { PasswordInput } from '../../../components/password-input/PasswordInput';
 | 
				
			||||||
 | 
					import { ConfirmPasswordMatch } from '../../../components/ConfirmPasswordMatch';
 | 
				
			||||||
 | 
					import { FieldError } from '../FiledError';
 | 
				
			||||||
 | 
					import { UIAFlowOverlay } from '../../../components/UIAFlowOverlay';
 | 
				
			||||||
 | 
					import { EmailStageDialog } from '../../../components/uia-stages';
 | 
				
			||||||
 | 
					import { ResetPasswordResult, resetPassword } from './resetPasswordUtil';
 | 
				
			||||||
 | 
					import { getLoginPath, withSearchParam } from '../../pathUtils';
 | 
				
			||||||
 | 
					import { LoginPathSearchParams } from '../../paths';
 | 
				
			||||||
 | 
					import { getUIAError, getUIAErrorCode } from '../../../utils/matrix-uia';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FormData = {
 | 
				
			||||||
 | 
					  email: string;
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					  clientSecret: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function ResetPasswordComplete({ email }: { email?: string }) {
 | 
				
			||||||
 | 
					  const server = useAuthServer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleClick = () => {
 | 
				
			||||||
 | 
					    const path = getLoginPath(server);
 | 
				
			||||||
 | 
					    if (email) {
 | 
				
			||||||
 | 
					      navigate(withSearchParam<LoginPathSearchParams>(path, { email }));
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    navigate(path);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Overlay open backdrop={<OverlayBackdrop />}>
 | 
				
			||||||
 | 
					      <OverlayCenter>
 | 
				
			||||||
 | 
					        <FocusTrap>
 | 
				
			||||||
 | 
					          <Dialog>
 | 
				
			||||||
 | 
					            <Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
 | 
				
			||||||
 | 
					              <Text>
 | 
				
			||||||
 | 
					                Password has been reset successfully. Please login with your new password.
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					              <Button variant="Primary" onClick={handleClick}>
 | 
				
			||||||
 | 
					                <Text size="B400" as="span">
 | 
				
			||||||
 | 
					                  Login
 | 
				
			||||||
 | 
					                </Text>
 | 
				
			||||||
 | 
					              </Button>
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					          </Dialog>
 | 
				
			||||||
 | 
					        </FocusTrap>
 | 
				
			||||||
 | 
					      </OverlayCenter>
 | 
				
			||||||
 | 
					    </Overlay>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PasswordResetFormProps = {
 | 
				
			||||||
 | 
					  defaultEmail?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) {
 | 
				
			||||||
 | 
					  const server = useAuthServer();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const serverDiscovery = useAutoDiscoveryInfo();
 | 
				
			||||||
 | 
					  const baseUrl = serverDiscovery['m.homeserver'].base_url;
 | 
				
			||||||
 | 
					  const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [formData, setFormData] = useState<FormData>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [passwordEmailState, passwordEmail] = usePasswordEmail(mx);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [resetPasswordState, handleResetPassword] = useAsyncCallback<
 | 
				
			||||||
 | 
					    ResetPasswordResult,
 | 
				
			||||||
 | 
					    MatrixError,
 | 
				
			||||||
 | 
					    [AuthDict, string]
 | 
				
			||||||
 | 
					  >(useCallback(async (authDict, newPassword) => resetPassword(mx, authDict, newPassword), [mx]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [ongoingAuthData, resetPasswordResult] =
 | 
				
			||||||
 | 
					    resetPasswordState.status === AsyncStatus.Success ? resetPasswordState.data : [];
 | 
				
			||||||
 | 
					  const resetPasswordError =
 | 
				
			||||||
 | 
					    resetPasswordState.status === AsyncStatus.Error ? resetPasswordState.error : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const flowErrorCode = ongoingAuthData && getUIAErrorCode(ongoingAuthData);
 | 
				
			||||||
 | 
					  const flowError = ongoingAuthData && getUIAError(ongoingAuthData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let waitingToVerifyEmail = true;
 | 
				
			||||||
 | 
					  if (resetPasswordResult) waitingToVerifyEmail = false;
 | 
				
			||||||
 | 
					  if (ongoingAuthData && flowErrorCode === undefined) waitingToVerifyEmail = false;
 | 
				
			||||||
 | 
					  if (resetPasswordError) waitingToVerifyEmail = false;
 | 
				
			||||||
 | 
					  if (resetPasswordState.status === AsyncStatus.Loading) waitingToVerifyEmail = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // We only support UIA m.login.password stage for reset password
 | 
				
			||||||
 | 
					  // So we will assume to process it as soon as
 | 
				
			||||||
 | 
					  // we have 401 with no error on initial request.
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (formData && ongoingAuthData && !flowErrorCode) {
 | 
				
			||||||
 | 
					      handleResetPassword(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: AuthType.Password,
 | 
				
			||||||
 | 
					          identifier: {
 | 
				
			||||||
 | 
					            type: 'm.id.thirdparty',
 | 
				
			||||||
 | 
					            medium: 'email',
 | 
				
			||||||
 | 
					            address: formData.email,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          password: formData.password,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        formData.password
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [ongoingAuthData, flowErrorCode, formData, handleResetPassword]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
 | 
				
			||||||
 | 
					    evt.preventDefault();
 | 
				
			||||||
 | 
					    const { emailInput, passwordInput, confirmPasswordInput } = evt.target as HTMLFormElement & {
 | 
				
			||||||
 | 
					      emailInput: HTMLInputElement;
 | 
				
			||||||
 | 
					      passwordInput: HTMLInputElement;
 | 
				
			||||||
 | 
					      confirmPasswordInput: HTMLInputElement;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const email = emailInput.value.trim();
 | 
				
			||||||
 | 
					    const password = passwordInput.value;
 | 
				
			||||||
 | 
					    const confirmPassword = confirmPasswordInput.value;
 | 
				
			||||||
 | 
					    if (!email) {
 | 
				
			||||||
 | 
					      emailInput.focus();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (password !== confirmPassword) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const clientSecret = mx.generateClientSecret();
 | 
				
			||||||
 | 
					    passwordEmail(email, clientSecret);
 | 
				
			||||||
 | 
					    setFormData({
 | 
				
			||||||
 | 
					      email,
 | 
				
			||||||
 | 
					      password,
 | 
				
			||||||
 | 
					      clientSecret,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleCancel = () => {
 | 
				
			||||||
 | 
					    window.location.reload();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleSubmitRequest = useCallback(
 | 
				
			||||||
 | 
					    (authDict: AuthDict) => {
 | 
				
			||||||
 | 
					      if (!formData) return;
 | 
				
			||||||
 | 
					      const { password } = formData;
 | 
				
			||||||
 | 
					      handleResetPassword(authDict, password);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [formData, handleResetPassword]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box as="form" onSubmit={handleSubmit} direction="Inherit" gap="400">
 | 
				
			||||||
 | 
					      <Text size="T300" priority="400">
 | 
				
			||||||
 | 
					        Homeserver <strong>{server}</strong> will send you an email to let you reset your password.
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					        <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					          Email
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					        <Input
 | 
				
			||||||
 | 
					          defaultValue={defaultEmail}
 | 
				
			||||||
 | 
					          type="email"
 | 
				
			||||||
 | 
					          name="emailInput"
 | 
				
			||||||
 | 
					          variant="Background"
 | 
				
			||||||
 | 
					          size="500"
 | 
				
			||||||
 | 
					          required
 | 
				
			||||||
 | 
					          outlined
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        {passwordEmailState.status === AsyncStatus.Error && (
 | 
				
			||||||
 | 
					          <FieldError
 | 
				
			||||||
 | 
					            message={`${passwordEmailState.error.errcode}: ${passwordEmailState.error.data?.error}`}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </Box>
 | 
				
			||||||
 | 
					      <ConfirmPasswordMatch initialValue>
 | 
				
			||||||
 | 
					        {(match, doMatch, passRef, confPassRef) => (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					              <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					                New Password
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					              <PasswordInput
 | 
				
			||||||
 | 
					                ref={passRef}
 | 
				
			||||||
 | 
					                onChange={doMatch}
 | 
				
			||||||
 | 
					                name="passwordInput"
 | 
				
			||||||
 | 
					                variant="Background"
 | 
				
			||||||
 | 
					                size="500"
 | 
				
			||||||
 | 
					                outlined
 | 
				
			||||||
 | 
					                required
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					            <Box direction="Column" gap="100">
 | 
				
			||||||
 | 
					              <Text as="label" size="L400" priority="300">
 | 
				
			||||||
 | 
					                Confirm Password
 | 
				
			||||||
 | 
					              </Text>
 | 
				
			||||||
 | 
					              <PasswordInput
 | 
				
			||||||
 | 
					                ref={confPassRef}
 | 
				
			||||||
 | 
					                onChange={doMatch}
 | 
				
			||||||
 | 
					                name="confirmPasswordInput"
 | 
				
			||||||
 | 
					                variant="Background"
 | 
				
			||||||
 | 
					                size="500"
 | 
				
			||||||
 | 
					                style={{ color: match ? undefined : color.Critical.Main }}
 | 
				
			||||||
 | 
					                outlined
 | 
				
			||||||
 | 
					                required
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Box>
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </ConfirmPasswordMatch>
 | 
				
			||||||
 | 
					      {resetPasswordError && (
 | 
				
			||||||
 | 
					        <FieldError
 | 
				
			||||||
 | 
					          message={`${resetPasswordError.errcode}: ${
 | 
				
			||||||
 | 
					            resetPasswordError.data?.error ?? 'Failed to reset password.'
 | 
				
			||||||
 | 
					          }`}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					      <span data-spacing-node />
 | 
				
			||||||
 | 
					      <Button type="submit" variant="Primary" size="500">
 | 
				
			||||||
 | 
					        <Text as="span" size="B500">
 | 
				
			||||||
 | 
					          Reset Password
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {resetPasswordResult && <ResetPasswordComplete email={formData?.email} />}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {passwordEmailState.status === AsyncStatus.Success && formData && waitingToVerifyEmail && (
 | 
				
			||||||
 | 
					        <UIAFlowOverlay currentStep={1} stepCount={1} onCancel={handleCancel}>
 | 
				
			||||||
 | 
					          <EmailStageDialog
 | 
				
			||||||
 | 
					            stageData={{
 | 
				
			||||||
 | 
					              type: AuthType.Email,
 | 
				
			||||||
 | 
					              errorCode: flowErrorCode,
 | 
				
			||||||
 | 
					              error: flowError,
 | 
				
			||||||
 | 
					              session: ongoingAuthData?.session,
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            submitAuthDict={handleSubmitRequest}
 | 
				
			||||||
 | 
					            email={formData.email}
 | 
				
			||||||
 | 
					            clientSecret={formData.clientSecret}
 | 
				
			||||||
 | 
					            requestEmailToken={passwordEmail}
 | 
				
			||||||
 | 
					            emailTokenState={passwordEmailState}
 | 
				
			||||||
 | 
					            onCancel={handleCancel}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </UIAFlowOverlay>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Overlay
 | 
				
			||||||
 | 
					        open={
 | 
				
			||||||
 | 
					          passwordEmailState.status === AsyncStatus.Loading ||
 | 
				
			||||||
 | 
					          resetPasswordState.status === AsyncStatus.Loading
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        backdrop={<OverlayBackdrop />}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <OverlayCenter>
 | 
				
			||||||
 | 
					          <Spinner variant="Secondary" size="600" />
 | 
				
			||||||
 | 
					        </OverlayCenter>
 | 
				
			||||||
 | 
					      </Overlay>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										36
									
								
								src/app/pages/auth/reset-password/ResetPassword.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/app/pages/auth/reset-password/ResetPassword.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					import { Box, Text } from 'folds';
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Link, useSearchParams } from 'react-router-dom';
 | 
				
			||||||
 | 
					import { getLoginPath } from '../../pathUtils';
 | 
				
			||||||
 | 
					import { useAuthServer } from '../../../hooks/useAuthServer';
 | 
				
			||||||
 | 
					import { PasswordResetForm } from './PasswordResetForm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ResetPasswordSearchParams = {
 | 
				
			||||||
 | 
					  email?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getResetPasswordSearchParams = (
 | 
				
			||||||
 | 
					  searchParams: URLSearchParams
 | 
				
			||||||
 | 
					): ResetPasswordSearchParams => ({
 | 
				
			||||||
 | 
					  email: searchParams.get('email') ?? undefined,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ResetPassword() {
 | 
				
			||||||
 | 
					  const server = useAuthServer();
 | 
				
			||||||
 | 
					  const [searchParams] = useSearchParams();
 | 
				
			||||||
 | 
					  const resetPasswordSearchParams = getResetPasswordSearchParams(searchParams);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Box direction="Column" gap="500">
 | 
				
			||||||
 | 
					      <Text size="H2" priority="400">
 | 
				
			||||||
 | 
					        Reset Password
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					      <PasswordResetForm defaultEmail={resetPasswordSearchParams.email} />
 | 
				
			||||||
 | 
					      <span data-spacing-node />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Text align="Center">
 | 
				
			||||||
 | 
					        Remember your password? <Link to={getLoginPath(server)}>Login</Link>
 | 
				
			||||||
 | 
					      </Text>
 | 
				
			||||||
 | 
					    </Box>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/app/pages/auth/reset-password/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/pages/auth/reset-password/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export * from './ResetPassword';
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/app/pages/auth/reset-password/resetPasswordUtil.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/app/pages/auth/reset-password/resetPasswordUtil.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					import to from 'await-to-js';
 | 
				
			||||||
 | 
					import { AuthDict, IAuthData, MatrixClient, MatrixError } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ResetPasswordResponse = Record<string, never>;
 | 
				
			||||||
 | 
					export type ResetPasswordResult = [IAuthData, undefined] | [undefined, ResetPasswordResponse];
 | 
				
			||||||
 | 
					export const resetPassword = async (
 | 
				
			||||||
 | 
					  mx: MatrixClient,
 | 
				
			||||||
 | 
					  authDict: AuthDict,
 | 
				
			||||||
 | 
					  newPassword: string
 | 
				
			||||||
 | 
					): Promise<ResetPasswordResult> => {
 | 
				
			||||||
 | 
					  const [err, res] = await to<ResetPasswordResponse, MatrixError>(
 | 
				
			||||||
 | 
					    mx.setPassword(authDict, newPassword, false)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (err) {
 | 
				
			||||||
 | 
					    if (err.httpStatus === 401) {
 | 
				
			||||||
 | 
					      const authData = err.data as IAuthData;
 | 
				
			||||||
 | 
					      return [authData, undefined];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return [undefined, res];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										53
									
								
								src/app/pages/auth/styles.css.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/app/pages/auth/styles.css.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					import { style } from '@vanilla-extract/css';
 | 
				
			||||||
 | 
					import { DefaultReset, color, config, toRem } from 'folds';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AuthLayout = style({
 | 
				
			||||||
 | 
					  minHeight: '100%',
 | 
				
			||||||
 | 
					  backgroundColor: color.Background.Container,
 | 
				
			||||||
 | 
					  color: color.Background.OnContainer,
 | 
				
			||||||
 | 
					  padding: config.space.S400,
 | 
				
			||||||
 | 
					  paddingRight: config.space.S200,
 | 
				
			||||||
 | 
					  paddingBottom: 0,
 | 
				
			||||||
 | 
					  position: 'relative',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AuthCard = style({
 | 
				
			||||||
 | 
					  marginTop: '1vh',
 | 
				
			||||||
 | 
					  maxWidth: toRem(460),
 | 
				
			||||||
 | 
					  width: '100%',
 | 
				
			||||||
 | 
					  backgroundColor: color.Surface.Container,
 | 
				
			||||||
 | 
					  color: color.Surface.OnContainer,
 | 
				
			||||||
 | 
					  borderRadius: config.radii.R400,
 | 
				
			||||||
 | 
					  boxShadow: config.shadow.E100,
 | 
				
			||||||
 | 
					  border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
 | 
				
			||||||
 | 
					  overflow: 'hidden',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AuthLogo = style([
 | 
				
			||||||
 | 
					  DefaultReset,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    width: toRem(26),
 | 
				
			||||||
 | 
					    height: toRem(26),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    borderRadius: '50%',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AuthHeader = style({
 | 
				
			||||||
 | 
					  padding: `0 ${config.space.S400}`,
 | 
				
			||||||
 | 
					  borderBottomWidth: config.borderWidth.B300,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AuthCardContent = style({
 | 
				
			||||||
 | 
					  maxWidth: toRem(402),
 | 
				
			||||||
 | 
					  width: '100%',
 | 
				
			||||||
 | 
					  margin: 'auto',
 | 
				
			||||||
 | 
					  padding: config.space.S400,
 | 
				
			||||||
 | 
					  paddingTop: config.space.S700,
 | 
				
			||||||
 | 
					  paddingBottom: toRem(44),
 | 
				
			||||||
 | 
					  gap: toRem(44),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const AuthFooter = style({
 | 
				
			||||||
 | 
					  padding: config.space.S200,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										28
									
								
								src/app/pages/pathUtils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/app/pages/pathUtils.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,28 @@
 | 
				
			||||||
 | 
					import { generatePath } from 'react-router-dom';
 | 
				
			||||||
 | 
					import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH, ROOT_PATH } from './paths';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const withSearchParam = <T extends Record<string, string>>(
 | 
				
			||||||
 | 
					  path: string,
 | 
				
			||||||
 | 
					  searchParam: T
 | 
				
			||||||
 | 
					): string => {
 | 
				
			||||||
 | 
					  const params = new URLSearchParams(searchParam);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return `${path}?${params}`;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getRootPath = (): string => ROOT_PATH;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getLoginPath = (server?: string): string => {
 | 
				
			||||||
 | 
					  const params = server ? { server: encodeURIComponent(server) } : undefined;
 | 
				
			||||||
 | 
					  return generatePath(LOGIN_PATH, params);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getRegisterPath = (server?: string): string => {
 | 
				
			||||||
 | 
					  const params = server ? { server: encodeURIComponent(server) } : undefined;
 | 
				
			||||||
 | 
					  return generatePath(REGISTER_PATH, params);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getResetPasswordPath = (server?: string): string => {
 | 
				
			||||||
 | 
					  const params = server ? { server: encodeURIComponent(server) } : undefined;
 | 
				
			||||||
 | 
					  return generatePath(RESET_PASSWORD_PATH, params);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/app/pages/paths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/app/pages/paths.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					export const ROOT_PATH = '/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type LoginPathSearchParams = {
 | 
				
			||||||
 | 
					  username?: string;
 | 
				
			||||||
 | 
					  email?: string;
 | 
				
			||||||
 | 
					  loginToken?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const LOGIN_PATH = '/login/:server?/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type RegisterPathSearchParams = {
 | 
				
			||||||
 | 
					  username?: string;
 | 
				
			||||||
 | 
					  email?: string;
 | 
				
			||||||
 | 
					  token?: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const REGISTER_PATH = '/register/:server?/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const RESET_PASSWORD_PATH = '/reset-password/:server?/';
 | 
				
			||||||
| 
						 | 
					@ -1,28 +1,26 @@
 | 
				
			||||||
import { useAtomValue, WritableAtom } from 'jotai';
 | 
					import { useAtomValue } from 'jotai';
 | 
				
			||||||
import { selectAtom } from 'jotai/utils';
 | 
					import { selectAtom } from 'jotai/utils';
 | 
				
			||||||
import { MatrixClient } from 'matrix-js-sdk';
 | 
					import { MatrixClient } from 'matrix-js-sdk';
 | 
				
			||||||
import { useCallback } from 'react';
 | 
					import { useCallback } from 'react';
 | 
				
			||||||
import { isDirectInvite, isRoom, isSpace, isUnsupportedRoom } from '../../utils/room';
 | 
					import { isDirectInvite, isRoom, isSpace, isUnsupportedRoom } from '../../utils/room';
 | 
				
			||||||
import { compareRoomsEqual, RoomsAction } from '../utils';
 | 
					import { compareRoomsEqual } from '../utils';
 | 
				
			||||||
import { MDirectAction } from '../mDirectList';
 | 
					import { mDirectAtom } from '../mDirectList';
 | 
				
			||||||
 | 
					import { allInvitesAtom } from '../inviteList';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useSpaceInvites = (
 | 
					export const useSpaceInvites = (mx: MatrixClient, invitesAtom: typeof allInvitesAtom) => {
 | 
				
			||||||
  mx: MatrixClient,
 | 
					 | 
				
			||||||
  allInvitesAtom: WritableAtom<string[], RoomsAction>
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
  const selector = useCallback(
 | 
					  const selector = useCallback(
 | 
				
			||||||
    (rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))),
 | 
					    (rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))),
 | 
				
			||||||
    [mx]
 | 
					    [mx]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
 | 
					  return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useRoomInvites = (
 | 
					export const useRoomInvites = (
 | 
				
			||||||
  mx: MatrixClient,
 | 
					  mx: MatrixClient,
 | 
				
			||||||
  allInvitesAtom: WritableAtom<string[], RoomsAction>,
 | 
					  invitesAtom: typeof allInvitesAtom,
 | 
				
			||||||
  mDirectAtom: WritableAtom<Set<string>, MDirectAction>
 | 
					  directAtom: typeof mDirectAtom
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const mDirects = useAtomValue(mDirectAtom);
 | 
					  const mDirects = useAtomValue(directAtom);
 | 
				
			||||||
  const selector = useCallback(
 | 
					  const selector = useCallback(
 | 
				
			||||||
    (rooms: string[]) =>
 | 
					    (rooms: string[]) =>
 | 
				
			||||||
      rooms.filter(
 | 
					      rooms.filter(
 | 
				
			||||||
| 
						 | 
					@ -32,15 +30,15 @@ export const useRoomInvites = (
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    [mx, mDirects]
 | 
					    [mx, mDirects]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
 | 
					  return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useDirectInvites = (
 | 
					export const useDirectInvites = (
 | 
				
			||||||
  mx: MatrixClient,
 | 
					  mx: MatrixClient,
 | 
				
			||||||
  allInvitesAtom: WritableAtom<string[], RoomsAction>,
 | 
					  invitesAtom: typeof allInvitesAtom,
 | 
				
			||||||
  mDirectAtom: WritableAtom<Set<string>, MDirectAction>
 | 
					  directAtom: typeof mDirectAtom
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const mDirects = useAtomValue(mDirectAtom);
 | 
					  const mDirects = useAtomValue(directAtom);
 | 
				
			||||||
  const selector = useCallback(
 | 
					  const selector = useCallback(
 | 
				
			||||||
    (rooms: string[]) =>
 | 
					    (rooms: string[]) =>
 | 
				
			||||||
      rooms.filter(
 | 
					      rooms.filter(
 | 
				
			||||||
| 
						 | 
					@ -48,16 +46,13 @@ export const useDirectInvites = (
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    [mx, mDirects]
 | 
					    [mx, mDirects]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
 | 
					  return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useUnsupportedInvites = (
 | 
					export const useUnsupportedInvites = (mx: MatrixClient, invitesAtom: typeof allInvitesAtom) => {
 | 
				
			||||||
  mx: MatrixClient,
 | 
					 | 
				
			||||||
  allInvitesAtom: WritableAtom<string[], RoomsAction>
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
  const selector = useCallback(
 | 
					  const selector = useCallback(
 | 
				
			||||||
    (rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))),
 | 
					    (rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))),
 | 
				
			||||||
    [mx]
 | 
					    [mx]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return useAtomValue(selectAtom(allInvitesAtom, selector, compareRoomsEqual));
 | 
					  return useAtomValue(selectAtom(invitesAtom, selector, compareRoomsEqual));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,54 +1,52 @@
 | 
				
			||||||
import { useAtomValue, WritableAtom } from 'jotai';
 | 
					import { useAtomValue } from 'jotai';
 | 
				
			||||||
import { selectAtom } from 'jotai/utils';
 | 
					import { selectAtom } from 'jotai/utils';
 | 
				
			||||||
import { MatrixClient } from 'matrix-js-sdk';
 | 
					import { MatrixClient } from 'matrix-js-sdk';
 | 
				
			||||||
import { useCallback } from 'react';
 | 
					import { useCallback } from 'react';
 | 
				
			||||||
import { isRoom, isSpace, isUnsupportedRoom } from '../../utils/room';
 | 
					import { isRoom, isSpace, isUnsupportedRoom } from '../../utils/room';
 | 
				
			||||||
import { compareRoomsEqual, RoomsAction } from '../utils';
 | 
					import { compareRoomsEqual } from '../utils';
 | 
				
			||||||
import { MDirectAction } from '../mDirectList';
 | 
					import { mDirectAtom } from '../mDirectList';
 | 
				
			||||||
 | 
					import { allRoomsAtom } from '../roomList';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useSpaces = (mx: MatrixClient, allRoomsAtom: WritableAtom<string[], RoomsAction>) => {
 | 
					export const useSpaces = (mx: MatrixClient, roomsAtom: typeof allRoomsAtom) => {
 | 
				
			||||||
  const selector = useCallback(
 | 
					  const selector = useCallback(
 | 
				
			||||||
    (rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))),
 | 
					    (rooms: string[]) => rooms.filter((roomId) => isSpace(mx.getRoom(roomId))),
 | 
				
			||||||
    [mx]
 | 
					    [mx]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual));
 | 
					  return useAtomValue(selectAtom(roomsAtom, selector, compareRoomsEqual));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useRooms = (
 | 
					export const useRooms = (
 | 
				
			||||||
  mx: MatrixClient,
 | 
					  mx: MatrixClient,
 | 
				
			||||||
  allRoomsAtom: WritableAtom<string[], RoomsAction>,
 | 
					  roomsAtom: typeof allRoomsAtom,
 | 
				
			||||||
  mDirectAtom: WritableAtom<Set<string>, MDirectAction>
 | 
					  directAtom: typeof mDirectAtom
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const mDirects = useAtomValue(mDirectAtom);
 | 
					  const mDirects = useAtomValue(directAtom);
 | 
				
			||||||
  const selector = useCallback(
 | 
					  const selector = useCallback(
 | 
				
			||||||
    (rooms: string[]) =>
 | 
					    (rooms: string[]) =>
 | 
				
			||||||
      rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && !mDirects.has(roomId)),
 | 
					      rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && !mDirects.has(roomId)),
 | 
				
			||||||
    [mx, mDirects]
 | 
					    [mx, mDirects]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual));
 | 
					  return useAtomValue(selectAtom(roomsAtom, selector, compareRoomsEqual));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useDirects = (
 | 
					export const useDirects = (
 | 
				
			||||||
  mx: MatrixClient,
 | 
					  mx: MatrixClient,
 | 
				
			||||||
  allRoomsAtom: WritableAtom<string[], RoomsAction>,
 | 
					  roomsAtom: typeof allRoomsAtom,
 | 
				
			||||||
  mDirectAtom: WritableAtom<Set<string>, MDirectAction>
 | 
					  directAtom: typeof mDirectAtom
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const mDirects = useAtomValue(mDirectAtom);
 | 
					  const mDirects = useAtomValue(directAtom);
 | 
				
			||||||
  const selector = useCallback(
 | 
					  const selector = useCallback(
 | 
				
			||||||
    (rooms: string[]) =>
 | 
					    (rooms: string[]) =>
 | 
				
			||||||
      rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && mDirects.has(roomId)),
 | 
					      rooms.filter((roomId) => isRoom(mx.getRoom(roomId)) && mDirects.has(roomId)),
 | 
				
			||||||
    [mx, mDirects]
 | 
					    [mx, mDirects]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual));
 | 
					  return useAtomValue(selectAtom(roomsAtom, selector, compareRoomsEqual));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useUnsupportedRooms = (
 | 
					export const useUnsupportedRooms = (mx: MatrixClient, roomsAtom: typeof allRoomsAtom) => {
 | 
				
			||||||
  mx: MatrixClient,
 | 
					 | 
				
			||||||
  allRoomsAtom: WritableAtom<string[], RoomsAction>
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
  const selector = useCallback(
 | 
					  const selector = useCallback(
 | 
				
			||||||
    (rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))),
 | 
					    (rooms: string[]) => rooms.filter((roomId) => isUnsupportedRoom(mx.getRoom(roomId))),
 | 
				
			||||||
    [mx]
 | 
					    [mx]
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
  return useAtomValue(selectAtom(allRoomsAtom, selector, compareRoomsEqual));
 | 
					  return useAtomValue(selectAtom(roomsAtom, selector, compareRoomsEqual));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,16 @@
 | 
				
			||||||
import { atom, useAtomValue, useSetAtom, WritableAtom } from 'jotai';
 | 
					import { atom, useAtomValue, useSetAtom } from 'jotai';
 | 
				
			||||||
import { SetAtom } from 'jotai/core/atom';
 | 
					 | 
				
			||||||
import { selectAtom } from 'jotai/utils';
 | 
					import { selectAtom } from 'jotai/utils';
 | 
				
			||||||
import { useMemo } from 'react';
 | 
					import { useMemo } from 'react';
 | 
				
			||||||
import { Settings } from '../settings';
 | 
					import { Settings, settingsAtom as sAtom } from '../settings';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useSetSetting = <K extends keyof Settings>(
 | 
					export type SettingSetter<K extends keyof Settings> =
 | 
				
			||||||
  settingsAtom: WritableAtom<Settings, Settings>,
 | 
					  | Settings[K]
 | 
				
			||||||
  key: K
 | 
					  | ((s: Settings[K]) => Settings[K]);
 | 
				
			||||||
) => {
 | 
					
 | 
				
			||||||
 | 
					export const useSetSetting = <K extends keyof Settings>(settingsAtom: typeof sAtom, key: K) => {
 | 
				
			||||||
  const setterAtom = useMemo(
 | 
					  const setterAtom = useMemo(
 | 
				
			||||||
    () =>
 | 
					    () =>
 | 
				
			||||||
      atom<null, Settings[K] | ((s: Settings[K]) => Settings[K])>(null, (get, set, value) => {
 | 
					      atom<null, [SettingSetter<K>], undefined>(null, (get, set, value) => {
 | 
				
			||||||
        const s = { ...get(settingsAtom) };
 | 
					        const s = { ...get(settingsAtom) };
 | 
				
			||||||
        s[key] = typeof value === 'function' ? value(s[key]) : value;
 | 
					        s[key] = typeof value === 'function' ? value(s[key]) : value;
 | 
				
			||||||
        set(settingsAtom, s);
 | 
					        set(settingsAtom, s);
 | 
				
			||||||
| 
						 | 
					@ -22,9 +22,9 @@ export const useSetSetting = <K extends keyof Settings>(
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useSetting = <K extends keyof Settings>(
 | 
					export const useSetting = <K extends keyof Settings>(
 | 
				
			||||||
  settingsAtom: WritableAtom<Settings, Settings>,
 | 
					  settingsAtom: typeof sAtom,
 | 
				
			||||||
  key: K
 | 
					  key: K
 | 
				
			||||||
): [Settings[K], SetAtom<Settings[K] | ((s: Settings[K]) => Settings[K]), void>] => {
 | 
					): [Settings[K], ReturnType<typeof useSetSetting<K>>] => {
 | 
				
			||||||
  const selector = useMemo(() => (s: Settings) => s[key], [key]);
 | 
					  const selector = useMemo(() => (s: Settings) => s[key], [key]);
 | 
				
			||||||
  const setting = useAtomValue(selectAtom(settingsAtom, selector));
 | 
					  const setting = useAtomValue(selectAtom(settingsAtom, selector));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ import { Membership } from '../../types/matrix/room';
 | 
				
			||||||
import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils';
 | 
					import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseRoomsAtom = atom<string[]>([]);
 | 
					const baseRoomsAtom = atom<string[]>([]);
 | 
				
			||||||
export const allInvitesAtom = atom<string[], RoomsAction>(
 | 
					export const allInvitesAtom = atom<string[], [RoomsAction], undefined>(
 | 
				
			||||||
  (get) => get(baseRoomsAtom),
 | 
					  (get) => get(baseRoomsAtom),
 | 
				
			||||||
  (get, set, action) => {
 | 
					  (get, set, action) => {
 | 
				
			||||||
    if (action.type === 'INITIALIZE') {
 | 
					    if (action.type === 'INITIALIZE') {
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ export const allInvitesAtom = atom<string[], RoomsAction>(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useBindAllInvitesAtom = (
 | 
					export const useBindAllInvitesAtom = (
 | 
				
			||||||
  mx: MatrixClient,
 | 
					  mx: MatrixClient,
 | 
				
			||||||
  allRooms: WritableAtom<string[], RoomsAction>
 | 
					  allRooms: WritableAtom<string[], [RoomsAction], undefined>
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  useBindRoomsWithMembershipsAtom(
 | 
					  useBindRoomsWithMembershipsAtom(
 | 
				
			||||||
    mx,
 | 
					    mx,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ export type ListAction<T> =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createListAtom = <T>() => {
 | 
					export const createListAtom = <T>() => {
 | 
				
			||||||
  const baseListAtom = atom<T[]>([]);
 | 
					  const baseListAtom = atom<T[]>([]);
 | 
				
			||||||
  return atom<T[], ListAction<T>>(
 | 
					  return atom<T[], [ListAction<T>], undefined>(
 | 
				
			||||||
    (get) => get(baseListAtom),
 | 
					    (get) => get(baseListAtom),
 | 
				
			||||||
    (get, set, action) => {
 | 
					    (get, set, action) => {
 | 
				
			||||||
      const items = get(baseListAtom);
 | 
					      const items = get(baseListAtom);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { atom, useSetAtom, WritableAtom } from 'jotai';
 | 
					import { atom, useSetAtom } from 'jotai';
 | 
				
			||||||
import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
 | 
					import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
 | 
				
			||||||
import { useEffect } from 'react';
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
import { AccountDataEvent } from '../../types/matrix/accountData';
 | 
					import { AccountDataEvent } from '../../types/matrix/accountData';
 | 
				
			||||||
| 
						 | 
					@ -10,17 +10,14 @@ export type MDirectAction = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseMDirectAtom = atom(new Set<string>());
 | 
					const baseMDirectAtom = atom(new Set<string>());
 | 
				
			||||||
export const mDirectAtom = atom<Set<string>, MDirectAction>(
 | 
					export const mDirectAtom = atom<Set<string>, [MDirectAction], undefined>(
 | 
				
			||||||
  (get) => get(baseMDirectAtom),
 | 
					  (get) => get(baseMDirectAtom),
 | 
				
			||||||
  (get, set, action) => {
 | 
					  (get, set, action) => {
 | 
				
			||||||
    set(baseMDirectAtom, action.rooms);
 | 
					    set(baseMDirectAtom, action.rooms);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useBindMDirectAtom = (
 | 
					export const useBindMDirectAtom = (mx: MatrixClient, mDirect: typeof mDirectAtom) => {
 | 
				
			||||||
  mx: MatrixClient,
 | 
					 | 
				
			||||||
  mDirect: WritableAtom<Set<string>, MDirectAction>
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
  const setMDirect = useSetAtom(mDirect);
 | 
					  const setMDirect = useSetAtom(mDirect);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { atom, WritableAtom, useSetAtom } from 'jotai';
 | 
					import { atom, useSetAtom } from 'jotai';
 | 
				
			||||||
import { ClientEvent, IPushRule, IPushRules, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
 | 
					import { ClientEvent, IPushRule, IPushRules, MatrixClient, MatrixEvent } from 'matrix-js-sdk';
 | 
				
			||||||
import { useEffect } from 'react';
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
import { MuteChanges } from '../../types/matrix/room';
 | 
					import { MuteChanges } from '../../types/matrix/room';
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export const muteChangesAtom = atom<MuteChanges>({
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseMutedRoomsAtom = atom(new Set<string>());
 | 
					const baseMutedRoomsAtom = atom(new Set<string>());
 | 
				
			||||||
export const mutedRoomsAtom = atom<Set<string>, MutedRoomsUpdate>(
 | 
					export const mutedRoomsAtom = atom<Set<string>, [MutedRoomsUpdate], undefined>(
 | 
				
			||||||
  (get) => get(baseMutedRoomsAtom),
 | 
					  (get) => get(baseMutedRoomsAtom),
 | 
				
			||||||
  (get, set, action) => {
 | 
					  (get, set, action) => {
 | 
				
			||||||
    const mutedRooms = new Set([...get(mutedRoomsAtom)]);
 | 
					    const mutedRooms = new Set([...get(mutedRoomsAtom)]);
 | 
				
			||||||
| 
						 | 
					@ -45,10 +45,7 @@ export const mutedRoomsAtom = atom<Set<string>, MutedRoomsUpdate>(
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useBindMutedRoomsAtom = (
 | 
					export const useBindMutedRoomsAtom = (mx: MatrixClient, mutedAtom: typeof mutedRoomsAtom) => {
 | 
				
			||||||
  mx: MatrixClient,
 | 
					 | 
				
			||||||
  mutedAtom: WritableAtom<Set<string>, MutedRoomsUpdate>
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
  const setMuted = useSetAtom(mutedAtom);
 | 
					  const setMuted = useSetAtom(mutedAtom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,11 @@
 | 
				
			||||||
import { atom, WritableAtom } from 'jotai';
 | 
					import { atom } from 'jotai';
 | 
				
			||||||
import { MatrixClient } from 'matrix-js-sdk';
 | 
					import { MatrixClient } from 'matrix-js-sdk';
 | 
				
			||||||
import { useMemo } from 'react';
 | 
					import { useMemo } from 'react';
 | 
				
			||||||
import { Membership } from '../../types/matrix/room';
 | 
					import { Membership } from '../../types/matrix/room';
 | 
				
			||||||
import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils';
 | 
					import { RoomsAction, useBindRoomsWithMembershipsAtom } from './utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseRoomsAtom = atom<string[]>([]);
 | 
					const baseRoomsAtom = atom<string[]>([]);
 | 
				
			||||||
export const allRoomsAtom = atom<string[], RoomsAction>(
 | 
					export const allRoomsAtom = atom<string[], [RoomsAction], undefined>(
 | 
				
			||||||
  (get) => get(baseRoomsAtom),
 | 
					  (get) => get(baseRoomsAtom),
 | 
				
			||||||
  (get, set, action) => {
 | 
					  (get, set, action) => {
 | 
				
			||||||
    if (action.type === 'INITIALIZE') {
 | 
					    if (action.type === 'INITIALIZE') {
 | 
				
			||||||
| 
						 | 
					@ -19,10 +19,7 @@ export const allRoomsAtom = atom<string[], RoomsAction>(
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
export const useBindAllRoomsAtom = (
 | 
					export const useBindAllRoomsAtom = (mx: MatrixClient, allRooms: typeof allRoomsAtom) => {
 | 
				
			||||||
  mx: MatrixClient,
 | 
					 | 
				
			||||||
  allRooms: WritableAtom<string[], RoomsAction>
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
  useBindRoomsWithMembershipsAtom(
 | 
					  useBindRoomsWithMembershipsAtom(
 | 
				
			||||||
    mx,
 | 
					    mx,
 | 
				
			||||||
    allRooms,
 | 
					    allRooms,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import produce from 'immer';
 | 
					import produce from 'immer';
 | 
				
			||||||
import { atom, useSetAtom, WritableAtom } from 'jotai';
 | 
					import { atom, useSetAtom } from 'jotai';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ClientEvent,
 | 
					  ClientEvent,
 | 
				
			||||||
  MatrixClient,
 | 
					  MatrixClient,
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ export type RoomToParentsAction =
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseRoomToParents = atom<RoomToParents>(new Map());
 | 
					const baseRoomToParents = atom<RoomToParents>(new Map());
 | 
				
			||||||
export const roomToParentsAtom = atom<RoomToParents, RoomToParentsAction>(
 | 
					export const roomToParentsAtom = atom<RoomToParents, [RoomToParentsAction], undefined>(
 | 
				
			||||||
  (get) => get(baseRoomToParents),
 | 
					  (get) => get(baseRoomToParents),
 | 
				
			||||||
  (get, set, action) => {
 | 
					  (get, set, action) => {
 | 
				
			||||||
    if (action.type === 'INITIALIZE') {
 | 
					    if (action.type === 'INITIALIZE') {
 | 
				
			||||||
| 
						 | 
					@ -69,7 +69,7 @@ export const roomToParentsAtom = atom<RoomToParents, RoomToParentsAction>(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useBindRoomToParentsAtom = (
 | 
					export const useBindRoomToParentsAtom = (
 | 
				
			||||||
  mx: MatrixClient,
 | 
					  mx: MatrixClient,
 | 
				
			||||||
  roomToParents: WritableAtom<RoomToParents, RoomToParentsAction>
 | 
					  roomToParents: typeof roomToParentsAtom
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const setRoomToParents = useSetAtom(roomToParents);
 | 
					  const setRoomToParents = useSetAtom(roomToParents);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import produce from 'immer';
 | 
					import produce from 'immer';
 | 
				
			||||||
import { atom, useSetAtom, PrimitiveAtom, WritableAtom, useAtomValue } from 'jotai';
 | 
					import { atom, useSetAtom, PrimitiveAtom, useAtomValue } from 'jotai';
 | 
				
			||||||
import { IRoomTimelineData, MatrixClient, MatrixEvent, Room, RoomEvent } from 'matrix-js-sdk';
 | 
					import { IRoomTimelineData, MatrixClient, MatrixEvent, Room, RoomEvent } from 'matrix-js-sdk';
 | 
				
			||||||
import { ReceiptContent, ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts';
 | 
					import { ReceiptContent, ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts';
 | 
				
			||||||
import { useEffect } from 'react';
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
| 
						 | 
					@ -82,7 +82,7 @@ const deleteUnreadInfo = (roomToUnread: RoomToUnread, allParents: Set<string>, r
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseRoomToUnread = atom<RoomToUnread>(new Map());
 | 
					const baseRoomToUnread = atom<RoomToUnread>(new Map());
 | 
				
			||||||
export const roomToUnreadAtom = atom<RoomToUnread, RoomToUnreadAction>(
 | 
					export const roomToUnreadAtom = atom<RoomToUnread, [RoomToUnreadAction], undefined>(
 | 
				
			||||||
  (get) => get(baseRoomToUnread),
 | 
					  (get) => get(baseRoomToUnread),
 | 
				
			||||||
  (get, set, action) => {
 | 
					  (get, set, action) => {
 | 
				
			||||||
    if (action.type === 'RESET') {
 | 
					    if (action.type === 'RESET') {
 | 
				
			||||||
| 
						 | 
					@ -127,7 +127,7 @@ export const roomToUnreadAtom = atom<RoomToUnread, RoomToUnreadAction>(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useBindRoomToUnreadAtom = (
 | 
					export const useBindRoomToUnreadAtom = (
 | 
				
			||||||
  mx: MatrixClient,
 | 
					  mx: MatrixClient,
 | 
				
			||||||
  unreadAtom: WritableAtom<RoomToUnread, RoomToUnreadAction>,
 | 
					  unreadAtom: typeof roomToUnreadAtom,
 | 
				
			||||||
  muteChangesAtom: PrimitiveAtom<MuteChanges>
 | 
					  muteChangesAtom: PrimitiveAtom<MuteChanges>
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const setUnreadAtom = useSetAtom(unreadAtom);
 | 
					  const setUnreadAtom = useSetAtom(unreadAtom);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										129
									
								
								src/app/state/sessions.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/app/state/sessions.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,129 @@
 | 
				
			||||||
 | 
					import { atom } from 'jotai';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  atomWithLocalStorage,
 | 
				
			||||||
 | 
					  getLocalStorageItem,
 | 
				
			||||||
 | 
					  setLocalStorageItem,
 | 
				
			||||||
 | 
					} from './utils/atomWithLocalStorage';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Session = {
 | 
				
			||||||
 | 
					  baseUrl: string;
 | 
				
			||||||
 | 
					  userId: string;
 | 
				
			||||||
 | 
					  deviceId: string;
 | 
				
			||||||
 | 
					  accessToken: string;
 | 
				
			||||||
 | 
					  expiresInMs?: number;
 | 
				
			||||||
 | 
					  refreshToken?: string;
 | 
				
			||||||
 | 
					  fallbackSdkStores?: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Sessions = Session[];
 | 
				
			||||||
 | 
					export type SessionStoreName = {
 | 
				
			||||||
 | 
					  sync: string;
 | 
				
			||||||
 | 
					  crypto: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Migration code for old session
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const FALLBACK_STORE_NAME: SessionStoreName = {
 | 
				
			||||||
 | 
					  sync: 'web-sync-store',
 | 
				
			||||||
 | 
					  crypto: 'crypto-store',
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const removeFallbackSession = () => {
 | 
				
			||||||
 | 
					  localStorage.removeItem('cinny_hs_base_url');
 | 
				
			||||||
 | 
					  localStorage.removeItem('cinny_user_id');
 | 
				
			||||||
 | 
					  localStorage.removeItem('cinny_device_id');
 | 
				
			||||||
 | 
					  localStorage.removeItem('cinny_access_token');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const getFallbackSession = (): Session | undefined => {
 | 
				
			||||||
 | 
					  const baseUrl = localStorage.getItem('cinny_hs_base_url');
 | 
				
			||||||
 | 
					  const userId = localStorage.getItem('cinny_user_id');
 | 
				
			||||||
 | 
					  const deviceId = localStorage.getItem('cinny_device_id');
 | 
				
			||||||
 | 
					  const accessToken = localStorage.getItem('cinny_access_token');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (baseUrl && userId && deviceId && accessToken) {
 | 
				
			||||||
 | 
					    const session: Session = {
 | 
				
			||||||
 | 
					      baseUrl,
 | 
				
			||||||
 | 
					      userId,
 | 
				
			||||||
 | 
					      deviceId,
 | 
				
			||||||
 | 
					      accessToken,
 | 
				
			||||||
 | 
					      fallbackSdkStores: true,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return session;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * End of migration code for old session
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getSessionStoreName = (session: Session): SessionStoreName => {
 | 
				
			||||||
 | 
					  if (session.fallbackSdkStores) {
 | 
				
			||||||
 | 
					    return FALLBACK_STORE_NAME;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    sync: `sync${session.userId}`,
 | 
				
			||||||
 | 
					    crypto: `crypto${session.userId}`,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MATRIX_SESSIONS_KEY = 'matrixSessions';
 | 
				
			||||||
 | 
					const baseSessionsAtom = atomWithLocalStorage<Sessions>(
 | 
				
			||||||
 | 
					  MATRIX_SESSIONS_KEY,
 | 
				
			||||||
 | 
					  (key) => {
 | 
				
			||||||
 | 
					    const defaultSessions: Sessions = [];
 | 
				
			||||||
 | 
					    const sessions = getLocalStorageItem(key, defaultSessions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Before multi account support session was stored
 | 
				
			||||||
 | 
					    // as multiple item in local storage.
 | 
				
			||||||
 | 
					    // So we need these migration code.
 | 
				
			||||||
 | 
					    const fallbackSession = getFallbackSession();
 | 
				
			||||||
 | 
					    if (fallbackSession) {
 | 
				
			||||||
 | 
					      removeFallbackSession();
 | 
				
			||||||
 | 
					      sessions.push(fallbackSession);
 | 
				
			||||||
 | 
					      setLocalStorageItem(key, sessions);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return sessions;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  (key, value) => {
 | 
				
			||||||
 | 
					    setLocalStorageItem(key, value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SessionsAction =
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: 'PUT';
 | 
				
			||||||
 | 
					      session: Session;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  | {
 | 
				
			||||||
 | 
					      type: 'DELETE';
 | 
				
			||||||
 | 
					      session: Session;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sessionsAtom = atom<Sessions, [SessionsAction], undefined>(
 | 
				
			||||||
 | 
					  (get) => get(baseSessionsAtom),
 | 
				
			||||||
 | 
					  (get, set, action) => {
 | 
				
			||||||
 | 
					    if (action.type === 'PUT') {
 | 
				
			||||||
 | 
					      const sessions = [...get(baseSessionsAtom)];
 | 
				
			||||||
 | 
					      const sessionIndex = sessions.findIndex(
 | 
				
			||||||
 | 
					        (session) => session.userId === action.session.userId
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (sessionIndex === -1) {
 | 
				
			||||||
 | 
					        sessions.push(action.session);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        sessions.splice(sessionIndex, 1, action.session);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      set(baseSessionsAtom, sessions);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (action.type === 'DELETE') {
 | 
				
			||||||
 | 
					      const sessions = get(baseSessionsAtom).filter(
 | 
				
			||||||
 | 
					        (session) => session.userId !== action.session.userId
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      set(baseSessionsAtom, sessions);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
| 
						 | 
					@ -64,7 +64,7 @@ export const setSettings = (settings: Settings) => {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseSettings = atom<Settings>(getSettings());
 | 
					const baseSettings = atom<Settings>(getSettings());
 | 
				
			||||||
export const settingsAtom = atom<Settings, Settings>(
 | 
					export const settingsAtom = atom<Settings, [Settings], undefined>(
 | 
				
			||||||
  (get) => get(baseSettings),
 | 
					  (get) => get(baseSettings),
 | 
				
			||||||
  (get, set, update) => {
 | 
					  (get, set, update) => {
 | 
				
			||||||
    set(baseSettings, update);
 | 
					    set(baseSettings, update);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ type TabToRoomAction = {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseTabToRoom = atom<TabToRoom>(new Map());
 | 
					const baseTabToRoom = atom<TabToRoom>(new Map());
 | 
				
			||||||
export const tabToRoomAtom = atom<TabToRoom, TabToRoomAction>(
 | 
					export const tabToRoomAtom = atom<TabToRoom, [TabToRoomAction], undefined>(
 | 
				
			||||||
  (get) => get(baseTabToRoom),
 | 
					  (get) => get(baseTabToRoom),
 | 
				
			||||||
  (get, set, action) => {
 | 
					  (get, set, action) => {
 | 
				
			||||||
    if (action.type === 'PUT') {
 | 
					    if (action.type === 'PUT') {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,11 @@ export type IRoomIdToTypingMembersAction =
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const baseRoomIdToTypingMembersAtom = atom<IRoomIdToTypingMembers>(new Map());
 | 
					const baseRoomIdToTypingMembersAtom = atom<IRoomIdToTypingMembers>(new Map());
 | 
				
			||||||
export const roomIdToTypingMembersAtom = atom<IRoomIdToTypingMembers, IRoomIdToTypingMembersAction>(
 | 
					export const roomIdToTypingMembersAtom = atom<
 | 
				
			||||||
 | 
					  IRoomIdToTypingMembers,
 | 
				
			||||||
 | 
					  [IRoomIdToTypingMembersAction],
 | 
				
			||||||
 | 
					  undefined
 | 
				
			||||||
 | 
					>(
 | 
				
			||||||
  (get) => get(baseRoomIdToTypingMembersAtom),
 | 
					  (get) => get(baseRoomIdToTypingMembersAtom),
 | 
				
			||||||
  (get, set, action) => {
 | 
					  (get, set, action) => {
 | 
				
			||||||
    const roomIdToTypingMembers = get(baseRoomIdToTypingMembersAtom);
 | 
					    const roomIdToTypingMembers = get(baseRoomIdToTypingMembersAtom);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ export const createUploadAtom = (file: TUploadContent) => {
 | 
				
			||||||
    file,
 | 
					    file,
 | 
				
			||||||
    status: UploadStatus.Idle,
 | 
					    status: UploadStatus.Idle,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  return atom<Upload, UploadAtomAction>(
 | 
					  return atom<Upload, [UploadAtomAction], undefined>(
 | 
				
			||||||
    (get) => get(baseUploadAtom),
 | 
					    (get) => get(baseUploadAtom),
 | 
				
			||||||
    (get, set, update) => {
 | 
					    (get, set, update) => {
 | 
				
			||||||
      const uploadState = get(baseUploadAtom);
 | 
					      const uploadState = get(baseUploadAtom);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ export type RoomsAction =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useBindRoomsWithMembershipsAtom = (
 | 
					export const useBindRoomsWithMembershipsAtom = (
 | 
				
			||||||
  mx: MatrixClient,
 | 
					  mx: MatrixClient,
 | 
				
			||||||
  roomsAtom: WritableAtom<string[], RoomsAction>,
 | 
					  roomsAtom: WritableAtom<string[], [RoomsAction], undefined>,
 | 
				
			||||||
  memberships: Membership[]
 | 
					  memberships: Membership[]
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  const setRoomsAtom = useSetAtom(roomsAtom);
 | 
					  const setRoomsAtom = useSetAtom(roomsAtom);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										51
									
								
								src/app/state/utils/atomWithLocalStorage.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/app/state/utils/atomWithLocalStorage.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,51 @@
 | 
				
			||||||
 | 
					import { atom } from 'jotai';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getLocalStorageItem = <T>(key: string, defaultValue: T): T => {
 | 
				
			||||||
 | 
					  const item = localStorage.getItem(key);
 | 
				
			||||||
 | 
					  if (item === null) return defaultValue;
 | 
				
			||||||
 | 
					  if (item === 'undefined') return undefined as T;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    return JSON.parse(item) as T;
 | 
				
			||||||
 | 
					  } catch {
 | 
				
			||||||
 | 
					    return defaultValue;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const setLocalStorageItem = <T>(key: string, value: T) => {
 | 
				
			||||||
 | 
					  localStorage.setItem(key, JSON.stringify(value));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type GetLocalStorageItem<T> = (key: string) => T;
 | 
				
			||||||
 | 
					export type SetLocalStorageItem<T> = (key: string, value: T) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const atomWithLocalStorage = <T>(
 | 
				
			||||||
 | 
					  key: string,
 | 
				
			||||||
 | 
					  getItem: GetLocalStorageItem<T>,
 | 
				
			||||||
 | 
					  setItem: SetLocalStorageItem<T>
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  const value = getItem(key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const baseAtom = atom<T>(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  baseAtom.onMount = (setAtom) => {
 | 
				
			||||||
 | 
					    const handleChange = (evt: StorageEvent) => {
 | 
				
			||||||
 | 
					      if (evt.key !== key) return;
 | 
				
			||||||
 | 
					      setAtom(getItem(key));
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.addEventListener('storage', handleChange);
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      window.removeEventListener('storage', handleChange);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const localStorageAtom = atom<T, [T], undefined>(
 | 
				
			||||||
 | 
					    (get) => get(baseAtom),
 | 
				
			||||||
 | 
					    (get, set, newValue) => {
 | 
				
			||||||
 | 
					      set(baseAtom, newValue);
 | 
				
			||||||
 | 
					      setItem(key, newValue);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return localStorageAtom;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/app/styles/Patterns.css.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/app/styles/Patterns.css.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					import { style } from '@vanilla-extract/css';
 | 
				
			||||||
 | 
					import { color, toRem } from 'folds';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const BackgroundDotPattern = style({
 | 
				
			||||||
 | 
					  backgroundImage: `radial-gradient(${color.Background.ContainerActive} ${toRem(2)}, ${
 | 
				
			||||||
 | 
					    color.Background.Container
 | 
				
			||||||
 | 
					  } ${toRem(2)})`,
 | 
				
			||||||
 | 
					  backgroundSize: `${toRem(40)} ${toRem(40)}`,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -44,6 +44,17 @@ export const fulfilledPromiseSettledResult = <T>(prs: PromiseSettledResult<T>[])
 | 
				
			||||||
    return values;
 | 
					    return values;
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const promiseFulfilledResult = <T>(
 | 
				
			||||||
 | 
					  settledResult: PromiseSettledResult<T>
 | 
				
			||||||
 | 
					): T | undefined => {
 | 
				
			||||||
 | 
					  if (settledResult.status === 'fulfilled') return settledResult.value;
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const promiseRejectedResult = <T>(settledResult: PromiseSettledResult<T>): any => {
 | 
				
			||||||
 | 
					  if (settledResult.status === 'rejected') return settledResult.reason;
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const binarySearch = <T>(items: T[], match: (item: T) => -1 | 0 | 1): T | undefined => {
 | 
					export const binarySearch = <T>(items: T[], match: (item: T) => -1 | 0 | 1): T | undefined => {
 | 
				
			||||||
  const search = (start: number, end: number): T | undefined => {
 | 
					  const search = (start: number, end: number): T | undefined => {
 | 
				
			||||||
    if (start > end) return undefined;
 | 
					    if (start > end) return undefined;
 | 
				
			||||||
| 
						 | 
					@ -77,3 +88,10 @@ export const parseGeoUri = (location: string) => {
 | 
				
			||||||
    longitude,
 | 
					    longitude,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const START_SLASHES_REG = /^\/+/g;
 | 
				
			||||||
 | 
					const END_SLASHES_REG = /\/+$/g;
 | 
				
			||||||
 | 
					export const trimLeadingSlash = (str: string): string => str.replace(START_SLASHES_REG, '');
 | 
				
			||||||
 | 
					export const trimTrailingSlash = (str: string): string => str.replace(END_SLASHES_REG, '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const trimSlash = (str: string): string => trimLeadingSlash(trimTrailingSlash(str));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										84
									
								
								src/app/utils/matrix-uia.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/app/utils/matrix-uia.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,84 @@
 | 
				
			||||||
 | 
					import { AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getSupportedUIAFlows = (uiaFlows: UIAFlow[], supportedStages: string[]): UIAFlow[] => {
 | 
				
			||||||
 | 
					  const supportedUIAFlows = uiaFlows.filter((flow) =>
 | 
				
			||||||
 | 
					    flow.stages.every((stage) => supportedStages.includes(stage))
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return supportedUIAFlows;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getUIACompleted = (authData: IAuthData): string[] => {
 | 
				
			||||||
 | 
					  const completed = authData.completed ?? [];
 | 
				
			||||||
 | 
					  return completed;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type UIAParams = Record<string, Record<string, unknown>>;
 | 
				
			||||||
 | 
					export const getUIAParams = (authData: IAuthData): UIAParams => {
 | 
				
			||||||
 | 
					  const params = authData.params ?? {};
 | 
				
			||||||
 | 
					  return params;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getUIASession = (authData: IAuthData): string | undefined => {
 | 
				
			||||||
 | 
					  const session = authData.session ?? undefined;
 | 
				
			||||||
 | 
					  return session;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getUIAErrorCode = (authData: IAuthData): string | undefined => {
 | 
				
			||||||
 | 
					  const errorCode =
 | 
				
			||||||
 | 
					    'errcode' in authData && typeof authData.errcode === 'string' ? authData.errcode : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return errorCode;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getUIAError = (authData: IAuthData): string | undefined => {
 | 
				
			||||||
 | 
					  const errorCode =
 | 
				
			||||||
 | 
					    'error' in authData && typeof authData.error === 'string' ? authData.error : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return errorCode;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getUIAFlowForStages = (uiaFlows: UIAFlow[], stages: string[]): UIAFlow | undefined => {
 | 
				
			||||||
 | 
					  const matchedFlows = uiaFlows
 | 
				
			||||||
 | 
					    .filter((flow) => {
 | 
				
			||||||
 | 
					      if (flow.stages.length < stages.length) return false;
 | 
				
			||||||
 | 
					      if (flow.stages.length > stages.length) {
 | 
				
			||||||
 | 
					        // As a valid flow can also have m.login.dummy type,
 | 
				
			||||||
 | 
					        // we will pick one extra length flow only if it has dummy
 | 
				
			||||||
 | 
					        if (flow.stages.length > stages.length + 1) return false;
 | 
				
			||||||
 | 
					        if (stages.includes(AuthType.Dummy)) return false;
 | 
				
			||||||
 | 
					        if (flow.stages.includes(AuthType.Dummy)) return true;
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .filter((flow) => stages.every((stage) => flow.stages.includes(stage)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (matchedFlows.length === 0) return undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  matchedFlows.sort((a, b) => a.stages.length - b.stages.length);
 | 
				
			||||||
 | 
					  return matchedFlows[0];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const hasStageInFlows = (uiaFlows: UIAFlow[], stage: string) =>
 | 
				
			||||||
 | 
					  uiaFlows.some((flow) => flow.stages.includes(stage));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const requiredStageInFlows = (uiaFlows: UIAFlow[], stage: string) =>
 | 
				
			||||||
 | 
					  uiaFlows.every((flow) => flow.stages.includes(stage));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getLoginTermUrl = (params: UIAParams): string | undefined => {
 | 
				
			||||||
 | 
					  const terms = params[AuthType.Terms];
 | 
				
			||||||
 | 
					  if (terms && 'policies' in terms && typeof terms.policies === 'object') {
 | 
				
			||||||
 | 
					    if (terms.policies === null) return undefined;
 | 
				
			||||||
 | 
					    if ('privacy_policy' in terms.policies && typeof terms.policies.privacy_policy === 'object') {
 | 
				
			||||||
 | 
					      if (terms.policies.privacy_policy === null) return undefined;
 | 
				
			||||||
 | 
					      const langToPolicy = terms.policies.privacy_policy as Record<string, any>;
 | 
				
			||||||
 | 
					      const url = langToPolicy.en?.url;
 | 
				
			||||||
 | 
					      if (typeof url === 'string') return url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const firstKey = Object.keys(langToPolicy)[0];
 | 
				
			||||||
 | 
					      return langToPolicy[firstKey]?.url;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,8 @@
 | 
				
			||||||
export const HTTP_URL_PATTERN = `https?:\\/\\/(?:www\\.)?(?:[^\\s)]*)(?<![.,:;!/?()[\\]\\s]+)`;
 | 
					export const HTTP_URL_PATTERN = `https?:\\/\\/(?:www\\.)?(?:[^\\s)]*)(?<![.,:;!/?()[\\]\\s]+)`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const EMAIL_REGEX =
 | 
				
			||||||
 | 
					  /^(([^<>()[\]\\.,;:\s@\\"]+(\.[^<>()[\]\\.,;:\s@\\"]+)*)|(\\".+\\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const URL_NEG_LB = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
 | 
					export const URL_NEG_LB = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)
 | 
					// https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,7 +98,7 @@ async function completeRegisterStage(
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
  createTemporaryClient, login, verifyEmail,
 | 
					  updateLocalStore, createTemporaryClient, login, verifyEmail,
 | 
				
			||||||
  loginWithToken, startSsoLogin,
 | 
					  loginWithToken, startSsoLogin,
 | 
				
			||||||
  completeRegisterStage,
 | 
					  completeRegisterStage,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ import * as sdk from 'matrix-js-sdk';
 | 
				
			||||||
import Olm from '@matrix-org/olm';
 | 
					import Olm from '@matrix-org/olm';
 | 
				
			||||||
// import { logger } from 'matrix-js-sdk/lib/logger';
 | 
					// import { logger } from 'matrix-js-sdk/lib/logger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { secret } from './state/auth';
 | 
					import { getSecret } from './state/auth';
 | 
				
			||||||
import RoomList from './state/RoomList';
 | 
					import RoomList from './state/RoomList';
 | 
				
			||||||
import AccountData from './state/AccountData';
 | 
					import AccountData from './state/AccountData';
 | 
				
			||||||
import RoomsInput from './state/RoomsInput';
 | 
					import RoomsInput from './state/RoomsInput';
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,7 @@ class InitMatrix extends EventEmitter {
 | 
				
			||||||
      dbName: 'web-sync-store',
 | 
					      dbName: 'web-sync-store',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await indexedDBStore.startup();
 | 
					    await indexedDBStore.startup();
 | 
				
			||||||
 | 
					    const secret = getSecret();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.matrixClient = sdk.createClient({
 | 
					    this.matrixClient = sdk.createClient({
 | 
				
			||||||
      baseUrl: secret.baseUrl,
 | 
					      baseUrl: secret.baseUrl,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,19 +0,0 @@
 | 
				
			||||||
import cons from './cons';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getSecret(key) {
 | 
					 | 
				
			||||||
  return localStorage.getItem(key);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const isAuthenticated = () => getSecret(cons.secretKey.ACCESS_TOKEN) !== null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const secret = {
 | 
					 | 
				
			||||||
  accessToken: getSecret(cons.secretKey.ACCESS_TOKEN),
 | 
					 | 
				
			||||||
  deviceId: getSecret(cons.secretKey.DEVICE_ID),
 | 
					 | 
				
			||||||
  userId: getSecret(cons.secretKey.USER_ID),
 | 
					 | 
				
			||||||
  baseUrl: getSecret(cons.secretKey.BASE_URL),
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export {
 | 
					 | 
				
			||||||
  isAuthenticated,
 | 
					 | 
				
			||||||
  secret,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
							
								
								
									
										12
									
								
								src/client/state/auth.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/client/state/auth.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					import cons from './cons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const isAuthenticated = () => localStorage.getItem(cons.secretKey.ACCESS_TOKEN) !== null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getSecret = () => ({
 | 
				
			||||||
 | 
					  accessToken: localStorage.getItem(cons.secretKey.ACCESS_TOKEN),
 | 
				
			||||||
 | 
					  deviceId: localStorage.getItem(cons.secretKey.DEVICE_ID),
 | 
				
			||||||
 | 
					  userId: localStorage.getItem(cons.secretKey.USER_ID),
 | 
				
			||||||
 | 
					  baseUrl: localStorage.getItem(cons.secretKey.BASE_URL),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { isAuthenticated, getSecret };
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/ext.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								src/ext.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -1,3 +1,5 @@
 | 
				
			||||||
 | 
					/// <reference types="vite/client" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare module 'browser-encrypt-attachment' {
 | 
					declare module 'browser-encrypt-attachment' {
 | 
				
			||||||
  export interface EncryptedAttachmentInfo {
 | 
					  export interface EncryptedAttachmentInfo {
 | 
				
			||||||
    v: string;
 | 
					    v: string;
 | 
				
			||||||
| 
						 | 
					@ -26,3 +28,8 @@ declare module 'browser-encrypt-attachment' {
 | 
				
			||||||
    info: EncryptedAttachmentInfo
 | 
					    info: EncryptedAttachmentInfo
 | 
				
			||||||
  ): Promise<ArrayBuffer>;
 | 
					  ): Promise<ArrayBuffer>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare module '*.svg' {
 | 
				
			||||||
 | 
					  const content: string;
 | 
				
			||||||
 | 
					  export default content;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
/* eslint-disable import/first */
 | 
					/* eslint-disable import/first */
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import ReactDom from 'react-dom';
 | 
					import { createRoot } from 'react-dom/client';
 | 
				
			||||||
import { enableMapSet } from 'immer';
 | 
					import { enableMapSet } from 'immer';
 | 
				
			||||||
import '@fontsource/inter/variable.css';
 | 
					import '@fontsource/inter/variable.css';
 | 
				
			||||||
import 'folds/dist/style.css';
 | 
					import 'folds/dist/style.css';
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,18 @@ import settings from './client/state/settings';
 | 
				
			||||||
import App from './app/pages/App';
 | 
					import App from './app/pages/App';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
document.body.classList.add(configClass, varsClass);
 | 
					document.body.classList.add(configClass, varsClass);
 | 
				
			||||||
 | 
					 | 
				
			||||||
settings.applyTheme();
 | 
					settings.applyTheme();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ReactDom.render(<App />, document.getElementById('root'));
 | 
					const mountApp = () => {
 | 
				
			||||||
 | 
					  const rootContainer = document.getElementById('root');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (rootContainer === null) {
 | 
				
			||||||
 | 
					    console.error('Root container element not found!');
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const root = createRoot(rootContainer);
 | 
				
			||||||
 | 
					  root.render(<App />);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mountApp();
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue