diff --git a/locales/en/app.json b/locales/en/app.json index b72af0d5..3e7c64a7 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -72,16 +72,22 @@ }, "disconnected_banner": "Connectivity to the server has been lost.", "error": { + "auth_connection_failed_details": "<0>The application could not reach the call authentication service at <2>{{url}}. If you are the server admin, check the network logs and make sure <5>lk-jwt-service is listening at that address.", + "auth_connection_rejected_details": "<0>The application connected to the call authentication service at <2>{{url}}, but it responded with status code {{status}}. If you are the server admin, make sure <8>lk-jwt-service is listening at that address and check the logs for more information.", "call_not_found": "Call not found", "call_not_found_description": "<0>That link doesn't appear to belong to any existing call. Check that you have the right link, or <1>create a new one.", + "connection_failed": "Connection failed", + "connection_failed_description": "Could not connect to the call server. Check your network connection or try again later.", "connection_lost": "Connection lost", "connection_lost_description": "You were disconnected from the call.", + "connection_rejected_description": "Could not connect to the call server. Try again later or contact your server admin if this persists.", "e2ee_unsupported": "Incompatible browser", "e2ee_unsupported_description": "Your web browser does not support encrypted calls. Supported browsers include Chrome, Safari, and Firefox 117+.", "generic": "Something went wrong", "generic_description": "Submitting debug logs will help us track down the problem.", "open_elsewhere": "Opened in another tab", - "open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page." + "open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.", + "show_details": "Show details" }, "group_call_loader": { "banned_body": "You have been banned from the room.", diff --git a/src/RichError.tsx b/src/RichError.tsx index effc7610..54076838 100644 --- a/src/RichError.tsx +++ b/src/RichError.tsx @@ -5,9 +5,19 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { type FC, type ReactNode } from "react"; -import { useTranslation } from "react-i18next"; -import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { + type ReactElement, + useCallback, + useState, + type FC, + type ReactNode, +} from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { + OfflineIcon, + PopOutIcon, +} from "@vector-im/compound-design-tokens/assets/web/icons"; +import { Button, Link } from "@vector-im/compound-web"; import { ErrorView } from "./ErrorView"; @@ -22,8 +32,9 @@ export class RichError extends Error { * The pretty, more helpful message to be shown on the error screen. */ public readonly richMessage: ReactNode, + cause?: unknown, ) { - super(message); + super(message, { cause }); } } @@ -46,3 +57,117 @@ export class OpenElsewhereError extends RichError { super("App opened in another tab", ); } } + +interface AuthConnectionFailedProps { + livekitServiceUrl: string; +} + +const AuthConnectionFailed: FC = ({ + livekitServiceUrl, +}) => { + const { t } = useTranslation(); + const [showDetails, setShowDetails] = useState(false); + const onShowDetailsClick = useCallback(() => setShowDetails(true), []); + + return ( + +

{t("error.connection_failed_description")}

+ {showDetails ? ( + +

+ The application could not reach the call authentication service at{" "} + + {{ url: livekitServiceUrl } as unknown as ReactElement} + + . If you are the server admin, check the network logs and make sure{" "} + + lk-jwt-service + {" "} + is listening at that address. +

+
+ ) : ( + + )} +
+ ); +}; + +export class AuthConnectionFailedError extends RichError { + public constructor(livekitServiceUrl: string, cause: unknown) { + super( + `Failed to connect to ${livekitServiceUrl}`, + , + cause, + ); + } +} + +interface AuthConnectionRejectedProps { + livekitServiceUrl: string; + status: number; +} + +const AuthConnectionRejected: FC = ({ + livekitServiceUrl, + status, +}) => { + const { t } = useTranslation(); + const [showDetails, setShowDetails] = useState(false); + const onShowDetailsClick = useCallback(() => setShowDetails(true), []); + + return ( + +

{t("error.connection_rejected_description")}

+ {showDetails ? ( + +

+ The application connected to the call authentication service at{" "} + + {{ url: livekitServiceUrl } as unknown as ReactElement} + + , but it responded with status code{" "} + {{ status } as unknown as ReactElement}. If you are the server + admin, make sure{" "} + + lk-jwt-service + {" "} + is listening at that address and check the logs for more + information. +

+
+ ) : ( + + )} +
+ ); +}; + +export class AuthConnectionRejectedError extends RichError { + public constructor(livekitServiceUrl: string, status: number) { + super( + `Failed to connect to ${livekitServiceUrl} (status ${status})`, + , + ); + } +} diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts index ab696d4e..076d6164 100644 --- a/src/livekit/openIDSFU.ts +++ b/src/livekit/openIDSFU.ts @@ -12,6 +12,10 @@ import { useEffect, useState } from "react"; import { type LivekitFocus } from "matrix-js-sdk/src/matrixrtc/LivekitFocus"; import { useActiveLivekitFocus } from "../room/useActiveFocus"; +import { + AuthConnectionFailedError, + AuthConnectionRejectedError, +} from "../RichError"; export interface SFUConfig { url: string; @@ -35,25 +39,24 @@ export function useOpenIDSFU( client: OpenIDClientParts, rtcSession: MatrixRTCSession, ): SFUConfig | undefined { - const [sfuConfig, setSFUConfig] = useState(undefined); + const [sfuConfig, setSFUConfig] = useState( + undefined, + ); const activeFocus = useActiveLivekitFocus(rtcSession); useEffect(() => { if (activeFocus) { getSFUConfigWithOpenID(client, activeFocus).then( - (sfuConfig) => { - setSFUConfig(sfuConfig); - }, - (e) => { - logger.error("Failed to get SFU config", e); - }, + (sfuConfig) => setSFUConfig(sfuConfig), + (e) => setSFUConfig(e), ); } else { setSFUConfig(undefined); } }, [client, activeFocus]); + if (sfuConfig instanceof Error) throw sfuConfig; return sfuConfig; } @@ -64,26 +67,18 @@ export async function getSFUConfigWithOpenID( const openIdToken = await client.getOpenIdToken(); logger.debug("Got openID token", openIdToken); - try { - logger.info( - `Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`, - ); - const sfuConfig = await getLiveKitJWT( - client, - activeFocus.livekit_service_url, - activeFocus.livekit_alias, - openIdToken, - ); - logger.info(`Got JWT from call's active focus URL.`); + logger.info( + `Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`, + ); + const sfuConfig = await getLiveKitJWT( + client, + activeFocus.livekit_service_url, + activeFocus.livekit_alias, + openIdToken, + ); + logger.info(`Got JWT from call's active focus URL.`); - return sfuConfig; - } catch (e) { - logger.warn( - `Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`, - e, - ); - return undefined; - } + return sfuConfig; } async function getLiveKitJWT( @@ -92,8 +87,9 @@ async function getLiveKitJWT( roomName: string, openIDToken: IOpenIDToken, ): Promise { + let res: Response; try { - const res = await fetch(livekitServiceURL + "/sfu/get", { + res = await fetch(livekitServiceURL + "/sfu/get", { method: "POST", headers: { "Content-Type": "application/json", @@ -104,11 +100,11 @@ async function getLiveKitJWT( device_id: client.getDeviceId(), }), }); - if (!res.ok) { - throw new Error("SFU Config fetch failed with status code " + res.status); - } - return await res.json(); } catch (e) { - throw new Error("SFU Config fetch failed with exception " + e); + throw new AuthConnectionFailedError(livekitServiceURL, e); } + if (!res.ok) { + throw new AuthConnectionRejectedError(livekitServiceURL, res.status); + } + return await res.json(); }