gkarr 23242 fe (#47754)
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -66,7 +66,7 @@ const AndroidPanel = ({ enrollSecret }: IAndroidPanelProps) => {
|
||||
<Radio
|
||||
name="enrollmentType"
|
||||
id="workProfile"
|
||||
label="Work profile"
|
||||
label="Personal (BYOD)"
|
||||
value="workProfile"
|
||||
checked={enrollmentType === "workProfile"}
|
||||
onChange={() => setEnrollmentType("workProfile")}
|
||||
@@ -74,7 +74,7 @@ const AndroidPanel = ({ enrollSecret }: IAndroidPanelProps) => {
|
||||
<Radio
|
||||
name="enrollmentType"
|
||||
id="fullyManaged"
|
||||
label="Fully-managed (no work profile)"
|
||||
label="Company-owned (fully-managed)"
|
||||
value="fullyManaged"
|
||||
checked={enrollmentType === "fullyManaged"}
|
||||
onChange={() => setEnrollmentType("fullyManaged")}
|
||||
|
||||
+45
-16
@@ -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<EnrollmentType>(
|
||||
"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 (
|
||||
<div className={baseClass}>
|
||||
<InputField
|
||||
label="Send this to your end users:"
|
||||
enableCopy
|
||||
readOnly
|
||||
inputWrapperClass={`${baseClass}__enroll-link`}
|
||||
name="enroll-link"
|
||||
value={url}
|
||||
helpText={helpText}
|
||||
/>
|
||||
<form>
|
||||
<fieldset className="form-field">
|
||||
<Radio
|
||||
name="iosIpadosEnrollmentType"
|
||||
id="iosIpadosPersonal"
|
||||
label="Personal (BYOD)"
|
||||
value="personal"
|
||||
checked={enrollmentType === "personal"}
|
||||
onChange={() => setEnrollmentType("personal")}
|
||||
/>
|
||||
<Radio
|
||||
name="iosIpadosEnrollmentType"
|
||||
id="iosIpadosCompanyOwned"
|
||||
label="Company-owned"
|
||||
value="companyOwned"
|
||||
checked={enrollmentType === "companyOwned"}
|
||||
onChange={() => setEnrollmentType("companyOwned")}
|
||||
/>
|
||||
</fieldset>
|
||||
<InputField
|
||||
label="Enrollment instructions:"
|
||||
enableCopy
|
||||
readOnly
|
||||
inputWrapperClass={`${baseClass}__enroll-link`}
|
||||
name="enroll-link"
|
||||
value={url}
|
||||
helpText={helpText}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-5
@@ -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", () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+14
@@ -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
|
||||
|
||||
@@ -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<string, JSX.Element> = {
|
||||
wipe: (
|
||||
<>
|
||||
Wipe permissions
|
||||
<br />
|
||||
are disabled for this host.
|
||||
</>
|
||||
),
|
||||
lock: (
|
||||
<>
|
||||
Lock permissions
|
||||
<br />
|
||||
are disabled for this host.
|
||||
</>
|
||||
),
|
||||
clearPasscode: (
|
||||
<>
|
||||
Clear passcode permissions
|
||||
<br />
|
||||
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) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
@@ -304,7 +304,7 @@ describe("SelfService", () => {
|
||||
<SelfService
|
||||
{...TEST_PROPS}
|
||||
isMobileView
|
||||
mdmEnrollmentStatus="On (personal)"
|
||||
mdmEnrollmentStatus="On (manual - personal)"
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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).";
|
||||
|
||||
@@ -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)",
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<span data-attribute="dynamic-device-type">iPhone or iPad</span> to
|
||||
Fleet
|
||||
</h1>
|
||||
<div>
|
||||
<div class="byod-tabs" role="tablist">
|
||||
<button type="button" class="byod-tab byod-tab--personal" role="tab" aria-selected="false" data-byod="personal">Personal (BYOD)</button>
|
||||
<button type="button" class="byod-tab byod-tab--company active" role="tab" aria-selected="true" data-byod="company">Company-owned</button>
|
||||
</div>
|
||||
<div class="byod-info-banner">
|
||||
<span class="byod-info-banner-icon" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="7.5" stroke="#515774"/>
|
||||
<path d="M8 7v4M8 5v.5" stroke="#515774" stroke-linecap="round" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="byod-info-banner-text">
|
||||
<span data-byod-content="personal" hidden>Your organization can only remotely remove work data and settings. They cannot wipe your device or lock you out.</span>
|
||||
<span data-byod-content="company">Your organization can see and delete all device information.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ol>
|
||||
<li>
|
||||
<p>
|
||||
@@ -329,7 +388,7 @@
|
||||
prompted, tap <b>Allow</b>.
|
||||
</span>
|
||||
</p>
|
||||
<a class="download-link" href="{{.EnrollURL}}">Download</a>
|
||||
<a class="download-link" href="{{.EnrollURL}}" data-base-href="{{.EnrollURL}}">Download</a>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
@@ -375,6 +434,24 @@
|
||||
<div class="content-with-sidebar">
|
||||
<section class="device-instructions-content">
|
||||
<h1>How to turn on MDM on your Mac</h1>
|
||||
<div>
|
||||
<div class="byod-tabs" role="tablist">
|
||||
<button type="button" class="byod-tab byod-tab--personal" role="tab" aria-selected="false" data-byod="personal">Personal (BYOD)</button>
|
||||
<button type="button" class="byod-tab byod-tab--company active" role="tab" aria-selected="true" data-byod="company">Company-owned</button>
|
||||
</div>
|
||||
<div class="byod-info-banner">
|
||||
<span class="byod-info-banner-icon" aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="7.5" stroke="#515774"/>
|
||||
<path d="M8 7v4M8 5v.5" stroke="#515774" stroke-linecap="round" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="byod-info-banner-text">
|
||||
<span data-byod-content="personal" hidden>Your organization can only remotely remove work data and settings. They cannot wipe your device or lock you out.</span>
|
||||
<span data-byod-content="company">Your organization can see and delete all device information.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ol>
|
||||
<li>
|
||||
<p>
|
||||
@@ -384,7 +461,7 @@
|
||||
You'll see a warning, which is expected.
|
||||
</span>
|
||||
</p>
|
||||
<a class="download-link" href="{{.EnrollURL}}">Download</a>
|
||||
<a class="download-link" href="{{.EnrollURL}}" data-base-href="{{.EnrollURL}}">Download</a>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -370,15 +370,14 @@ export const MDM_STATUS_TOOLTIP: Record<
|
||||
),
|
||||
"On (manual)": (
|
||||
<span>
|
||||
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.
|
||||
</span>
|
||||
),
|
||||
"On (personal)": (
|
||||
"On (manual - personal)": (
|
||||
<span>
|
||||
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.
|
||||
</span>
|
||||
),
|
||||
"On (company-owned)": null,
|
||||
|
||||
Reference in New Issue
Block a user