+++ /dev/null
-import { ReactNode, useCallback, useEffect } from 'react';
-import { Capabilities } from 'matrix-js-sdk';
-import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
-import { useMatrixClient } from '../hooks/useMatrixClient';
-import { MediaConfig } from '../hooks/useMediaConfig';
-import { promiseFulfilledResult } from '../utils/common';
-
-type CapabilitiesAndMediaConfigLoaderProps = {
- children: (capabilities?: Capabilities, mediaConfig?: MediaConfig) => ReactNode;
-};
-export function CapabilitiesAndMediaConfigLoader({
- children,
-}: CapabilitiesAndMediaConfigLoaderProps) {
- const mx = useMatrixClient();
-
- const [state, load] = useAsyncCallback<
- [Capabilities | undefined, MediaConfig | undefined],
- unknown,
- []
- >(
- useCallback(async () => {
- const result = await Promise.allSettled([mx.getCapabilities(), mx.getMediaConfig()]);
- const capabilities = promiseFulfilledResult(result[0]);
- const mediaConfig = promiseFulfilledResult(result[1]);
- return [capabilities, mediaConfig];
- }, [mx])
- );
-
- useEffect(() => {
- load();
- }, [load]);
-
- const [capabilities, mediaConfig] =
- state.status === AsyncStatus.Success ? state.data : [undefined, undefined];
- return children(capabilities, mediaConfig);
-}
--- /dev/null
+import { ReactNode, useCallback, useMemo } from 'react';
+import { Capabilities, validateAuthMetadata, ValidatedAuthMetadata } from 'matrix-js-sdk';
+import { AsyncStatus, useAsyncCallbackValue } from '../hooks/useAsyncCallback';
+import { useMatrixClient } from '../hooks/useMatrixClient';
+import { MediaConfig } from '../hooks/useMediaConfig';
+import { promiseFulfilledResult } from '../utils/common';
+
+export type ServerConfigs = {
+ capabilities?: Capabilities;
+ mediaConfig?: MediaConfig;
+ authMetadata?: ValidatedAuthMetadata;
+};
+
+type ServerConfigsLoaderProps = {
+ children: (configs: ServerConfigs) => ReactNode;
+};
+export function ServerConfigsLoader({ children }: ServerConfigsLoaderProps) {
+ const mx = useMatrixClient();
+ const fallbackConfigs = useMemo(() => ({}), []);
+
+ const [configsState] = useAsyncCallbackValue<ServerConfigs, unknown>(
+ useCallback(async () => {
+ const result = await Promise.allSettled([
+ mx.getCapabilities(),
+ mx.getMediaConfig(),
+ mx.getAuthMetadata(),
+ ]);
+
+ const capabilities = promiseFulfilledResult(result[0]);
+ const mediaConfig = promiseFulfilledResult(result[1]);
+ const authMetadata = promiseFulfilledResult(result[2]);
+ let validatedAuthMetadata: ValidatedAuthMetadata | undefined;
+
+ try {
+ validatedAuthMetadata = validateAuthMetadata(authMetadata);
+ } catch (e) {
+ console.error(e);
+ }
+
+ return {
+ capabilities,
+ mediaConfig,
+ authMetadata: validatedAuthMetadata,
+ };
+ }, [mx])
+ );
+
+ const configs: ServerConfigs =
+ configsState.status === AsyncStatus.Success ? configsState.data : fallbackConfigs;
+
+ return children(configs);
+}
import { DeviceVerificationStatus } from '../../../components/DeviceVerificationStatus';
import { VerifyOtherDeviceTile } from './Verification';
import { VerificationStatus } from '../../../hooks/useDeviceVerificationStatus';
+import { useAuthMetadata } from '../../../hooks/useAuthMetadata';
+import { withSearchParam } from '../../../pages/pathUtils';
+import { useAccountManagementActions } from '../../../hooks/useAccountManagement';
+import { SettingTile } from '../../../components/setting-tile';
type OtherDevicesProps = {
devices: IMyDevice[];
export function OtherDevices({ devices, refreshDeviceList, showVerification }: OtherDevicesProps) {
const mx = useMatrixClient();
const crypto = mx.getCrypto();
+ const authMetadata = useAuthMetadata();
+ const accountManagementActions = useAccountManagementActions();
+
const [deleted, setDeleted] = useState<Set<string>>(new Set());
+ const handleDashboardOIDC = useCallback(() => {
+ const authUrl = authMetadata?.account_management_uri ?? authMetadata?.issuer;
+ if (!authUrl) return;
+
+ window.open(
+ withSearchParam(authUrl, {
+ action: accountManagementActions.sessionsList,
+ }),
+ '_blank'
+ );
+ }, [authMetadata, accountManagementActions]);
+
+ const handleDeleteOIDC = useCallback(
+ (deviceId: string) => {
+ const authUrl = authMetadata?.account_management_uri ?? authMetadata?.issuer;
+ if (!authUrl) return;
+
+ window.open(
+ withSearchParam(authUrl, {
+ action: accountManagementActions.sessionEnd,
+ device_id: deviceId,
+ }),
+ '_blank'
+ );
+ },
+ [authMetadata, accountManagementActions]
+ );
+
const handleToggleDelete = useCallback((deviceId: string) => {
setDeleted((deviceIds) => {
const newIds = new Set(deviceIds);
<>
<Box direction="Column" gap="100">
<Text size="L400">Others</Text>
+ {authMetadata && (
+ <SequenceCard
+ className={SequenceCardStyle}
+ variant="SurfaceVariant"
+ direction="Column"
+ gap="400"
+ >
+ <SettingTile
+ title="Device Dashboard"
+ description="Manage your devices on OIDC dashboard."
+ after={
+ <Button
+ size="300"
+ variant="Secondary"
+ fill="Soft"
+ radii="300"
+ outlined
+ onClick={handleDashboardOIDC}
+ >
+ <Text size="B300">Open</Text>
+ </Button>
+ }
+ />
+ </SequenceCard>
+ )}
{devices
.sort((d1, d2) => {
if (!d1.last_seen_ts || !d2.last_seen_ts) return 0;
refreshDeviceList={refreshDeviceList}
disabled={deleting}
options={
- <DeviceDeleteBtn
- deviceId={device.device_id}
- deleted={deleted.has(device.device_id)}
- onDeleteToggle={handleToggleDelete}
- disabled={deleting}
- />
+ authMetadata ? (
+ <DeviceDeleteBtn
+ deviceId={device.device_id}
+ deleted={false}
+ onDeleteToggle={handleDeleteOIDC}
+ />
+ ) : (
+ <DeviceDeleteBtn
+ deviceId={device.device_id}
+ deleted={deleted.has(device.device_id)}
+ onDeleteToggle={handleToggleDelete}
+ disabled={deleting}
+ />
+ )
}
/>
{showVerification && crypto && (
DeviceVerificationSetup,
} from '../../../components/DeviceVerificationSetup';
import { stopPropagation } from '../../../utils/keyboard';
+import { useAuthMetadata } from '../../../hooks/useAuthMetadata';
+import { withSearchParam } from '../../../pages/pathUtils';
+import { useAccountManagementActions } from '../../../hooks/useAccountManagement';
type VerificationStatusBadgeProps = {
verificationStatus: VerificationStatus;
export function DeviceVerificationOptions() {
const [menuCords, setMenuCords] = useState<RectCords>();
+ const authMetadata = useAuthMetadata();
+ const accountManagementActions = useAccountManagementActions();
const [reset, setReset] = useState(false);
const handleReset = () => {
setMenuCords(undefined);
+
+ if (authMetadata) {
+ const authUrl = authMetadata.account_management_uri ?? authMetadata.issuer;
+ window.open(
+ withSearchParam(authUrl, {
+ action: accountManagementActions.crossSigningReset,
+ }),
+ '_blank'
+ );
+ return;
+ }
+
setReset(true);
};
--- /dev/null
+import { useMemo } from 'react';
+
+export const useAccountManagementActions = () => {
+ const actions = useMemo(
+ () => ({
+ profile: 'org.matrix.profile',
+ sessionsList: 'org.matrix.sessions_list',
+ sessionView: 'org.matrix.session_view',
+ sessionEnd: 'org.matrix.session_end',
+ accountDeactivate: 'org.matrix.account_deactivate',
+ crossSigningReset: 'org.matrix.cross_signing_reset',
+ }),
+ []
+ );
+
+ return actions;
+};
--- /dev/null
+import { ValidatedAuthMetadata } from 'matrix-js-sdk';
+import { createContext, useContext } from 'react';
+
+const AuthMetadataContext = createContext<ValidatedAuthMetadata | undefined>(undefined);
+
+export const AuthMetadataProvider = AuthMetadataContext.Provider;
+
+export const useAuthMetadata = (): ValidatedAuthMetadata | undefined => {
+ const metadata = useContext(AuthMetadataContext);
+
+ return metadata;
+};
} from '../../../client/initMatrix';
import { getSecret } from '../../../client/state/auth';
import { SplashScreen } from '../../components/splash-screen';
-import { CapabilitiesAndMediaConfigLoader } from '../../components/CapabilitiesAndMediaConfigLoader';
+import { ServerConfigsLoader } from '../../components/ServerConfigsLoader';
import { CapabilitiesProvider } from '../../hooks/useCapabilities';
import { MediaConfigProvider } from '../../hooks/useMediaConfig';
import { MatrixClientProvider } from '../../hooks/useMatrixClient';
import { useSyncState } from '../../hooks/useSyncState';
import { stopPropagation } from '../../utils/keyboard';
import { SyncStatus } from './SyncStatus';
+import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
function ClientRootLoading() {
return (
<ClientRootLoading />
) : (
<MatrixClientProvider value={mx}>
- <CapabilitiesAndMediaConfigLoader>
- {(capabilities, mediaConfig) => (
- <CapabilitiesProvider value={capabilities ?? {}}>
- <MediaConfigProvider value={mediaConfig ?? {}}>
- {children}
- <Windows />
- <Dialogs />
- <ReusableContextMenu />
+ <ServerConfigsLoader>
+ {(serverConfigs) => (
+ <CapabilitiesProvider value={serverConfigs.capabilities ?? {}}>
+ <MediaConfigProvider value={serverConfigs.mediaConfig ?? {}}>
+ <AuthMetadataProvider value={serverConfigs.authMetadata}>
+ {children}
+ <Windows />
+ <Dialogs />
+ <ReusableContextMenu />
+ </AuthMetadataProvider>
</MediaConfigProvider>
</CapabilitiesProvider>
)}
- </CapabilitiesAndMediaConfigLoader>
+ </ServerConfigsLoader>
</MatrixClientProvider>
)}
</SpecVersions>