From bdf2ebd30131e4d7fae4231bab968915d1a13e74 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 13 Oct 2023 10:43:39 +0100 Subject: [PATCH] Avoid rendering app download buttons if disabled in config (#11741) * Add default desktop_builds and mobile_builds into SdkConfig.DEFAULTS Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Avoid rendering app download buttons if config sets to `null` Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Disable app download onboarding task if config has no apps to download Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tests and update types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix types Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/IConfigOptions.ts | 10 +- src/SdkConfig.ts | 11 + src/components/structures/MatrixChat.tsx | 11 +- .../views/dialogs/AppDownloadDialog.tsx | 130 +++-- src/hooks/useUserOnboardingTasks.ts | 11 +- .../views/dialogs/AppDownloadDialog-test.tsx | 80 +++ .../AppDownloadDialog-test.tsx.snap | 540 ++++++++++++++++++ 7 files changed, 723 insertions(+), 70 deletions(-) create mode 100644 test/components/views/dialogs/AppDownloadDialog-test.tsx create mode 100644 test/components/views/dialogs/__snapshots__/AppDownloadDialog-test.tsx.snap diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 2df5e7fca1..0f4eab1b93 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -71,15 +71,15 @@ export interface IConfigOptions { permalink_prefix?: string; update_base_url?: string; - desktop_builds?: { + desktop_builds: { available: boolean; logo: string; // url url: string; // download url }; - mobile_builds?: { - ios?: string; // download url - android?: string; // download url - fdroid?: string; // download url + mobile_builds: { + ios: string | null; // download url + android: string | null; // download url + fdroid: string | null; // download url }; mobile_guide_toast?: boolean; diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index a12eb34d78..0dbc4f2448 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -61,6 +61,17 @@ export const DEFAULTS: DeepReadonly = { "https://github.com/vector-im/element-web/issues?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc", new_issue_url: "https://github.com/vector-im/element-web/issues/new/choose", }, + + desktop_builds: { + available: true, + logo: "vector-icons/1024.png", + url: "https://element.io/download", + }, + mobile_builds: { + ios: "https://apps.apple.com/app/vector/id1083446067", + android: "https://play.google.com/store/apps/details?id=im.vector.app", + fdroid: "https://f-droid.org/repository/browse/?fdid=im.vector.app", + }, }; export type ConfigOptions = Defaultize; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b1473e392d..dde4768e8e 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -44,7 +44,7 @@ import PosthogTrackers from "../../PosthogTrackers"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; -import SdkConfig from "../../SdkConfig"; +import SdkConfig, { ConfigOptions } from "../../SdkConfig"; import dis from "../../dispatcher/dispatcher"; import Notifier from "../../Notifier"; import Modal from "../../Modal"; @@ -122,7 +122,6 @@ import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePaylo import { AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeaveRoomPayload"; import { DoAfterSyncPreparedPayload } from "../../dispatcher/payloads/DoAfterSyncPreparedPayload"; import { ViewStartChatOrReusePayload } from "../../dispatcher/payloads/ViewStartChatOrReusePayload"; -import { IConfigOptions } from "../../IConfigOptions"; import { SnakedObject } from "../../utils/SnakedObject"; import { leaveRoomBehaviour } from "../../utils/leave-behaviour"; import { CallStore } from "../../stores/CallStore"; @@ -165,7 +164,7 @@ interface IScreen { } interface IProps { - config: IConfigOptions; + config: ConfigOptions; onNewScreen: (screen: string, replaceLast: boolean) => void; enableGuest?: boolean; // the queryParams extracted from the [real] query-string of the URI @@ -1138,7 +1137,7 @@ export default class MatrixChat extends React.PureComponent { } private chatCreateOrReuse(userId: string): void { - const snakedConfig = new SnakedObject(this.props.config); + const snakedConfig = new SnakedObject(this.props.config); // Use a deferred action to reshow the dialog once the user has registered if (MatrixClientPeg.safeGet().isGuest()) { // No point in making 2 DMs with welcome bot. This assumes view_set_mxid will @@ -1295,7 +1294,7 @@ export default class MatrixChat extends React.PureComponent { * @returns {string} The room ID of the new room, or null if no room was created */ private async startWelcomeUserChat(): Promise { - const snakedConfig = new SnakedObject(this.props.config); + const snakedConfig = new SnakedObject(this.props.config); const welcomeUserId = snakedConfig.get("welcome_user_id"); if (!welcomeUserId) return null; @@ -1390,7 +1389,7 @@ export default class MatrixChat extends React.PureComponent { } else if (MatrixClientPeg.currentUserIsJustRegistered()) { MatrixClientPeg.setJustRegisteredUserId(null); - const snakedConfig = new SnakedObject(this.props.config); + const snakedConfig = new SnakedObject(this.props.config); if (snakedConfig.get("welcome_user_id") && getCurrentLanguage().startsWith("en")) { const welcomeUserRoom = await this.startWelcomeUserChat(); if (welcomeUserRoom === null) { diff --git a/src/components/views/dialogs/AppDownloadDialog.tsx b/src/components/views/dialogs/AppDownloadDialog.tsx index a4acd80d20..63e8cd44f2 100644 --- a/src/components/views/dialogs/AppDownloadDialog.tsx +++ b/src/components/views/dialogs/AppDownloadDialog.tsx @@ -26,24 +26,32 @@ import QRCode from "../elements/QRCode"; import Heading from "../typography/Heading"; import BaseDialog from "./BaseDialog"; -const fallbackAppStore = "https://apps.apple.com/app/vector/id1083446067"; -const fallbackGooglePlay = "https://play.google.com/store/apps/details?id=im.vector.app"; -const fallbackFDroid = "https://f-droid.org/repository/browse/?fdid=im.vector.app"; - interface Props { onFinished(): void; } +export const showAppDownloadDialogPrompt = (): boolean => { + const desktopBuilds = SdkConfig.getObject("desktop_builds"); + const mobileBuilds = SdkConfig.getObject("mobile_builds"); + + return ( + !!desktopBuilds?.get("available") || + !!mobileBuilds?.get("ios") || + !!mobileBuilds?.get("android") || + !!mobileBuilds?.get("fdroid") + ); +}; + export const AppDownloadDialog: FC = ({ onFinished }) => { const brand = SdkConfig.get("brand"); const desktopBuilds = SdkConfig.getObject("desktop_builds"); const mobileBuilds = SdkConfig.getObject("mobile_builds"); - const urlAppStore = mobileBuilds?.get("ios") ?? fallbackAppStore; + const urlAppStore = mobileBuilds?.get("ios"); - const urlAndroid = mobileBuilds?.get("android") ?? mobileBuilds?.get("fdroid") ?? fallbackGooglePlay; - const urlGooglePlay = mobileBuilds?.get("android") ?? fallbackGooglePlay; - const urlFDroid = mobileBuilds?.get("fdroid") ?? fallbackFDroid; + const urlGooglePlay = mobileBuilds?.get("android"); + const urlFDroid = mobileBuilds?.get("fdroid"); + const urlAndroid = urlGooglePlay ?? urlFDroid; return ( = ({ onFinished }) => { )}
-
- {_t("common|ios")} - -
- {_t("onboarding|qr_or_app_links", { - appLinks: "", - qrCode: "", - })} + {urlAppStore && ( +
+ {_t("common|ios")} + +
+ {_t("onboarding|qr_or_app_links", { + appLinks: "", + qrCode: "", + })} +
+
+ {}} + > + + +
-
- {}} - > - - + )} + {urlAndroid && ( +
+ {_t("common|android")} + +
+ {_t("onboarding|qr_or_app_links", { + appLinks: "", + qrCode: "", + })} +
+
+ {urlGooglePlay && ( + {}} + > + + + )} + {urlFDroid && ( + {}} + > + + + )} +
-
-
- {_t("common|android")} - -
- {_t("onboarding|qr_or_app_links", { - appLinks: "", - qrCode: "", - })} -
-
- {}} - > - - - {}} - > - - -
-
+ )}

