From 065a52cb9bc664c604eb637fa8efc38e56feb192 Mon Sep 17 00:00:00 2001 From: George Karr Date: Mon, 29 Jun 2026 11:55:24 -0500 Subject: [PATCH] gkarr 23242 fe (#47754) --- .../AddHostsModal/AddHostsModal.tests.tsx | 6 +- .../AndroidPanel/AndroidPanel.tsx | 4 +- .../IosIpadosPanel/IosIpadosPanel.tsx | 61 ++++++-- .../HostMdmStatusCell.tests.tsx | 10 +- frontend/interfaces/host.ts | 9 ++ frontend/interfaces/mdm.ts | 16 +- .../pages/DashboardPage/DashboardPage.tsx | 2 +- .../DashboardPage/cards/MDM/MDM.tests.tsx | 4 +- .../HostActionsDropdown.tests.tsx | 8 +- .../HostActionsDropdown.tsx | 14 ++ .../HostActionsDropdown/helpers.tsx | 75 ++++++++- .../HostDetailsPage/HostDetailsPage.tsx | 3 + .../ClearPasscodeModal/ClearPasscodeModal.tsx | 2 +- .../SelfService/SelfService.tests.tsx | 2 +- .../details/cards/Software/helpers.tests.ts | 8 +- .../hosts/details/cards/Software/helpers.tsx | 2 +- .../details/cards/Vitals/Vitals.tests.tsx | 8 +- frontend/templates/enroll-ota.html | 148 +++++++++++++++++- frontend/utilities/constants.tsx | 11 +- 19 files changed, 333 insertions(+), 60 deletions(-) diff --git a/frontend/components/AddHostsModal/AddHostsModal.tests.tsx b/frontend/components/AddHostsModal/AddHostsModal.tests.tsx index 3aff96d231..3fa223629b 100644 --- a/frontend/components/AddHostsModal/AddHostsModal.tests.tsx +++ b/frontend/components/AddHostsModal/AddHostsModal.tests.tsx @@ -116,9 +116,9 @@ describe("AddHostsModal", () => { ); await user.click(screen.getByRole("tab", { name: "iOS & iPadOS" })); - expect( - screen.queryByText(/Send this to your end users:/i) - ).toBeInTheDocument(); + expect(screen.queryByText(/Enrollment instructions:/i)).toBeInTheDocument(); + expect(screen.getByLabelText("Personal (BYOD)")).toBeInTheDocument(); + expect(screen.getByLabelText("Company-owned")).toBeInTheDocument(); }); it("renders enroll url input for android if android mdm is enabled", async () => { diff --git a/frontend/components/AddHostsModal/PlatformWrapper/AndroidPanel/AndroidPanel.tsx b/frontend/components/AddHostsModal/PlatformWrapper/AndroidPanel/AndroidPanel.tsx index 6990a65db1..835088695f 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/AndroidPanel/AndroidPanel.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/AndroidPanel/AndroidPanel.tsx @@ -66,7 +66,7 @@ const AndroidPanel = ({ enrollSecret }: IAndroidPanelProps) => { setEnrollmentType("workProfile")} @@ -74,7 +74,7 @@ const AndroidPanel = ({ enrollSecret }: IAndroidPanelProps) => { setEnrollmentType("fullyManaged")} diff --git a/frontend/components/AddHostsModal/PlatformWrapper/IosIpadosPanel/IosIpadosPanel.tsx b/frontend/components/AddHostsModal/PlatformWrapper/IosIpadosPanel/IosIpadosPanel.tsx index 07e387b93d..19b85c1aae 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/IosIpadosPanel/IosIpadosPanel.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/IosIpadosPanel/IosIpadosPanel.tsx @@ -1,16 +1,14 @@ -import React, { useContext } from "react"; +import React, { useContext, useState } from "react"; import CustomLink from "components/CustomLink"; import PATHS from "router/paths"; import { AppContext } from "context/app"; +import { getPathWithQueryParams } from "utilities/url"; import InputField from "components/forms/fields/InputField"; +import Radio from "components/forms/fields/Radio"; -const generateUrl = (serverUrl: string, enrollSecret: string) => { - return `${serverUrl}/enroll?enroll_secret=${encodeURIComponent( - enrollSecret - )}`; -}; +type EnrollmentType = "personal" | "companyOwned"; const baseClass = "ios-ipados-panel"; @@ -21,6 +19,11 @@ interface IosIpadosPanelProps { const IosIpadosPanel = ({ enrollSecret }: IosIpadosPanelProps) => { const { config, isMacMdmEnabledAndConfigured } = useContext(AppContext); + // Default to "Personal (BYOD)" per #23242 design. + const [enrollmentType, setEnrollmentType] = useState( + "personal" + ); + const helpText = "When the end user navigates to this URL, the enrollment profile " + "will download in their browser. End users will have to install the profile " + @@ -40,19 +43,45 @@ const IosIpadosPanel = ({ enrollSecret }: IosIpadosPanelProps) => { ); } - const url = generateUrl(config.server_settings.server_url, enrollSecret); + const url = getPathWithQueryParams( + `${config.server_settings.server_url}/enroll`, + { + enroll_secret: enrollSecret, + byod: enrollmentType === "personal" ? "true" : undefined, + } + ); return (
- +
+
+ setEnrollmentType("personal")} + /> + setEnrollmentType("companyOwned")} + /> +
+ +
); }; diff --git a/frontend/components/TableContainer/DataTable/HostMdmStatusCell/HostMdmStatusCell.tests.tsx b/frontend/components/TableContainer/DataTable/HostMdmStatusCell/HostMdmStatusCell.tests.tsx index 7234f853c1..82739d1e6b 100644 --- a/frontend/components/TableContainer/DataTable/HostMdmStatusCell/HostMdmStatusCell.tests.tsx +++ b/frontend/components/TableContainer/DataTable/HostMdmStatusCell/HostMdmStatusCell.tests.tsx @@ -33,9 +33,9 @@ describe("HostMdmStatusCell", () => { expect(screen.getByText("On (company-owned)")).toBeInTheDocument(); }); - it("renders 'On (BYOD)' for iOS hosts with personal enrollment", () => { - renderCell("ios", "On (personal)"); - expect(screen.getByText("On (BYOD)")).toBeInTheDocument(); + it("renders 'On (manual - personal)' for iOS hosts with personal enrollment", () => { + renderCell("ios", "On (manual - personal)"); + expect(screen.getByText("On (manual - personal)")).toBeInTheDocument(); }); it("renders 'Pending' for macOS hosts with pending enrollment", () => { @@ -44,8 +44,8 @@ describe("HostMdmStatusCell", () => { }); it("renders the MDM status for Android hosts", () => { - renderCell("android", "On (personal)"); - expect(screen.getByText("On (BYOD)")).toBeInTheDocument(); + renderCell("android", "On (manual - personal)"); + expect(screen.getByText("On (manual - personal)")).toBeInTheDocument(); }); it("renders the MDM status for Windows hosts", () => { diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index 72ac7ffb91..c433e5ba05 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -179,6 +179,15 @@ export interface IHostMdmData { device_status: HostMdmDeviceStatus; pending_action: HostMdmPendingAction; connected_to_fleet?: boolean; + /** + * wipe/lock/clear_passcode_allowed indicate whether the corresponding MDM + * commands are permitted for this host based on the AccessRights delivered + * in the host's manual (SCEP/ACME) enrollment profile. They are only + * populated for the host-details endpoint; absent on list-hosts payloads. + */ + wipe_allowed?: boolean; + lock_allowed?: boolean; + clear_passcode_allowed?: boolean; } export interface IHostMaintenanceWindow { diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index 22c70b0680..d4081b4cfc 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -61,7 +61,7 @@ export const getMdmServerUrl = ({ server_url }: IConfigServerSettings) => { export type MdmEnrollmentStatus = | "On (manual)" | "On (automatic)" - | "On (personal)" + | "On (manual - personal)" | "On (company-owned)" | "Off" | "Pending"; @@ -96,8 +96,8 @@ export const MDM_ENROLLMENT_STATUS_UI_MAP: Record< displayName: "On (company-owned)", filterValue: "automatic", }, - "On (personal)": { - displayName: "On (BYOD)", + "On (manual - personal)": { + displayName: "On (manual - personal)", filterValue: "personal", }, Off: { @@ -302,7 +302,7 @@ export const isEnrolledInMdm = ( return [ "On (automatic)", "On (manual)", - "On (personal)", + "On (manual - personal)", "On (company-owned)", ].includes(hostMdmEnrollmentStatus); }; @@ -314,11 +314,13 @@ export const isBYODManualEnrollment = ( }; /** This checks if the device is enrolled via an Apple ID user enrollment. - * We refer to that as "account driven user enrollment" */ + * We refer to that as "account driven user enrollment". Note that this same + * status now also covers manual BYOD enrollments (Apple) and Android BYO + * (work profile); see issue #23242. */ export const isBYODAccountDrivenUserEnrollment = ( enrollmentStatus: MdmEnrollmentStatus | null ) => { - return enrollmentStatus === "On (personal)"; + return enrollmentStatus === "On (manual - personal)"; }; /** This check is the device is enrolled via Automated Device Enrollment (ADE, also known as DEP) @@ -335,7 +337,7 @@ export const isAutomaticDeviceEnrollment = ( /** Android BYO (work profile, personally-owned) enrollment. */ export const isAndroidBYO = (enrollmentStatus: MdmEnrollmentStatus | null) => { - return enrollmentStatus === "On (personal)"; + return enrollmentStatus === "On (manual - personal)"; }; /** Android COBO (company-owned, fully managed) enrollment. */ diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index 0e93f2ea87..32bf7e65af 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -485,7 +485,7 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => { hosts: enrolled_automated_hosts_count, }, { - status: "On (personal)", + status: "On (manual - personal)", hosts: enrolled_personal_hosts_count, }, { status: "Off", hosts: unenrolled_hosts_count }, diff --git a/frontend/pages/DashboardPage/cards/MDM/MDM.tests.tsx b/frontend/pages/DashboardPage/cards/MDM/MDM.tests.tsx index 89c02cb0ae..19eb2b649d 100644 --- a/frontend/pages/DashboardPage/cards/MDM/MDM.tests.tsx +++ b/frontend/pages/DashboardPage/cards/MDM/MDM.tests.tsx @@ -43,7 +43,7 @@ describe("MDM Card", () => { mdmStatusData={[ { status: "On (automatic)", hosts: 10 }, { status: "On (manual)", hosts: 5 }, - { status: "On (personal)", hosts: 3 }, + { status: "On (manual - personal)", hosts: 3 }, { status: "Off", hosts: 1 }, { status: "Pending", hosts: 3 }, ]} @@ -65,7 +65,7 @@ describe("MDM Card", () => { ).toBeInTheDocument(); expect( screen.getByRole("row", { - name: /On \(BYOD\)(.*?)3 view all hosts/i, + name: /On \(manual - personal\)(.*?)3 view all hosts/i, }) ).toBeInTheDocument(); diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx index d6349ff92a..5011bd9af1 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx @@ -1557,7 +1557,7 @@ describe("Host Actions Dropdown", () => { onSelect={noop} hostStatus="online" hostPlatform="android" - hostMdmEnrollmentStatus="On (personal)" + hostMdmEnrollmentStatus="On (manual - personal)" isConnectedToFleetMdm hostMdmDeviceStatus="unlocked" hostScriptsEnabled={false} @@ -1785,7 +1785,7 @@ describe("Host Actions Dropdown", () => { ); }); - describe("personally enrolled hosts (e.g. enrollment status => On (personal)", () => { + describe("personally enrolled hosts (e.g. enrollment status => On (manual - personal))", () => { it("render only the Transfer and Delete options for personally enrolled ios host", async () => { const render = createCustomRenderer({ context: { @@ -1803,7 +1803,7 @@ describe("Host Actions Dropdown", () => { hostTeamId={null} onSelect={noop} hostStatus="online" - hostMdmEnrollmentStatus={"On (personal)"} + hostMdmEnrollmentStatus={"On (manual - personal)"} hostMdmDeviceStatus="unlocked" isConnectedToFleetMdm hostScriptsEnabled @@ -1843,7 +1843,7 @@ describe("Host Actions Dropdown", () => { hostTeamId={null} onSelect={noop} hostStatus="online" - hostMdmEnrollmentStatus={"On (personal)"} + hostMdmEnrollmentStatus={"On (manual - personal)"} isConnectedToFleetMdm hostMdmDeviceStatus="unlocked" hostScriptsEnabled diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx index 0f170197da..75a9cc2dac 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx @@ -29,6 +29,14 @@ interface IHostActionsDropdownProps { isManagedLocalAccountEnabled?: boolean; managedAccountStatus?: string | null; managedAccountPasswordAvailable?: boolean; + /** + * BYOD permission gates from the host MDM payload. Undefined when the host's + * stored AccessRights are not known (non-Apple-MDM or pre-#23242 hosts); + * treat undefined as "allowed" so the dropdown matches today's behavior. + */ + wipeAllowed?: boolean; + lockAllowed?: boolean; + clearPasscodeAllowed?: boolean; } const HostActionsDropdown = ({ @@ -48,6 +56,9 @@ const HostActionsDropdown = ({ isManagedLocalAccountEnabled = false, managedAccountStatus, managedAccountPasswordAvailable = false, + wipeAllowed, + lockAllowed, + clearPasscodeAllowed, }: IHostActionsDropdownProps) => { const { isPremiumTier = false, @@ -105,6 +116,9 @@ const HostActionsDropdown = ({ isManagedLocalAccountEnabled, managedAccountStatus, managedAccountPasswordAvailable, + wipeAllowed, + lockAllowed, + clearPasscodeAllowed, }); // No options to render. Exit early diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index 3d92e95915..ff8104fee2 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -118,6 +118,14 @@ interface IHostActionConfigOptions { isManagedLocalAccountEnabled: boolean; managedAccountStatus: string | null | undefined; managedAccountPasswordAvailable: boolean; + /** + * BYOD permission gates (issue #23242). Undefined when the host's stored + * AccessRights are not yet known; treat undefined as "allowed" to preserve + * pre-feature behavior. + */ + wipeAllowed?: boolean; + lockAllowed?: boolean; + clearPasscodeAllowed?: boolean; } const canTransferTeam = (config: IHostActionConfigOptions) => { @@ -540,12 +548,47 @@ const removeUnavailableOptions = ( return options; }; +// Tooltip copy for the BYOD-disabled state per issue #23242. Shown when the +// host's stored AccessRights bitmask omits the relevant bit. +const BYOD_DISABLED_TOOLTIPS: Record = { + wipe: ( + <> + Wipe permissions +
+ are disabled for this host. + + ), + lock: ( + <> + Lock permissions +
+ are disabled for this host. + + ), + clearPasscode: ( + <> + Clear passcode permissions +
+ are disabled for this host. + + ), +}; + // Available tooltips for disabled options export const getDropdownOptionTooltipContent = ( value: string | number, isHostOnline?: boolean, - scriptsGloballyDisabled?: boolean + scriptsGloballyDisabled?: boolean, + byodDisabled?: boolean ) => { + if ( + byodDisabled && + typeof value === "string" && + BYOD_DISABLED_TOOLTIPS[value] + ) { + return BYOD_DISABLED_TOOLTIPS[value]; + } + if (value === "runScript" && scriptsGloballyDisabled) { return <>Running scripts is disabled in organization settings.; } @@ -598,6 +641,9 @@ const modifyOptions = ( recoveryLockPasswordAvailable, managedAccountStatus, managedAccountPasswordAvailable, + wipeAllowed, + lockAllowed, + clearPasscodeAllowed, }: IHostActionConfigOptions ) => { const disableOptions = (optionsToDisable: IDropdownOption[]) => { @@ -611,6 +657,33 @@ const modifyOptions = ( }); }; + // BYOD-disabled options get a different tooltip. Each action maps to its + // own *Allowed flag; only treat the boolean false as disabled (undefined = + // unknown rights, leave the action enabled). + const byodDisableOptions = (optionsToDisable: IDropdownOption[]) => { + optionsToDisable.forEach((option) => { + option.disabled = true; + option.tooltipContent = getDropdownOptionTooltipContent( + option.value, + isHostOnline, + scriptsGloballyDisabled, + true + ); + }); + }; + + if (wipeAllowed === false) { + byodDisableOptions(options.filter((option) => option.value === "wipe")); + } + if (lockAllowed === false) { + byodDisableOptions(options.filter((option) => option.value === "lock")); + } + if (clearPasscodeAllowed === false) { + byodDisableOptions( + options.filter((option) => option.value === "clearPasscode") + ); + } + let optionsToDisable: IDropdownOption[] = []; // When the host is offline, always disable Query, but allow Unenroll for iOS/iPadOS and Android. if (!isHostOnline) { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index f04c8da9a7..a6e9e290e7 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -1088,6 +1088,9 @@ const HostDetailsPage = ({ host.mdm.os_settings?.managed_local_account?.password_available ?? false } + wipeAllowed={host.mdm.wipe_allowed} + lockAllowed={host.mdm.lock_allowed} + clearPasscodeAllowed={host.mdm.clear_passcode_allowed} /> ); }; diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/ClearPasscodeModal/ClearPasscodeModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/ClearPasscodeModal/ClearPasscodeModal.tsx index d91f8f1be9..a8f2adea5b 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/ClearPasscodeModal/ClearPasscodeModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/ClearPasscodeModal/ClearPasscodeModal.tsx @@ -33,7 +33,7 @@ const ClearPasscodeModal = ({ const isAndroidHost = isAndroid(hostPlatform); const isAndroidBYO = - isAndroidHost && hostMdmEnrollmentStatus === "On (personal)"; + isAndroidHost && hostMdmEnrollmentStatus === "On (manual - personal)"; const onClearPasscode = async () => { setIsClearingPasscode(true); diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx index 2865ebf2ea..b88c9cd42c 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tests.tsx @@ -304,7 +304,7 @@ describe("SelfService", () => { ); diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tests.ts b/frontend/pages/hosts/details/cards/Software/helpers.tests.ts index 63e5e18ab9..cb0092dd95 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tests.ts +++ b/frontend/pages/hosts/details/cards/Software/helpers.tests.ts @@ -481,10 +481,10 @@ describe("getUiStatus", () => { }); describe("getSoftwareSubheader", () => { - test("iOS device, MDM status 'On (personal)', my device page", () => { + test("iOS device, MDM status 'On (manual - personal)', my device page", () => { const result = getSoftwareSubheader({ platform: "ios", - hostMdmEnrollmentStatus: "On (personal)", + hostMdmEnrollmentStatus: "On (manual - personal)", isMyDevicePage: true, }); expect(result).toBe( @@ -492,10 +492,10 @@ describe("getSoftwareSubheader", () => { ); }); - test("iOS device, MDM status 'On (personal)', NOT my device page", () => { + test("iOS device, MDM status 'On (manual - personal)', NOT my device page", () => { const result = getSoftwareSubheader({ platform: "ios", - hostMdmEnrollmentStatus: "On (personal)", + hostMdmEnrollmentStatus: "On (manual - personal)", isMyDevicePage: false, }); expect(result).toBe( diff --git a/frontend/pages/hosts/details/cards/Software/helpers.tsx b/frontend/pages/hosts/details/cards/Software/helpers.tsx index 41b584efda..109509bca9 100644 --- a/frontend/pages/hosts/details/cards/Software/helpers.tsx +++ b/frontend/pages/hosts/details/cards/Software/helpers.tsx @@ -488,7 +488,7 @@ export const getSoftwareSubheader = ({ isMyDevicePage, }: IGetSoftwareSubheader): string => { if (isIPadOrIPhone(platform)) { - if (hostMdmEnrollmentStatus === "On (personal)") { + if (hostMdmEnrollmentStatus === "On (manual - personal)") { return isMyDevicePage ? "Software installed on your work profile (Managed Apple Account)." : "Software installed on work profile (Managed Apple Account)."; diff --git a/frontend/pages/hosts/details/cards/Vitals/Vitals.tests.tsx b/frontend/pages/hosts/details/cards/Vitals/Vitals.tests.tsx index 333971ee25..ac9611f24b 100644 --- a/frontend/pages/hosts/details/cards/Vitals/Vitals.tests.tsx +++ b/frontend/pages/hosts/details/cards/Vitals/Vitals.tests.tsx @@ -34,7 +34,7 @@ describe("Vitals Card component", () => { hardware_serial: "", uuid: "enrollment-id-12345", mdm: createMockHostMdmData({ - enrollment_status: "On (personal)", + enrollment_status: "On (manual - personal)", }), }); @@ -56,7 +56,7 @@ describe("Vitals Card component", () => { hardware_serial: "", uuid: "enrollment-id-12345", mdm: createMockHostMdmData({ - enrollment_status: "On (personal)", + enrollment_status: "On (manual - personal)", }), }); @@ -78,7 +78,7 @@ describe("Vitals Card component", () => { hardware_serial: "", uuid: "enrollment-id-12345", mdm: createMockHostMdmData({ - enrollment_status: "On (personal)", + enrollment_status: "On (manual - personal)", }), }); @@ -146,7 +146,7 @@ describe("Vitals Card component", () => { public_ip: "203.0.113.1", uuid: "enrollment-id-12345", mdm: createMockHostMdmData({ - enrollment_status: "On (personal)", + enrollment_status: "On (manual - personal)", }), }); diff --git a/frontend/templates/enroll-ota.html b/frontend/templates/enroll-ota.html index 2df479d66f..6bd3b5807b 100644 --- a/frontend/templates/enroll-ota.html +++ b/frontend/templates/enroll-ota.html @@ -197,6 +197,47 @@ text-align: center; } + .byod-tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; + } + + .byod-tab { + cursor: pointer; + padding: 6px 12px; + border-radius: 6px; + font-size: 14px; + line-height: 21px; + font-weight: 400; + color: #515774; + background: transparent; + border: none; + font-family: inherit; + } + + .byod-tab.active { + background-color: #f1f0ff; + color: #25234a; + font-weight: 600; + } + + .byod-info-banner { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 16px; + background-color: #f9fafc; + border-radius: 8px; + font-size: 14px; + margin-bottom: 24px; + } + + .byod-info-banner-icon { + flex-shrink: 0; + margin-top: 2px; + } + @media screen and (max-width: 1344px) and (pointer: coarse) { .device-instructions-content { gap: 24px; @@ -320,6 +361,24 @@ iPhone or iPad to Fleet +
+
+ + +
+
+ + + + Your organization can see and delete all device information. + +
+
  1. @@ -329,7 +388,7 @@ prompted, tap Allow.

    - Download + Download
  2. @@ -375,6 +434,24 @@

    How to turn on MDM on your Mac

    +
    +
    + + +
    +
    + + + + Your organization can see and delete all device information. + +
    +
    1. @@ -384,7 +461,7 @@ You'll see a warning, which is expected.

      - Download + Download
    2. @@ -628,6 +705,71 @@ } }; + // Wires up the Personal (BYOD) / Company-owned tabs that appear on the + // macOS, iOS, and iPadOS instruction screens. The active tab determines + // both the visible info-banner copy and whether the Download link's URL + // includes &byod=true (consumed by the OTA endpoint to strip lock/erase + // rights from the enrollment profile for personal devices). + // + // The default selection follows the byod URL query param (set by the + // Add hosts modal when an admin chooses "Personal (BYOD)"); only an + // explicit byod=true selects personal — everything else (absent param, + // byod=false, byod=0) defaults to company-owned. + const wireBYODTabs = () => { + const tabs = document.querySelectorAll(".byod-tab"); + if (tabs.length === 0) { + return; + } + const downloadLink = document.querySelector(".download-link"); + if (!downloadLink) { + return; + } + const baseHref = downloadLink.getAttribute("data-base-href") || downloadLink.getAttribute("href"); + + const params = new URLSearchParams(window.location.search); + const initial = + params.get("byod") === "true" || params.get("byod") === "1" + ? "personal" + : "company"; + + const setSelection = (selection) => { + tabs.forEach((tab) => { + const isActive = tab.getAttribute("data-byod") === selection; + tab.classList.toggle("active", isActive); + tab.setAttribute("aria-selected", isActive ? "true" : "false"); + }); + document + .querySelectorAll("[data-byod-content]") + .forEach((el) => { + el.hidden = el.getAttribute("data-byod-content") !== selection; + }); + // Append byod=true to the OTA download URL for BYOD; remove it for + // company-owned. Routing the DOM-sourced href through the URL parser + // keeps existing query params intact, resolves relative URLs against + // the origin, and avoids reinterpreting that value unsafely. + try { + const downloadUrl = new URL(baseHref, window.location.origin); + if (selection === "personal") { + downloadUrl.searchParams.set("byod", "true"); + } else { + downloadUrl.searchParams.delete("byod"); + } + downloadLink.setAttribute("href", downloadUrl.toString()); + } catch (e) { + // baseHref came from the DOM; if it can't be parsed as a URL, leave + // the server-rendered href untouched rather than reinterpreting it. + } + }; + + tabs.forEach((tab) => { + tab.addEventListener("click", () => { + setSelection(tab.getAttribute("data-byod")); + }); + }); + + setSelection(initial); + }; + const setEnrollTokenUrl = (url) => { document.querySelector(".enroll-link").setAttribute("href", url); }; @@ -804,6 +946,7 @@ window.location.href, document.querySelector(".qr-code") ); + wireBYODTabs(); } // handle rendering for ios and ipad @@ -828,6 +971,7 @@ renderContent(templateId); setIosIpadContent(platform); + wireBYODTabs(); } }); diff --git a/frontend/utilities/constants.tsx b/frontend/utilities/constants.tsx index df2af27e13..313175fc75 100644 --- a/frontend/utilities/constants.tsx +++ b/frontend/utilities/constants.tsx @@ -370,15 +370,14 @@ export const MDM_STATUS_TOOLTIP: Record< ), "On (manual)": ( - On Apple hosts, the enrollment profile was installed manually. Windows - hosts were enrolled without Autopilot. End users can turn MDM off. + Enrolled with a manual enrollment profile as a company-owned device. IT + admins can wipe this device and enforce all MDM restrictions. ), - "On (personal)": ( + "On (manual - personal)": ( - MDM was turned on by signing in with a Managed Apple Account on - iOS/iPadOS, or by adding a work profile on Android. End users can turn MDM - off. + Enrolled with a manual enrollment profile as a personal (BYOD) device. IT + admins cannot wipe this device or lock the end user out. ), "On (company-owned)": null,