fleet/frontend/templates/enroll-ota.html
Noah Talerman 121625638f
/enroll page: Update copy (#42602)
- We use "BYO mobile" instead of "corporate mobile":
https://docs.google.com/document/d/1aVZ_eAiUjq1pdltR5ckwcbOXKB0DMzmboWZlegqJXDk/edit?tab=t.0
- Decided to just go with "mobile" because that's more familiar to end
users
- Context:
https://fleetdm.slack.com/archives/C03C41L5YEL/p1774377975564699
2026-03-30 09:44:21 -04:00

813 lines
24 KiB
HTML

<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" />
<link rel="shortcut icon" href="{{.URLPrefix}}/assets/favicon.ico" />
<title>Fleet</title>
<style nonce="{{.CSPNonce}}">
@font-face {
font-family: "Inter";
font-weight: 400;
src: url("../assets/fonts/inter/Inter-Regular.woff2") format("woff2"),
url("../assets/fonts/inter/Inter-Regular.woff") format("woff");
}
@font-face {
font-family: "Inter";
font-weight: 600;
src: url("../assets/fonts/inter/Inter-Semibold.woff2") format("woff2"),
url("../assets/fonts/inter/Inter-Semibold.woff") format("woff");
}
html {
box-sizing: border-box;
font-family: "Inter", sans-serif;
color: #515774;
line-height: 1.5;
font-size: 1.25rem;
}
*,
*:before,
*:after {
box-sizing: border-box;
}
body,
h1,
p {
margin: 0;
}
.download-link,
.enroll-link {
display: inline-block;
color: #fff;
background-color: #009a7d;
padding: 8px 16px;
border-radius: 4px;
text-decoration: none;
font-weight: 600;
font-size: 14px;
line-height: 21px;
text-align: center;
}
header {
padding: 13px 20px;
border-bottom: 1px solid #e2e4ea;
}
p {
font-size: 14px;
}
a {
color: #515774;
text-decoration: none;
font-weight: 600;
border-bottom: 1px solid #e2e4ea;
}
ol {
display: flex;
flex-direction: column;
gap: 48px;
list-style: none;
padding: 0;
margin: 0;
}
li {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.android-content {
margin-top: 48px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.switch-browser-content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.page-description {
font-size: 16px;
margin-bottom: 48px;
}
.profile-downloaded-container,
.install-profile-container {
width: 100%;
background-color: #f9fafc;
text-align: center;
}
.profile-downloaded-img,
.install-profile-img {
max-width: 100%;
}
.profile-downloaded-img {
max-height: 150px;
}
.install-profile-img {
max-height: 118px;
}
#main-content {
margin: 0 auto;
padding: 48px 24px;
max-width: 1000px;
}
.device-instructions-content {
display: flex;
flex-direction: column;
gap: 48px;
}
.content-with-sidebar {
display: flex;
justify-content: space-between;
gap: 64px;
}
.mobile-enroll-sidebar p {
max-width: 208px;
font-weight: bold;
}
.qr-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
background-color: #f9fafc;
padding: 24px;
border: 1px solid #e2e4ea;
border-radius: 8px;
}
.fully-managed-instructions .qr-container {
padding: 8px;
border-radius: 4px;
background-color: #fff;
}
.device-enroll-message {
margin-top: 48px;
display: flex;
gap: 8px;
}
.error-header {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
margin-bottom: 24px;
font-size: 14px;
}
.error-description {
text-align: center;
}
@media screen and (max-width: 430px) {
.device-instructions-content {
gap: 24px;
}
.enroll-link {
width: 100%;
}
}
</style>
</head>
<body>
<!-- unsupported platform content, not macos, ios, ipados, or android -->
<template id="unsupported-template">
<div class="content-with-sidebar">
<section class="device-instructions-content">
<h1>How to enroll your device to Fleet</h1>
<p class="page-description">
To enroll your Mac, please open this page on your device.
</p>
</section>
<section class="mobile-enroll-sidebar">
<div class="qr-container">
<p>Trying to enroll your mobile device?</p>
<canvas class="qr-code"></canvas>
</div>
</section>
</div>
</template>
<!-- android content when not fully managed-->
<template id="android-template">
<section class="device-instructions-content">
<h1>How to enroll your Android device to Fleet</h1>
<div class="android-content">
<p>Select <b>Enroll</b> and follow the steps.</p>
<a class="enroll-link" href="" target="_blank">Enroll</a>
<p>Already finished? You can close this tab.</p>
</div>
</section>
</template>
<!-- content when full managed is enabled. This will show for any platform, despite the copy referencing Android -->
<template id="fully-managed-template">
<section class="device-instructions-content">
<h1>How to enroll your Android device to Fleet</h1>
<ol class="fully-managed-instructions">
<li>
<p>
<span>1.</span>
<span>
Turn on your new device or
<a
target="_blank"
href="https://support.google.com/android/answer/6088915?hl=en"
>
factory reset it
<svg
width="9"
height="9"
viewBox="0 0 9 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="8"
height="8"
rx="2"
stroke="#515774"
/>
<path
d="M3.49992 2.83325H6.16659M6.16659 2.83325V5.49992M6.16659 2.83325L2.83325 6.16659"
stroke="#515774"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
</span>
</p>
</li>
<li>
<p>
<span>2.</span>
<span>
On the very first screen, tap the screen six times in a blank
area.
</span>
</p>
</li>
<li>
<p>
<span>3.</span>
<span>Scan QR code below with the QR reader that shows up.</span>
</p>
<div class="qr-container">
<canvas class="qr-code"></canvas>
</div>
</li>
<li>
<p>
<span>4.</span>
<span>
Connect to Wi-Fi and follow the instructions on the screen.
</span>
</p>
</li>
</ol>
<p>QR code invalid? Refresh the page.</p>
</section>
</template>
<!-- ios, ipados content -->
<template id="ios-ipad-template">
<section class="device-instructions-content">
<h1>
How to enroll your
<span data-attribute="dynamic-device-type">iPhone or iPad</span> to
Fleet
</h1>
<ol>
<li>
<p>
<span>1.</span>
<span>
Tap the button below to download the Fleet profile. When
prompted, tap <b>Allow</b>.
</span>
</p>
<a class="download-link" href="{{.EnrollURL}}">Download</a>
</li>
<li>
<p>
<span>2.</span>
<span>Go to <b>Settings > Profile Downloaded</b>.</span>
</p>
<div class="profile-downloaded-container">
<img
class="profile-downloaded-img"
src=""
alt="select profile downloaded in settings"
/>
</div>
</li>
<li>
<p>
<span>3.</span>
<span>
Tap <b>Install</b>. You'll see a few warnings, which is
expected. Tap <b>Install</b> again when prompted.
</span>
</p>
<div class="install-profile-container">
<img class="install-profile-img" src="" alt="select install" />
</div>
</li>
<li>
<p>
<span>4.</span>
<span>
When you see the <b>Remote Management</b> screen, tap
<b>Trust</b>.
</span>
</p>
</li>
</ol>
<p>Done! You can close this tab.</p>
</section>
</template>
<!-- macos content -->
<template id="macos-template">
<div class="content-with-sidebar">
<section class="device-instructions-content">
<h1>How to turn on MDM on your Mac</h1>
<ol>
<li>
<p>
<span>1.</span>
<span>
Tap the button below to download the Fleet profile. Open it.
You'll see a warning, which is expected.
</span>
</p>
<a class="download-link" href="{{.EnrollURL}}">Download</a>
</li>
<li>
<p>
<span>2.</span>
<span>
Go to the Apple menu in the top left corner of your screen.
Select
<b>System Settings</b>.
</span>
</p>
</li>
<li>
<p>
<span>3.</span>
<span>
Go to <b>Profile Downloaded</b>. Double-click the
<b>[Organization name] enrollment</b> profile.
</span>
</p>
</li>
<li>
<p>
<span>4.</span>
<span>Select <b>Install</b>. Enter your password.</span>
</p>
</li>
</ol>
<p>Done! You can close this tab.</p>
</section>
<section class="mobile-enroll-sidebar">
<div class="qr-container">
<p>Trying to enroll your mobile device?</p>
<canvas class="qr-code"></canvas>
</div>
</section>
</div>
</template>
<!-- Open in Safari content (iOS/iPadOS only) -->
<template id="open-in-safari-template">
<section class="device-instructions-content">
<h1>Please open in Safari</h1>
<div class="switch-browser-content">
<p>
To enroll your device, this page needs to be opened in
<b>Safari</b>.
</p>
<button class="enroll-link open-safari-btn">Open in Safari</button>
<p class="safari-fallback-msg" style="display: none">
URL copied to clipboard. Please open <b>Safari</b> and paste the
URL.
</p>
</div>
</section>
</template>
<!-- Error content -->
<template id="error-template">
<h1 class="error-header">
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 0.5C3.58 0.5 0 4.08 0 8.5C0 12.92 3.58 16.5 8 16.5C12.42 16.5 16 12.92 16 8.5C16 4.08 12.42 0.5 8 0.5ZM8 3.75C8.41421 3.75 8.75 4.08579 8.75 4.5V9.5C8.75 9.91421 8.41421 10.25 8 10.25C7.58579 10.25 7.25 9.91421 7.25 9.5V4.5C7.25 4.08579 7.58579 3.75 8 3.75ZM8 13.5C8.55229 13.5 9 13.0523 9 12.5C9 11.9477 8.55229 11.5 8 11.5C7.44772 11.5 7 11.9477 7 12.5C7 13.0523 7.44772 13.5 8 13.5Z"
fill="#D66C7B"
/>
</svg>
<p class="error-title"></p>
</h1>
<p class="error-description"></p>
</template>
<header>
<img src="{{.URLPrefix}}/assets/images/fleet-logo.svg" />
</header>
<!-- container for the dynamic content -->
<section id="main-content"></section>
<!-- using qrcode library to generate QR codes -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/qrcode/1.5.1/qrcode.min.js"
integrity="sha512-PEhlWBZBrQL7flpJPY8lXx8tIN7HWX912GzGhFTDqA3iWFrakVH3lVHomCoU9BhfKzgxfEk6EG2C3xej+9srOQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
defer
nonce="{{.CSPNonce}}"
></script>
<script nonce="{{.CSPNonce}}">
const ANDROID_GET_TOKEN_URL =
"{{.URLPrefix}}/api/v1/fleet/android_enterprise/enrollment_token";
// using string comparison to satisfy the linter.
const ANDROID_MDM_ENABLED = "{{.AndroidMDMEnabled}}" === "true";
const MAC_MDM_ENABLED = "{{.MacMDMEnabled}}" == "true";
const ERROR_MESSAGE = "{{.ErrorMessage}}";
const isFullyManaged = new URLSearchParams(window.location.search).has(
"fully_managed",
true
);
const getPlatform = () => {
const userAgent = navigator.userAgent;
const isIPhone = /iPhone/i.test(userAgent);
const isIPad =
/iPad/i.test(userAgent) ||
(/Macintosh/i.test(userAgent) &&
navigator.maxTouchPoints !== undefined &&
navigator.maxTouchPoints > 1);
const isAndroid = /Android/i.test(userAgent);
const isMac =
(/Macintosh/i.test(userAgent) && !isIPad) ||
/Mac OS X/i.test(userAgent);
switch (true) {
case isAndroid:
return "android";
case isIPhone:
return "ios";
case isIPad:
return "ipad";
case isMac:
return "macos";
default:
return "unsupported";
}
};
// Detects if the browser is Safari on iOS/iPadOS
// Other browsers on iOS: CriOS (Chrome), FxiOS (Firefox), EdgiOS (Edge), OPiOS or OPT (Opera)
// Brave (Brave)
const isSafariBrowser = () => {
const ua = navigator.userAgent;
return (
/Safari/i.test(ua) &&
!/CriOS/i.test(ua) &&
!/FxiOS/i.test(ua) &&
!/EdgiOS/i.test(ua) &&
!/OPiOS/i.test(ua) &&
!/OPT/i.test(ua) &&
!/Brave/i.test(ua)
);
};
// Attempts to open the current URL in Safari, falls back to clipboard copy
const openInSafari = async () => {
const currentUrl = window.location.href;
const safariUrl = currentUrl.replace(
/^https?:\/\//,
"x-safari-https://"
);
// Track if the page loses focus (indicates Safari opened)
let didNavigate = false;
const handleBlur = () => {
didNavigate = true;
};
window.addEventListener("blur", handleBlur);
// Try to open Safari via URL scheme
window.location.href = safariUrl;
// Wait a moment to see if navigation occurred
setTimeout(async () => {
window.removeEventListener("blur", handleBlur);
if (!didNavigate) {
// URL scheme didn't work, fall back to clipboard
try {
await navigator.clipboard.writeText(currentUrl);
const fallbackMsg = document.querySelector(
".safari-fallback-msg"
);
const openBtn = document.querySelector(".open-safari-btn");
if (fallbackMsg) {
fallbackMsg.style.display = "block";
}
if (openBtn) {
openBtn.textContent = "URL Copied";
openBtn.disabled = true;
openBtn.style.backgroundColor = "#6A6A6A";
}
} catch (err) {
console.error("Failed to copy URL to clipboard", err);
}
}
}, 500);
};
/**
* Renders a QR code for the given text into the specified element.
* @param {string} text - The text to encode in the QR code.
* @param {HTMLElement} element - The HTML element where the QR code will be rendered.
* @param {Object} [options] - Optional configuration for the QR code (e.g., width, margin,
* color). valid options can be found here:
* https://github.com/soldair/node-qrcode?tab=readme-ov-file#options-9
*/
const renderQRCode = (text, element, options) => {
let QROptions = {
width: 208,
margin: 0,
color: { dark: "#515774", light: "#F9FAFC" },
};
if (options) {
QROptions = { ...QROptions, ...options };
}
QRCode.toCanvas(element, text, QROptions, function (error) {
if (error) console.error("problem with rendering QR code", error);
});
};
const getTemplateIdFromPlatform = (platform) => {
switch (true) {
case platform === "android":
return "android-template";
case platform === "ios" || platform === "ipad":
return "ios-ipad-template";
case platform === "macos":
return "macos-template";
default:
return "unsupported-template";
}
};
// renders the content based on the template id
const renderContent = (templateId) => {
const template = document.getElementById(templateId);
if (template) {
document
.getElementById("main-content")
.appendChild(template.content.cloneNode(true));
}
};
const setEnrollTokenUrl = (url) => {
document.querySelector(".enroll-link").setAttribute("href", url);
};
// dynmaic content rendering for ios and ipad only
const setIosIpadContent = (platform) => {
if (platform === "ios") {
deviceType = "iPhone";
} else if (platform === "ipad") {
deviceType = "iPad";
}
document
.querySelectorAll('[data-attribute="dynamic-device-type"]')
.forEach((el) => {
el.textContent = deviceType;
});
// update image src based on OS
const osImagePrefix = platform === "ios" ? "ios" : "iPadOS";
document.querySelector(
".profile-downloaded-img"
).src = `{{.URLPrefix}}/assets/images/${osImagePrefix}-profile-downloaded.png`;
document.querySelector(
".install-profile-img"
).src = `{{.URLPrefix}}/assets/images/${osImagePrefix}-install-profile.png`;
};
const renderError = (title, description) => {
const template = document.getElementById("error-template");
if (template) {
const clone = template.content.cloneNode(true);
clone.querySelector(".error-title").textContent = title;
clone.querySelector(".error-description").textContent = description;
document.getElementById("main-content").appendChild(clone);
}
};
const getEnrollmentSecret = () => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get("enroll_secret");
};
const getEnrollmentToken = async (fullyManaged) => {
const enrollSecret = getEnrollmentSecret();
let url = `${ANDROID_GET_TOKEN_URL}?enroll_secret=${encodeURIComponent(
enrollSecret
)}`;
if (fullyManaged) {
url = url.concat("&fully_managed=true");
}
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to get enrollment token");
}
return await response.json();
};
// here we render the page content
document.addEventListener("DOMContentLoaded", async function () {
// if there is an error message, render it
if (ERROR_MESSAGE) {
// format of error message is expected to use colon as separator character, e.g., "An error occurred. : Failed to parse original URL."
const [title, description] = ERROR_MESSAGE.split(":");
renderError(title.trim(), description.trim());
return;
}
// check if there is an enrollment secret
if (!getEnrollmentSecret()) {
renderError(
"This URL is invalid.",
"Enroll secret is missing. Please contact your IT admin."
);
return;
}
const platform = getPlatform();
if (isFullyManaged) {
if (platform === "android" && !ANDROID_MDM_ENABLED) {
renderError(
"Android MDM is turned off.",
"To enroll your Android device please contact your IT admin."
);
return;
}
try {
const data = await getEnrollmentToken(true);
if (!data.android_enrollment_qr_code) {
throw new Error("Failed to get QR code");
}
renderContent("fully-managed-template");
renderQRCode(
data.android_enrollment_qr_code,
document.querySelector(".qr-code"),
{ width: 245, color: { dark: "#515774", light: "#fff" } }
);
} catch (error) {
renderError(
"Couldn't get Android enrollment token.",
"Please refresh the page. If the issue continues, contact your IT admin."
);
return;
}
return;
}
// render unsupported platform content. Unsupported means not macos, ios, ipad, or android
if (platform === "unsupported") {
renderContent("unsupported-template");
renderQRCode(
window.location.href,
document.querySelector(".qr-code")
);
return;
}
let templateId = getTemplateIdFromPlatform(platform);
// handle android rendering
if (platform === "android") {
if (!ANDROID_MDM_ENABLED) {
renderError(
"Android MDM is turned off.",
"To enroll your Android device please contact your IT admin."
);
return;
}
// we need to get the token and then place it in the enroll link
try {
const data = await getEnrollmentToken();
renderContent(templateId);
if (!data.android_enrollment_url) {
throw new Error("Failed to get enrollment token");
}
setEnrollTokenUrl(data.android_enrollment_url);
} catch (error) {
renderError(
"Couldn't get Android enrollment token.",
"Please refresh the page. If the issue continues, contact your IT admin."
);
return;
}
}
// handle rendering for macos
if (platform === "macos") {
if (!MAC_MDM_ENABLED) {
renderError(
"Apple MDM is turned off.",
"To enroll your Mac device please contact your IT admin."
);
return;
}
renderContent(templateId);
renderQRCode(
window.location.href,
document.querySelector(".qr-code")
);
}
// handle rendering for ios and ipad
if (platform === "ios" || platform === "ipad") {
if (!MAC_MDM_ENABLED) {
const description = `To enroll your ${
platform === "ios" ? "iPhone" : "iPad"
} please contact your IT admin.`;
renderError("Apple MDM is turned off.", description);
return;
}
// Check if user is NOT in Safari - redirect them to Safari
if (!isSafariBrowser()) {
renderContent("open-in-safari-template");
const openBtn = document.querySelector(".open-safari-btn");
if (openBtn) {
openBtn.addEventListener("click", openInSafari);
}
return;
}
renderContent(templateId);
setIosIpadContent(platform);
}
});
</script>
</body>
</html>