From 83a637469c9944d69bff1331f031bb42d4f05e5f Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Mon, 6 Oct 2025 10:44:58 +0100 Subject: [PATCH] handle all three ios and ipad unenrollment states and diplay correct copy in unenroll modal (#33804) This is a quick fix to handle all three cases when showing the unenroll modal for ios and ipad devices. It also adds better naming to the functions that check the enrollment type. - [x] QA'd all new/changed functionality manually --- frontend/interfaces/mdm.ts | 31 +++++++-- .../hosts/ManageHostsPage/HostTableConfig.tsx | 6 +- .../HostActionsDropdown/helpers.tsx | 13 ++-- .../HostDetailsPage/HostDetailsPage.tsx | 12 ++-- .../UnenrollMdmModal/UnenrollMdmModal.tsx | 63 +++++++++++++------ .../pages/hosts/details/cards/About/About.tsx | 4 +- frontend/utilities/constants.tsx | 1 + 7 files changed, 88 insertions(+), 42 deletions(-) diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index 8472a518eb..1ba5fff994 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -49,6 +49,7 @@ export type MdmEnrollmentStatus = | "On (manual)" | "On (automatic)" | "On (personal)" + | "On (company-owned)" | "Off" | "Pending"; @@ -94,6 +95,10 @@ export const MDM_ENROLLMENT_STATUS_UI_MAP: Record< displayName: "Pending", filterValue: "pending", }, + "On (company-owned)": { + displayName: "On (company-owned)", + filterValue: "automatic", + }, }; export interface IMdmStatusCardData { @@ -261,7 +266,7 @@ export interface IMdmCommandResult { host_uuid: string; command_uuid: string; /** Status is the status of the command. It can be one of Acknowledged, Error, or NotNow for - // Apple, or 200, 400, etc for Windows. */ + // Apple, or 200, 400, etc for Windows. */ status: string; updated_at: string; request_type: string; @@ -278,14 +283,28 @@ export const isEnrolledInMdm = ( if (!hostMdmEnrollmentStatus) { return false; } - return ["On (automatic)", "On (manual)", "On (personal)"].includes( - hostMdmEnrollmentStatus - ); + return [ + "On (automatic)", + "On (manual)", + "On (personal)", + "On (company-owned)", + ].includes(hostMdmEnrollmentStatus); }; -/** determines if the host enrolled in mdm is a personal device */ -export const isPersonalEnrollmentInMdm = ( +export const isBYODManualEnrollment = ( + enrollmentStatus: MdmEnrollmentStatus | null +) => { + return enrollmentStatus === "On (manual)"; +}; + +export const isBYODAccountDrivenEnrollment = ( enrollmentStatus: MdmEnrollmentStatus | null ) => { return enrollmentStatus === "On (personal)"; }; + +export const isCompanyOwnedEnrollment = ( + enrollmentStatus: MdmEnrollmentStatus | null +) => { + return enrollmentStatus === "On (company-owned)"; +}; diff --git a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx index 775e4f2927..0a11caff0d 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx @@ -11,7 +11,7 @@ import { isAppleDevice, isMobilePlatform, } from "interfaces/platform"; -import { isPersonalEnrollmentInMdm } from "interfaces/mdm"; +import { isBYODAccountDrivenEnrollment } from "interfaces/mdm"; import TooltipWrapperArchLinuxRolling from "components/TooltipWrapperArchLinuxRolling"; import Checkbox from "components/forms/fields/Checkbox"; @@ -644,7 +644,9 @@ const allHostTableHeaders: IHostTableColumnConfig[] = [ // TODO(android): is iOS/iPadOS supported? if ( isAndroid(cellProps.row.original.platform) || - isPersonalEnrollmentInMdm(cellProps.row.original.mdm.enrollment_status) + isBYODAccountDrivenEnrollment( + cellProps.row.original.mdm.enrollment_status + ) ) { return NotSupported; } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index da174a5611..8408868241 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -10,7 +10,10 @@ import { isIPadOrIPhone, } from "interfaces/platform"; import { isScriptSupportedPlatform } from "interfaces/script"; -import { isPersonalEnrollmentInMdm, MdmEnrollmentStatus } from "interfaces/mdm"; +import { + isBYODAccountDrivenEnrollment, + MdmEnrollmentStatus, +} from "interfaces/mdm"; import { HostMdmDeviceStatusUIState, @@ -177,16 +180,16 @@ const canWipeHost = ({ const canWipeWindowsOrAppleOS = hostMdmEnabled && isConnectedToFleetMdm && isEnrolledInMdm; - // there is a special case for iOS and iPadOS devices that are personally enrolled + // there is a special case for iOS and iPadOS devices that are account driven enrolled // in MDM. These hosts cannot be wiped. - const isPersonallyEnrolledIosOrIpadDevice = + const isAccountDrivenEnrolledIosOrIpadosDevice = isIPadOrIPhone(hostPlatform) && - isPersonalEnrollmentInMdm(hostMdmEnrollmentStatus); + isBYODAccountDrivenEnrollment(hostMdmEnrollmentStatus); return ( isPremiumTier && !isAndroid(hostPlatform) && - !isPersonallyEnrolledIosOrIpadDevice && + !isAccountDrivenEnrolledIosOrIpadosDevice && hostMdmDeviceStatus === "unlocked" && (isLinuxLike(hostPlatform) || canWipeWindowsOrAppleOS) && (isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer) diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 9ffd8ee652..8d8953aa03 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -42,6 +42,7 @@ import { IHostCertificate, CERTIFICATES_DEFAULT_SORT, } from "interfaces/certificates"; +import { isBYODAccountDrivenEnrollment } from "interfaces/mdm"; import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; import permissions from "utilities/permissions"; @@ -59,7 +60,6 @@ import { isIPadOrIPhone, isLinuxLike, } from "interfaces/platform"; -import { isPersonalEnrollmentInMdm } from "interfaces/mdm"; import Spinner from "components/Spinner"; import TabNav from "components/TabNav"; @@ -1021,10 +1021,10 @@ const HostDetailsPage = ({ )} - {/* There is a special case for personally enrolled mdm hosts where we are not + {/* There is a special case for BYOD account driven enrolled mdm hosts where we are not currently supporting software installs. This check should be removed when we add that feature. */} - {isPersonalEnrollmentInMdm(host.mdm.enrollment_status) ? ( + {isBYODAccountDrivenEnrollment(host.mdm.enrollment_status) ? ( )} - {showUnenrollMdmModal && !!host && ( + {showUnenrollMdmModal && !!host && host.mdm.enrollment_status && ( )} diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/UnenrollMdmModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/UnenrollMdmModal.tsx index 08afed2ee4..dbcb439d3f 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/UnenrollMdmModal.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/UnenrollMdmModal/UnenrollMdmModal.tsx @@ -4,25 +4,32 @@ import DataError from "components/DataError"; import Button from "components/buttons/Button"; import Modal from "components/Modal"; import { NotificationContext } from "context/notification"; +import CustomLink from "components/CustomLink"; import mdmAPI from "services/entities/mdm"; import { isAndroid, isIPadOrIPhone } from "interfaces/platform"; +import { + isBYODAccountDrivenEnrollment, + isBYODManualEnrollment, + isCompanyOwnedEnrollment, + MdmEnrollmentStatus, +} from "interfaces/mdm"; + +const baseClass = "unenroll-mdm-modal"; interface IUnenrollMdmModalProps { hostId: number; hostPlatform: string; hostName: string; - isBYODEnrollment?: boolean; + enrollmentStatus: MdmEnrollmentStatus | null; onClose: () => void; } -const baseClass = "unenroll-mdm-modal"; - const UnenrollMdmModal = ({ hostId, hostPlatform, hostName, - isBYODEnrollment = false, + enrollmentStatus, onClose, }: IUnenrollMdmModalProps) => { const [requestState, setRequestState] = useState< @@ -66,27 +73,43 @@ const UnenrollMdmModal = ({ setRequestState(undefined); }; + const generateIosOrIpadosDescription = () => { + if (isBYODManualEnrollment(enrollmentStatus)) { + return ( +

+ To re-enroll, go to Hosts > Add hosts > iOS/iPadOS and + share the link with end user. +

+ ); + } else if (isBYODAccountDrivenEnrollment(enrollmentStatus)) { + return ( +

+ To re-enroll, ask your end user to navigate to{" "} + + Settings > General > VPN & Device Management > Sign in + to Work or School Account... + {" "} + on their host and to log in with their work email. +

+ ); + } else if (isCompanyOwnedEnrollment(enrollmentStatus)) { + return ( +

+ To re-enroll, make sure that the host is still in Apple Business + Manager (ABM). The host will automatically enroll after it's + reset. +

+ ); + } + return null; + }; + const generateDescription = () => { if (isIPadOrIPhone(hostPlatform)) { return ( <>

Settings configured by Fleet will be removed.

- {isBYODEnrollment ? ( -

- To re-enroll, ask your end user to navigate to{" "} - - Settings > General > VPN & Device Management > Sign - in to Work or School Account... - {" "} - on their host and to log in with their work email. -

- ) : ( -

- To re-enroll, make sure that the host is still in Apple Business - Manager (ABM). The host will automatically enroll after it's - reset. -

- )} + {generateIosOrIpadosDescription()} ); } diff --git a/frontend/pages/hosts/details/cards/About/About.tsx b/frontend/pages/hosts/details/cards/About/About.tsx index 797bca05bb..5cce829973 100644 --- a/frontend/pages/hosts/details/cards/About/About.tsx +++ b/frontend/pages/hosts/details/cards/About/About.tsx @@ -4,7 +4,7 @@ import classnames from "classnames"; import { IHostMdmData, IMunkiData } from "interfaces/host"; import { isAndroid, isIPadOrIPhone } from "interfaces/platform"; import { - isPersonalEnrollmentInMdm, + isBYODAccountDrivenEnrollment, MDM_ENROLLMENT_STATUS_UI_MAP, } from "interfaces/mdm"; import { @@ -55,7 +55,7 @@ const About = ({ aboutData, munki, mdm, className }: IAboutProps) => { // for all host types, we show the Enrollment ID dataset if the host // is enrolled in MDM personally. Personal (BYOD) devices do not report // their serial numbers, so we show the Enrollment ID instead. - if (mdm && isPersonalEnrollmentInMdm(mdm.enrollment_status)) { + if (mdm && isBYODAccountDrivenEnrollment(mdm.enrollment_status)) { deviceIdDataSet = ( ), + "On (company-owned)": null, Off: undefined, // no tooltip specified Pending: (