2022-01-06 09:19:03 +08:00
|
|
|
/*
|
2022-05-28 04:08:03 +08:00
|
|
|
Copyright 2021-2022 New Vector Ltd
|
2022-01-06 09:19:03 +08:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import React, {
|
2022-05-28 04:08:03 +08:00
|
|
|
FC,
|
2022-01-06 09:19:03 +08:00
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useState,
|
|
|
|
createContext,
|
|
|
|
useMemo,
|
|
|
|
useContext,
|
2022-11-02 23:23:05 +08:00
|
|
|
useRef,
|
2022-01-06 09:19:03 +08:00
|
|
|
} from "react";
|
|
|
|
import { useHistory } from "react-router-dom";
|
2022-11-22 01:39:48 +08:00
|
|
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
2022-06-28 05:41:07 +08:00
|
|
|
import { logger } from "matrix-js-sdk/src/logger";
|
2022-10-10 21:19:10 +08:00
|
|
|
import { useTranslation } from "react-i18next";
|
2022-05-28 04:08:03 +08:00
|
|
|
|
2022-02-11 09:10:36 +08:00
|
|
|
import { ErrorView } from "./FullScreenView";
|
2022-07-28 04:14:05 +08:00
|
|
|
import {
|
|
|
|
initClient,
|
2022-08-13 05:58:29 +08:00
|
|
|
CryptoStoreIntegrityError,
|
2022-10-26 19:58:41 +08:00
|
|
|
fallbackICEServerAllowed,
|
2022-07-28 04:14:05 +08:00
|
|
|
} from "./matrix-utils";
|
2022-09-09 14:08:17 +08:00
|
|
|
import { widget } from "./widget";
|
2023-03-01 20:47:36 +08:00
|
|
|
import {
|
|
|
|
PosthogAnalytics,
|
|
|
|
RegistrationType,
|
|
|
|
} from "./analytics/PosthogAnalytics";
|
2022-10-10 21:19:10 +08:00
|
|
|
import { translatedError } from "./TranslatedError";
|
2022-11-22 01:39:48 +08:00
|
|
|
import { useEventTarget } from "./useEvents";
|
2022-12-21 01:26:45 +08:00
|
|
|
import { Config } from "./config/Config";
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-05-28 04:08:03 +08:00
|
|
|
declare global {
|
|
|
|
interface Window {
|
|
|
|
matrixclient: MatrixClient;
|
2022-12-19 19:16:59 +08:00
|
|
|
isPasswordlessUser: boolean;
|
2022-05-28 04:08:03 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Session {
|
|
|
|
user_id: string;
|
|
|
|
device_id: string;
|
|
|
|
access_token: string;
|
|
|
|
passwordlessUser: boolean;
|
|
|
|
tempPassword?: string;
|
|
|
|
}
|
|
|
|
|
2022-11-29 05:15:47 +08:00
|
|
|
const loadChannel =
|
|
|
|
"BroadcastChannel" in window ? new BroadcastChannel("load") : null;
|
2022-11-22 01:39:48 +08:00
|
|
|
|
2022-05-28 04:08:03 +08:00
|
|
|
const loadSession = (): Session => {
|
|
|
|
const data = localStorage.getItem("matrix-auth-store");
|
|
|
|
if (data) return JSON.parse(data);
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
const saveSession = (session: Session) =>
|
|
|
|
localStorage.setItem("matrix-auth-store", JSON.stringify(session));
|
|
|
|
const clearSession = () => localStorage.removeItem("matrix-auth-store");
|
|
|
|
|
|
|
|
interface ClientState {
|
|
|
|
loading: boolean;
|
|
|
|
isAuthenticated: boolean;
|
|
|
|
isPasswordlessUser: boolean;
|
|
|
|
client: MatrixClient;
|
|
|
|
userName: string;
|
|
|
|
changePassword: (password: string) => Promise<void>;
|
|
|
|
logout: () => void;
|
|
|
|
setClient: (client: MatrixClient, session: Session) => void;
|
2022-07-28 06:22:48 +08:00
|
|
|
error?: Error;
|
2022-05-28 04:08:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const ClientContext = createContext<ClientState>(null);
|
|
|
|
|
|
|
|
type ClientProviderState = Omit<
|
|
|
|
ClientState,
|
|
|
|
"changePassword" | "logout" | "setClient"
|
|
|
|
> & { error?: Error };
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-07-08 21:56:00 +08:00
|
|
|
interface Props {
|
|
|
|
children: JSX.Element;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const ClientProvider: FC<Props> = ({ children }) => {
|
2022-01-06 09:19:03 +08:00
|
|
|
const history = useHistory();
|
2022-11-02 23:23:05 +08:00
|
|
|
const initializing = useRef(false);
|
2022-01-06 09:19:03 +08:00
|
|
|
const [
|
2022-02-11 09:10:36 +08:00
|
|
|
{ loading, isAuthenticated, isPasswordlessUser, client, userName, error },
|
2022-01-06 09:19:03 +08:00
|
|
|
setState,
|
2022-05-28 04:08:03 +08:00
|
|
|
] = useState<ClientProviderState>({
|
2022-01-06 09:19:03 +08:00
|
|
|
loading: true,
|
|
|
|
isAuthenticated: false,
|
|
|
|
isPasswordlessUser: false,
|
|
|
|
client: undefined,
|
|
|
|
userName: null,
|
2022-02-11 09:10:36 +08:00
|
|
|
error: undefined,
|
2022-01-06 09:19:03 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
2022-11-02 23:23:05 +08:00
|
|
|
// In case the component is mounted, unmounted, and remounted quickly (as
|
|
|
|
// React does in strict mode), we need to make sure not to doubly initialize
|
|
|
|
// the client
|
|
|
|
if (initializing.current) return;
|
|
|
|
initializing.current = true;
|
|
|
|
|
2022-06-28 05:41:07 +08:00
|
|
|
const init = async (): Promise<
|
2022-05-28 04:08:03 +08:00
|
|
|
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
|
|
|
> => {
|
2022-09-09 14:08:17 +08:00
|
|
|
if (widget) {
|
|
|
|
// We're inside a widget, so let's engage *matryoshka mode*
|
|
|
|
logger.log("Using a matryoshka client");
|
2022-06-28 05:41:07 +08:00
|
|
|
return {
|
2022-09-09 14:08:17 +08:00
|
|
|
client: await widget.client,
|
2022-06-28 05:41:07 +08:00
|
|
|
isPasswordlessUser: false,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
// We're running as a standalone application
|
|
|
|
try {
|
|
|
|
const session = loadSession();
|
2022-08-13 05:58:29 +08:00
|
|
|
if (!session) return { client: undefined, isPasswordlessUser: false };
|
|
|
|
|
|
|
|
logger.log("Using a standalone client");
|
|
|
|
|
|
|
|
/* eslint-disable camelcase */
|
|
|
|
const { user_id, device_id, access_token, passwordlessUser } =
|
|
|
|
session;
|
|
|
|
|
|
|
|
try {
|
|
|
|
return {
|
|
|
|
client: await initClient(
|
|
|
|
{
|
2022-12-21 01:26:45 +08:00
|
|
|
baseUrl: Config.defaultHomeserverUrl(),
|
2022-08-13 05:58:29 +08:00
|
|
|
accessToken: access_token,
|
|
|
|
userId: user_id,
|
|
|
|
deviceId: device_id,
|
2022-10-26 19:58:41 +08:00
|
|
|
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
2022-08-13 05:58:29 +08:00
|
|
|
},
|
|
|
|
true
|
|
|
|
),
|
|
|
|
isPasswordlessUser: passwordlessUser,
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
if (err instanceof CryptoStoreIntegrityError) {
|
|
|
|
// We can't use this session anymore, so let's log it out
|
|
|
|
try {
|
|
|
|
const client = await initClient(
|
|
|
|
{
|
2022-12-21 01:26:45 +08:00
|
|
|
baseUrl: Config.defaultHomeserverUrl(),
|
2022-08-13 05:58:29 +08:00
|
|
|
accessToken: access_token,
|
|
|
|
userId: user_id,
|
|
|
|
deviceId: device_id,
|
2022-10-26 19:58:41 +08:00
|
|
|
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
2022-08-13 05:58:29 +08:00
|
|
|
},
|
|
|
|
false // Don't need the crypto store just to log out
|
|
|
|
);
|
2022-10-14 09:25:15 +08:00
|
|
|
await client.logout(true);
|
2022-08-13 05:58:29 +08:00
|
|
|
} catch (err_) {
|
|
|
|
logger.warn(
|
|
|
|
"The previous session was lost, and we couldn't log it out, " +
|
|
|
|
"either"
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw err;
|
2022-06-28 05:41:07 +08:00
|
|
|
}
|
2022-08-13 05:58:29 +08:00
|
|
|
/* eslint-enable camelcase */
|
2022-06-28 05:41:07 +08:00
|
|
|
} catch (err) {
|
|
|
|
clearSession();
|
|
|
|
throw err;
|
|
|
|
}
|
2022-01-06 09:19:03 +08:00
|
|
|
}
|
2022-05-28 04:08:03 +08:00
|
|
|
};
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-06-28 05:41:07 +08:00
|
|
|
init()
|
2022-05-28 04:08:03 +08:00
|
|
|
.then(({ client, isPasswordlessUser }) => {
|
2022-01-06 09:19:03 +08:00
|
|
|
setState({
|
|
|
|
client,
|
|
|
|
loading: false,
|
2022-05-28 04:08:03 +08:00
|
|
|
isAuthenticated: Boolean(client),
|
|
|
|
isPasswordlessUser,
|
2022-01-06 09:19:03 +08:00
|
|
|
userName: client?.getUserIdLocalpart(),
|
2022-08-02 06:46:16 +08:00
|
|
|
error: undefined,
|
2022-01-06 09:19:03 +08:00
|
|
|
});
|
|
|
|
})
|
2022-06-28 05:41:07 +08:00
|
|
|
.catch((err) => {
|
|
|
|
logger.error(err);
|
2022-01-06 09:19:03 +08:00
|
|
|
setState({
|
|
|
|
client: undefined,
|
|
|
|
loading: false,
|
|
|
|
isAuthenticated: false,
|
|
|
|
isPasswordlessUser: false,
|
|
|
|
userName: null,
|
2022-08-02 06:46:16 +08:00
|
|
|
error: undefined,
|
2022-01-06 09:19:03 +08:00
|
|
|
});
|
2022-11-02 23:23:05 +08:00
|
|
|
})
|
|
|
|
.finally(() => (initializing.current = false));
|
2022-01-06 09:19:03 +08:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
const changePassword = useCallback(
|
2022-05-28 04:08:03 +08:00
|
|
|
async (password: string) => {
|
|
|
|
const { tempPassword, ...session } = loadSession();
|
2022-01-06 09:19:03 +08:00
|
|
|
|
|
|
|
await client.setPassword(
|
|
|
|
{
|
|
|
|
type: "m.login.password",
|
|
|
|
identifier: {
|
|
|
|
type: "m.id.user",
|
2022-05-28 04:08:03 +08:00
|
|
|
user: session.user_id,
|
2022-01-06 09:19:03 +08:00
|
|
|
},
|
2022-05-28 04:08:03 +08:00
|
|
|
user: session.user_id,
|
2022-01-06 09:19:03 +08:00
|
|
|
password: tempPassword,
|
|
|
|
},
|
|
|
|
password
|
|
|
|
);
|
|
|
|
|
2022-05-28 04:08:03 +08:00
|
|
|
saveSession({ ...session, passwordlessUser: false });
|
2022-01-06 09:19:03 +08:00
|
|
|
|
|
|
|
setState({
|
|
|
|
client,
|
|
|
|
loading: false,
|
|
|
|
isAuthenticated: true,
|
|
|
|
isPasswordlessUser: false,
|
|
|
|
userName: client.getUserIdLocalpart(),
|
2022-08-02 06:46:16 +08:00
|
|
|
error: undefined,
|
2022-01-06 09:19:03 +08:00
|
|
|
});
|
|
|
|
},
|
|
|
|
[client]
|
|
|
|
);
|
|
|
|
|
2022-02-16 04:46:58 +08:00
|
|
|
const setClient = useCallback(
|
2022-05-28 04:08:03 +08:00
|
|
|
(newClient: MatrixClient, session: Session) => {
|
2022-02-16 04:46:58 +08:00
|
|
|
if (client && client !== newClient) {
|
|
|
|
client.stopClient();
|
|
|
|
}
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-02-16 04:46:58 +08:00
|
|
|
if (newClient) {
|
2022-05-28 04:08:03 +08:00
|
|
|
saveSession(session);
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-02-16 04:46:58 +08:00
|
|
|
setState({
|
|
|
|
client: newClient,
|
|
|
|
loading: false,
|
|
|
|
isAuthenticated: true,
|
2022-05-28 04:08:03 +08:00
|
|
|
isPasswordlessUser: session.passwordlessUser,
|
2022-02-16 04:46:58 +08:00
|
|
|
userName: newClient.getUserIdLocalpart(),
|
2022-08-02 06:46:16 +08:00
|
|
|
error: undefined,
|
2022-02-16 04:46:58 +08:00
|
|
|
});
|
|
|
|
} else {
|
2022-05-28 04:08:03 +08:00
|
|
|
clearSession();
|
2022-02-16 04:46:58 +08:00
|
|
|
|
|
|
|
setState({
|
|
|
|
client: undefined,
|
|
|
|
loading: false,
|
|
|
|
isAuthenticated: false,
|
|
|
|
isPasswordlessUser: false,
|
|
|
|
userName: null,
|
2022-08-02 06:46:16 +08:00
|
|
|
error: undefined,
|
2022-02-16 04:46:58 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[client]
|
|
|
|
);
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-09-13 22:48:04 +08:00
|
|
|
const logout = useCallback(async () => {
|
2022-10-14 09:25:15 +08:00
|
|
|
await client.logout(true);
|
2022-09-26 20:01:43 +08:00
|
|
|
await client.clearStores();
|
2022-05-28 04:08:03 +08:00
|
|
|
clearSession();
|
2022-09-13 22:48:04 +08:00
|
|
|
setState({
|
|
|
|
client: undefined,
|
|
|
|
loading: false,
|
|
|
|
isAuthenticated: false,
|
|
|
|
isPasswordlessUser: true,
|
|
|
|
userName: "",
|
|
|
|
error: undefined,
|
|
|
|
});
|
2022-05-28 04:08:03 +08:00
|
|
|
history.push("/");
|
2022-11-04 20:07:14 +08:00
|
|
|
PosthogAnalytics.instance.setRegistrationType(RegistrationType.Guest);
|
2022-09-13 22:48:04 +08:00
|
|
|
}, [history, client]);
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-10-10 21:19:10 +08:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
2022-11-22 01:39:48 +08:00
|
|
|
// To protect against multiple sessions writing to the same storage
|
|
|
|
// simultaneously, we send a broadcast message that shuts down all other
|
|
|
|
// running instances of the app. This isn't necessary if the app is running in
|
|
|
|
// a widget though, since then it'll be mostly stateless.
|
2022-02-11 09:10:36 +08:00
|
|
|
useEffect(() => {
|
2022-11-29 05:15:47 +08:00
|
|
|
if (!widget) loadChannel?.postMessage({});
|
2022-11-22 01:39:48 +08:00
|
|
|
}, []);
|
2022-02-11 09:10:36 +08:00
|
|
|
|
2022-11-22 01:39:48 +08:00
|
|
|
useEventTarget(
|
|
|
|
loadChannel,
|
|
|
|
"message",
|
|
|
|
useCallback(() => {
|
|
|
|
client?.stopClient();
|
|
|
|
|
|
|
|
setState((prev) => ({
|
|
|
|
...prev,
|
|
|
|
error: translatedError(
|
|
|
|
"This application has been opened in another tab.",
|
|
|
|
t
|
|
|
|
),
|
|
|
|
}));
|
|
|
|
}, [client, setState, t])
|
|
|
|
);
|
2022-02-11 09:10:36 +08:00
|
|
|
|
2022-05-28 04:08:03 +08:00
|
|
|
const context = useMemo<ClientState>(
|
2022-01-06 09:19:03 +08:00
|
|
|
() => ({
|
|
|
|
loading,
|
|
|
|
isAuthenticated,
|
|
|
|
isPasswordlessUser,
|
|
|
|
client,
|
|
|
|
changePassword,
|
|
|
|
logout,
|
|
|
|
userName,
|
|
|
|
setClient,
|
2022-08-02 06:46:16 +08:00
|
|
|
error: undefined,
|
2022-01-06 09:19:03 +08:00
|
|
|
}),
|
|
|
|
[
|
|
|
|
loading,
|
|
|
|
isAuthenticated,
|
|
|
|
isPasswordlessUser,
|
|
|
|
client,
|
|
|
|
changePassword,
|
|
|
|
logout,
|
|
|
|
userName,
|
|
|
|
setClient,
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
2022-02-16 04:46:58 +08:00
|
|
|
useEffect(() => {
|
|
|
|
window.matrixclient = client;
|
2022-12-19 19:16:59 +08:00
|
|
|
window.isPasswordlessUser = isPasswordlessUser;
|
|
|
|
}, [client, isPasswordlessUser]);
|
2022-02-16 04:46:58 +08:00
|
|
|
|
2022-02-11 09:10:36 +08:00
|
|
|
if (error) {
|
|
|
|
return <ErrorView error={error} />;
|
|
|
|
}
|
|
|
|
|
2022-01-06 09:19:03 +08:00
|
|
|
return (
|
|
|
|
<ClientContext.Provider value={context}>{children}</ClientContext.Provider>
|
|
|
|
);
|
2022-05-28 04:08:03 +08:00
|
|
|
};
|
2022-01-06 09:19:03 +08:00
|
|
|
|
2022-05-28 04:08:03 +08:00
|
|
|
export const useClient = () => useContext(ClientContext);
|