{_t("onboarding|apple_trademarks")}

diff --git a/src/hooks/useUserOnboardingTasks.ts b/src/hooks/useUserOnboardingTasks.ts index f48fd1cdd9..8dc06efa5b 100644 --- a/src/hooks/useUserOnboardingTasks.ts +++ b/src/hooks/useUserOnboardingTasks.ts @@ -16,7 +16,7 @@ limitations under the License. import { useMemo } from "react"; -import { AppDownloadDialog } from "../components/views/dialogs/AppDownloadDialog"; +import { AppDownloadDialog, showAppDownloadDialogPrompt } from "../components/views/dialogs/AppDownloadDialog"; import { UserTab } from "../components/views/dialogs/UserTab"; import { ButtonEvent } from "../components/views/elements/AccessibleButton"; import { Action } from "../dispatcher/actions"; @@ -42,6 +42,7 @@ interface UserOnboardingTask { hideOnComplete?: boolean; }; completed: (ctx: UserOnboardingContext) => boolean; + disabled?(): boolean; } export interface UserOnboardingTaskWithResolvedCompletion extends Omit { @@ -111,6 +112,9 @@ const tasks: UserOnboardingTask[] = [ Modal.createDialog(AppDownloadDialog, {}, "mx_AppDownloadDialog_wrapper", false, true); }, }, + disabled(): boolean { + return !showAppDownloadDialogPrompt(); + }, }, { id: "setup-profile", @@ -149,7 +153,10 @@ export function useUserOnboardingTasks(context: UserOnboardingContext): UserOnbo return useMemo(() => { return tasks - .filter((task) => !task.relevant || task.relevant.includes(useCase)) + .filter((task) => { + if (task.disabled?.()) return false; + return !task.relevant || task.relevant.includes(useCase); + }) .map((task) => ({ ...task, completed: task.completed(context), diff --git a/test/components/views/dialogs/AppDownloadDialog-test.tsx b/test/components/views/dialogs/AppDownloadDialog-test.tsx new file mode 100644 index 0000000000..aa9abf6bec --- /dev/null +++ b/test/components/views/dialogs/AppDownloadDialog-test.tsx @@ -0,0 +1,80 @@ +/* +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 React from "react"; +import { render, screen } from "@testing-library/react"; + +import { AppDownloadDialog } from "../../../../src/components/views/dialogs/AppDownloadDialog"; +import SdkConfig, { ConfigOptions } from "../../../../src/SdkConfig"; + +describe("AppDownloadDialog", () => { + afterEach(() => { + SdkConfig.reset(); + }); + + it("should render with desktop, ios, android, fdroid buttons by default", () => { + const { asFragment } = render(); + expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should allow disabling fdroid build", () => { + SdkConfig.add({ + mobile_builds: { + fdroid: null, + }, + } as ConfigOptions); + const { asFragment } = render(); + expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should allow disabling desktop build", () => { + SdkConfig.add({ + desktop_builds: { + available: false, + }, + } as ConfigOptions); + const { asFragment } = render(); + expect(screen.queryByRole("button", { name: "Download Element Desktop" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Download on the App Store" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get it on Google Play" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should allow disabling mobile builds", () => { + SdkConfig.add({ + mobile_builds: { + ios: null, + android: null, + fdroid: null, + }, + } as ConfigOptions); + const { asFragment } = render(); + expect(screen.queryByRole("button", { name: "Download Element Desktop" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Download on the App Store" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get it on Google Play" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get it on F-Droid" })).not.toBeInTheDocument(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/dialogs/__snapshots__/AppDownloadDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/AppDownloadDialog-test.tsx.snap new file mode 100644 index 0000000000..e33eacfd9a --- /dev/null +++ b/test/components/views/dialogs/__snapshots__/AppDownloadDialog-test.tsx.snap @@ -0,0 +1,540 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AppDownloadDialog should allow disabling desktop build 1`] = ` + +
+