diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 3656ecfeac0..0e7d8e866b6 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -765,7 +765,7 @@ export default class MatrixChat extends React.PureComponent { const tabPayload = payload as OpenToTabPayload; Modal.createDialog( UserSettingsDialog, - { initialTabId: tabPayload.initialTabId as UserTab }, + { initialTabId: tabPayload.initialTabId as UserTab, sdkContext: this.stores }, /*className=*/ undefined, /*isPriority=*/ false, /*isStatic=*/ true, diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 7ea8ac800a8..840fc6e82db 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -36,9 +36,11 @@ import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsT import SessionManagerTab from "../settings/tabs/user/SessionManagerTab"; import { UserTab } from "./UserTab"; import { NonEmptyArray } from "../../../@types/common"; +import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext"; interface IProps { initialTabId?: UserTab; + sdkContext: SdkContextClass; onFinished(): void; } @@ -197,20 +199,25 @@ export default class UserSettingsDialog extends React.Component public render(): React.ReactNode { return ( - -
- -
-
+ // XXX: SDKContext is provided within the LoggedInView subtree. + // Modals function outside the MatrixChat React tree, so sdkContext is reprovided here to simulate that. + // The longer term solution is to move our ModalManager into the React tree to inherit contexts properly. + + +
+ +
+
+
); } } diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index 9adb5bf287e..c735b2cbcec 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -32,13 +32,13 @@ import { VerificationRequest } from "matrix-js-sdk/src/crypto-api"; import { logger } from "matrix-js-sdk/src/logger"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; -import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import { _t } from "../../../../languageHandler"; import { getDeviceClientInformation, pruneClientInformation } from "../../../../utils/device/clientInformation"; import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types"; import { useEventEmitter } from "../../../../hooks/useEventEmitter"; import { parseUserAgent } from "../../../../utils/device/parseUserAgent"; import { isDeviceVerified } from "../../../../utils/device/isDeviceVerified"; +import { SDKContext } from "../../../../contexts/SDKContext"; const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceAppInfo => { const { name, version, url } = getDeviceClientInformation(matrixClient, device.device_id); @@ -90,7 +90,8 @@ export type DevicesState = { supportsMSC3881?: boolean | undefined; }; export const useOwnDevices = (): DevicesState => { - const matrixClient = useContext(MatrixClientContext); + const sdkContext = useContext(SDKContext); + const matrixClient = sdkContext.client!; const currentDeviceId = matrixClient.getDeviceId()!; const userId = matrixClient.getSafeUserId(); diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 40acfb31da9..79eae267c22 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -56,9 +56,8 @@ import SettingsSubsection, { SettingsSubsectionText } from "../../shared/Setting import { SettingsSubsectionHeading } from "../../shared/SettingsSubsectionHeading"; import Heading from "../../../typography/Heading"; import InlineSpinner from "../../../elements/InlineSpinner"; -import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import { ThirdPartyIdentifier } from "../../../../../AddThreepid"; -import { getDelegatedAuthAccountUrl } from "../../../../../utils/oidc/getDelegatedAuthAccountUrl"; +import { SDKContext } from "../../../../../contexts/SDKContext"; interface IProps { closeSettingsFn: () => void; @@ -94,20 +93,22 @@ interface IState { } export default class GeneralUserSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: React.ContextType; + public static contextType = SDKContext; + public context!: React.ContextType; private readonly dispatcherRef: string; - public constructor(props: IProps, context: React.ContextType) { + public constructor(props: IProps, context: React.ContextType) { super(props); this.context = context; + const cli = this.context.client!; + this.state = { language: languageHandler.getCurrentLanguage(), spellCheckEnabled: false, spellCheckLanguages: [], - haveIdServer: Boolean(this.context.getIdentityServerUrl()), + haveIdServer: Boolean(cli.getIdentityServerUrl()), idServerHasUnsignedTerms: false, requiredPolicyInfo: { // This object is passed along to a component for handling @@ -150,7 +151,7 @@ export default class GeneralUserSettingsTab extends React.Component { if (payload.action === "id_server_changed") { - this.setState({ haveIdServer: Boolean(this.context.getIdentityServerUrl()) }); + this.setState({ haveIdServer: Boolean(this.context.client!.getIdentityServerUrl()) }); this.getThreepidState(); } }; @@ -164,7 +165,7 @@ export default class GeneralUserSettingsTab extends React.Component { - const cli = this.context; + const cli = this.context.client!; const capabilities = await cli.getCapabilities(); // this is cached const changePasswordCap = capabilities["m.change_password"]; @@ -174,7 +175,7 @@ export default class GeneralUserSettingsTab extends React.Component { - const cli = this.context; + const cli = this.context.client!; // Check to see if terms need accepting this.checkTerms(); @@ -195,7 +196,7 @@ export default class GeneralUserSettingsTab extends React.Component { // By starting the terms flow we get the logic for checking which terms the user has signed // for free. So we might as well use that for our own purposes. - const idServerUrl = this.context.getIdentityServerUrl(); + const idServerUrl = this.context.client!.getIdentityServerUrl(); if (!this.state.haveIdServer || !idServerUrl) { this.setState({ idServerHasUnsignedTerms: false }); return; @@ -221,7 +222,7 @@ export default class GeneralUserSettingsTab extends React.Component { return new Promise((resolve, reject) => { diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index cc88cb27e83..bc06103255c 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -19,7 +19,6 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../../languageHandler"; -import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import Modal from "../../../../../Modal"; import SettingsSubsection from "../../shared/SettingsSubsection"; import SetupEncryptionDialog from "../../../dialogs/security/SetupEncryptionDialog"; @@ -39,8 +38,8 @@ import QuestionDialog from "../../../dialogs/QuestionDialog"; import { FilterVariation } from "../../devices/filter"; import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionHeading"; import { SettingsSection } from "../../shared/SettingsSection"; -import { getDelegatedAuthAccountUrl } from "../../../../../utils/oidc/getDelegatedAuthAccountUrl"; import { OidcLogoutDialog } from "../../../dialogs/oidc/OidcLogoutDialog"; +import { SDKContext } from "../../../../../contexts/SDKContext"; const confirmSignOut = async (sessionsToSignOutCount: number): Promise => { const { finished } = Modal.createDialog(QuestionDialog, { @@ -167,13 +166,14 @@ const SessionManagerTab: React.FC = () => { const filteredDeviceListRef = useRef(null); const scrollIntoViewTimeoutRef = useRef(); - const matrixClient = useContext(MatrixClientContext); + const sdkContext = useContext(SDKContext); + const matrixClient = sdkContext.client!; /** * If we have a delegated auth account management URL, all sessions but the current session need to be managed in the * delegated auth provider. * See https://github.com/matrix-org/matrix-spec-proposals/pull/3824 */ - const delegatedAuthAccountUrl = getDelegatedAuthAccountUrl(matrixClient.getClientWellKnown()); + const delegatedAuthAccountUrl = sdkContext.oidcClientStore.accountManagementEndpoint; const disableMultipleSignout = !!delegatedAuthAccountUrl; const userId = matrixClient?.getUserId(); diff --git a/src/utils/oidc/getDelegatedAuthAccountUrl.ts b/src/utils/oidc/getDelegatedAuthAccountUrl.ts deleted file mode 100644 index cfb61cb4434..00000000000 --- a/src/utils/oidc/getDelegatedAuthAccountUrl.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -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 { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; - -/** - * Get the delegated auth account management url if configured - * @param clientWellKnown from MatrixClient.getClientWellKnown - * @returns the account management url, or undefined - */ -export const getDelegatedAuthAccountUrl = (clientWellKnown: IClientWellKnown | undefined): string | undefined => { - const delegatedAuthConfig = M_AUTHENTICATION.findIn(clientWellKnown); - return delegatedAuthConfig?.account; -}; diff --git a/test/components/views/dialogs/UserSettingsDialog-test.tsx b/test/components/views/dialogs/UserSettingsDialog-test.tsx index fc0e36b93b6..586288d2bdb 100644 --- a/test/components/views/dialogs/UserSettingsDialog-test.tsx +++ b/test/components/views/dialogs/UserSettingsDialog-test.tsx @@ -16,7 +16,8 @@ limitations under the License. import React, { ReactElement } from "react"; import { render } from "@testing-library/react"; -import { mocked } from "jest-mock"; +import { mocked, MockedObject } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import SettingsStore, { CallbackFn } from "../../../../src/settings/SettingsStore"; import SdkConfig from "../../../../src/SdkConfig"; @@ -30,6 +31,7 @@ import { } from "../../../test-utils"; import { UIFeature } from "../../../../src/settings/UIFeature"; import { SettingLevel } from "../../../../src/settings/SettingLevel"; +import { SdkContextClass } from "../../../../src/contexts/SDKContext"; mockPlatformPeg({ supportsSpellCheckSettings: jest.fn().mockReturnValue(false), @@ -55,18 +57,22 @@ describe("", () => { const userId = "@alice:server.org"; const mockSettingsStore = mocked(SettingsStore); const mockSdkConfig = mocked(SdkConfig); - getMockClientWithEventEmitter({ - ...mockClientMethodsUser(userId), - ...mockClientMethodsServer(), - }); + let mockClient!: MockedObject; + let sdkContext: SdkContextClass; const defaultProps = { onFinished: jest.fn() }; const getComponent = (props: Partial = {}): ReactElement => ( - + ); beforeEach(() => { jest.clearAllMocks(); + mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + }); + sdkContext = new SdkContextClass(); + sdkContext.client = mockClient; mockSettingsStore.getValue.mockReturnValue(false); mockSettingsStore.getFeatureSettingNames.mockReturnValue([]); mockSdkConfig.get.mockReturnValue({ brand: "Test" }); diff --git a/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx index 7e7bdb4585b..9ed18fa2256 100644 --- a/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx @@ -13,11 +13,11 @@ limitations under the License. import { fireEvent, render, screen, within } from "@testing-library/react"; import React from "react"; -import { M_AUTHENTICATION, ThreepidMedium } from "matrix-js-sdk/src/matrix"; +import { ThreepidMedium } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab"; -import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; +import { SdkContextClass, SDKContext } from "../../../../../../src/contexts/SDKContext"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; import { getMockClientWithEventEmitter, @@ -28,6 +28,7 @@ import { } from "../../../../../test-utils"; import { UIFeature } from "../../../../../../src/settings/UIFeature"; import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; +import { OidcClientStore } from "../../../../../../src/stores/oidc/OidcClientStore"; describe("", () => { const defaultProps = { @@ -44,19 +45,18 @@ describe("", () => { deleteThreePid: jest.fn(), }); + let stores: SdkContextClass; + const getComponent = () => ( - + - + ); - const clientWellKnownSpy = jest.spyOn(mockClient, "getClientWellKnown"); - beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); mockPlatformPeg(); jest.clearAllMocks(); - clientWellKnownSpy.mockReturnValue({}); jest.spyOn(SettingsStore, "getValue").mockRestore(); jest.spyOn(logger, "error").mockRestore(); @@ -67,6 +67,12 @@ describe("", () => { mockClient.deleteThreePid.mockResolvedValue({ id_server_unbind_result: "success", }); + + stores = new SdkContextClass(); + stores.client = mockClient; + // stub out this store completely to avoid mocking initialisation + const mockOidcClientStore = {} as unknown as OidcClientStore; + jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore); }); it("does not show account management link when not available", () => { @@ -78,12 +84,11 @@ describe("", () => { it("show account management link in expected format", async () => { const accountManagementLink = "https://id.server.org/my-account"; - clientWellKnownSpy.mockReturnValue({ - [M_AUTHENTICATION.name]: { - issuer: "https://id.server.org", - account: accountManagementLink, - }, - }); + const mockOidcClientStore = { + accountManagementEndpoint: accountManagementLink, + } as unknown as OidcClientStore; + jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore); + const { getByTestId } = render(getComponent()); // wait for well-known call to settle @@ -167,12 +172,11 @@ describe("", () => { (settingName) => settingName === UIFeature.Deactivate, ); // account is managed externally when we have delegated auth configured - mockClient.getClientWellKnown.mockReturnValue({ - [M_AUTHENTICATION.name]: { - issuer: "https://issuer.org", - account: "https://issuer.org/account", - }, - }); + const accountManagementLink = "https://id.server.org/my-account"; + const mockOidcClientStore = { + accountManagementEndpoint: accountManagementLink, + } as unknown as OidcClientStore; + jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore); render(getComponent()); await flushPromises(); diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index df5037acb4f..636d3693441 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -32,9 +32,9 @@ import { CryptoApi, DeviceVerificationStatus, MatrixError, - M_AUTHENTICATION, + MatrixClient, } from "matrix-js-sdk/src/matrix"; -import { mocked } from "jest-mock"; +import { mocked, MockedObject } from "jest-mock"; import { clearAllModals, @@ -45,13 +45,14 @@ import { mockPlatformPeg, } from "../../../../../test-utils"; import SessionManagerTab from "../../../../../../src/components/views/settings/tabs/user/SessionManagerTab"; -import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; import Modal from "../../../../../../src/Modal"; import LogoutDialog from "../../../../../../src/components/views/dialogs/LogoutDialog"; import { DeviceSecurityVariation, ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types"; import { INACTIVE_DEVICE_AGE_MS } from "../../../../../../src/components/views/settings/devices/filter"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; import { getClientInformationEventType } from "../../../../../../src/utils/device/clientInformation"; +import { SDKContext, SdkContextClass } from "../../../../../../src/contexts/SDKContext"; +import { OidcClientStore } from "../../../../../../src/stores/oidc/OidcClientStore"; mockPlatformPeg(); @@ -91,31 +92,14 @@ describe("", () => { requestDeviceVerification: jest.fn().mockResolvedValue(mockVerificationRequest), } as unknown as CryptoApi); - let mockClient = getMockClientWithEventEmitter({ - ...mockClientMethodsUser(aliceId), - getCrypto: jest.fn().mockReturnValue(mockCrypto), - getDevices: jest.fn(), - getStoredDevice: jest.fn(), - getDeviceId: jest.fn().mockReturnValue(deviceId), - deleteMultipleDevices: jest.fn(), - generateClientSecret: jest.fn(), - setDeviceDetails: jest.fn(), - getAccountData: jest.fn(), - deleteAccountData: jest.fn(), - doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true), - getPushers: jest.fn(), - setPusher: jest.fn(), - setLocalNotificationSettings: jest.fn(), - getVersions: jest.fn().mockResolvedValue({}), - getCapabilities: jest.fn().mockResolvedValue({}), - getClientWellKnown: jest.fn().mockReturnValue({}), - }); + let mockClient!: MockedObject; + let sdkContext: SdkContextClass; const defaultProps = {}; const getComponent = (props = {}): React.ReactElement => ( - + - + ); const toggleDeviceDetails = ( @@ -230,6 +214,9 @@ describe("", () => { } }); + sdkContext = new SdkContextClass(); + sdkContext.client = mockClient; + // @ts-ignore allow delete of non-optional prop delete window.location; // @ts-ignore ugly mocking @@ -1051,12 +1038,11 @@ describe("", () => { describe("for an OIDC-aware server", () => { beforeEach(() => { - mockClient.getClientWellKnown.mockReturnValue({ - [M_AUTHENTICATION.name]: { - issuer: "https://issuer.org", - account: "https://issuer.org/account", - }, - }); + // just do an ugly mock here to avoid mocking initialisation + const mockOidcClientStore = { + accountManagementEndpoint: "https://issuer.org/account", + } as unknown as OidcClientStore; + jest.spyOn(sdkContext, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore); }); // signing out the current device works as usual diff --git a/test/utils/oidc/getDelegatedAuthAccountUrl-test.ts b/test/utils/oidc/getDelegatedAuthAccountUrl-test.ts deleted file mode 100644 index e4ba4c5756d..00000000000 --- a/test/utils/oidc/getDelegatedAuthAccountUrl-test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -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 { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; - -import { getDelegatedAuthAccountUrl } from "../../../src/utils/oidc/getDelegatedAuthAccountUrl"; - -describe("getDelegatedAuthAccountUrl()", () => { - it("should return undefined when wk is undefined", () => { - expect(getDelegatedAuthAccountUrl(undefined)).toBeUndefined(); - }); - - it("should return undefined when wk has no authentication config", () => { - expect(getDelegatedAuthAccountUrl({})).toBeUndefined(); - }); - - it("should return undefined when wk authentication config has no configured account url", () => { - expect( - getDelegatedAuthAccountUrl({ - [M_AUTHENTICATION.stable!]: { - issuer: "issuer.org", - }, - }), - ).toBeUndefined(); - }); - - it("should return the account url for authentication config using the unstable prefix", () => { - expect( - getDelegatedAuthAccountUrl({ - [M_AUTHENTICATION.unstable!]: { - issuer: "issuer.org", - account: "issuer.org/account", - }, - }), - ).toEqual("issuer.org/account"); - }); - - it("should return the account url for authentication config using the stable prefix", () => { - expect( - getDelegatedAuthAccountUrl({ - [M_AUTHENTICATION.stable!]: { - issuer: "issuer.org", - account: "issuer.org/account", - }, - }), - ).toEqual("issuer.org/account"); - }); -});