Traps
Everything on this page cost somebody a day. None of it is in Microsoft's documentation, and most of it was found by running the SDK on a physical device against a real tenant rather than by reading anything.
The colour is a risk model, not a tone of voice, and it is the same scale
npx react-native-intune doctor uses:
| Means | |
|---|---|
| Silent | It builds, it runs, and it protects nothing. Nobody finds out. |
| Loud | The build breaks or the first sign-in fails. Costs an hour, not a breach. |
| Measured | Observed on a device. Safe to rely on. |
| Worth knowing | Not a defect. It will surprise you anyway. |
Silent — nothing fails, nothing logs
Applied to this library instead of your app module, the plugin rewrites only the library's own bytecode. The build is clean, the app runs, and no policy is enforced. Since SDK 8.0 libraries cannot be processed selectively, so there is no partial success to notice.
No error and no warning. The SDK is linked and simply never gets the chance to enforce
anything. If you have no Application subclass at all, set android:name in the manifest
instead — doctor says which case you are in.
Microsoft's own documentation notes that policy applies successfully and the app is still unprotected. There is no signal of any kind at runtime.
IntuneMAMPolicyManager.policy returns the policy for the calling thread's identity.
On React Native's JavaScript thread that is not your signed-in account: it is a non-nil,
permissive stub, so every check passes and every restriction reads as "not required".
Use policyForAccountId: with the primary account instead. This library does, but anyone
wrapping the SDK by hand will hit it — the symptom is a policy that never appears to
change no matter what the administrator does.
succeeded as "block the user"notLicensed and notTargeted mean the opposite of failed, and that distinction is
prescribed by Microsoft rather than invented here. Blocking on them locks out every
employee without an Intune licence, and every pilot where the policy has not been targeted
yet — while the app reports itself as secure. See
Enrollment outcomes.
Loud — it breaks, but the error names the wrong cause
msauthv2://broker, not msauthv2://canOpenURL: with the bare scheme returns false even with Authenticator installed, so
broker detection reports "no broker" on a device that has one. MSAL's own
isRequiredBrokerPresent uses the //broker host, which is how this was found.
Without com.microsoft.adalcache in the entitlements, the broker acquires a token the SDK
cannot see. Enrollment then fails with "could not access the user's AAD token" even though
sign-in succeeded — an error that points at identity rather than at entitlements.
Your own bundle ID must be first in the list: without an explicit access group, iOS writes to the first group in the entitlements, so a Microsoft group in that position would receive keychain items your app writes.
<queries> on Android 11+ means MSAL falls back to a browser, silentlyPackage visibility stops your app from even detecting the broker. MSAL then uses a browser instead and brokered auth "just doesn't work" with no error anywhere.
The redirect URI registered in Entra percent-encodes / and =; the manifest's
android:path uses the decoded form. Same value, two spellings, in two places that must
agree. An EAS build is signed with a keystore Expo manages, so the hash comes from
eas credentials — it cannot be derived.
reset() leaves the module unconfiguredBy design: it clears the runtime configuration along with the enrollment. The next call
then rejects with E_NOT_CONFIGURED, which reads like a bug in the module. Call
configure() again after a reset.
enroll() for an already-registered account hangsRoughly 90 seconds, on both platforms, and then nothing. The SDK skips accounts it has
already registered without reporting anything. This library short-circuits it and returns
immediately; if you call the SDK directly, check enrolledAccountId first.
assembleRelease succeeds only because the React Native template ships
enableProguardInReleaseBuilds = false, so R8 never runs. Turn it on and the build fails
outright on missing classes — including com.microsoft.device.display, which this library
excludes deliberately because it is published only to Microsoft's own feed. This library
ships consumer ProGuard rules for that; a hand integration needs its own -dontwarn
entries.
openid, profile and offline_access are added by MSAL itself and passing them
explicitly is an error. Ask for your own API's scopes only.
signIn when an account is already presentSingle-account mode throws "An account is already signed in." rather than returning the
cached account. Try the cache first and only fall back to interactive sign-in — which is
why signIn behaves slightly differently on the two platforms. See
Authentication.
Measured on a device
The SDK asks for a token before it has an identity to ask for, so the first request of a process is expected to fail and be retried. It is not a misconfiguration and it does not need handling.
It reads the MSAL account cache, so you do not have to supply a UPN alongside the account identifier on iOS. Android needs both, in the order the class files dictate.
Measured on an iPhone 17 Pro simulator, Release, three cold launches of a build that calls
configure() against three of a build that does not:
with configure() | without | |
|---|---|---|
| SDK first activity | 55 ms | never |
| remote config fetch | 465 ms | never |
| configuration scan | 1120 ms | never |
| on-launch diagnostics | 4198 ms | never |
An app that links this library and never configures it pays the binary size — about
10.7 MB on iOS, 1.3 MB on Android — and nothing at launch. Everything above, including a
network request to config.edge.skype.com, happens because configure() ran.
Useful the other way round too: once you configure, the SDK does around a second of
keychain and configuration work and a network round trip. Call configure() where a
second of background work is acceptable, not on a path that blocks first paint.
When the SDK kills the process mid-reset, the journal picks the reset up on the next launch
— and it carries the original reason, not a generic "resume". Verified on an iPad against
a real tenant: a reset started as support_reset was killed at the cleaningLocal stage
and resumed as support_reset.
This matters for exactly one branch. On an administrator-initiated wipe the process dies before your handler can run, so the resume is the only chance to tell the user "an administrator revoked access" rather than nothing.
Reading it takes some care: the resume finishes before the first render, so by the time
your UI exists pendingReset is already null. Log the reason where the resume happens
rather than binding it to a screen.
All three iOS runtime overrides set through configure(), no ADAL* keys in the plist:
enrollment succeeded, policy applied, the app restarted and the PIN was enforced. This was
the largest risk in the per-tenant model.
Moving an installed app from one tenant to another is not verified — it needs a second tenant. The runtime overrides do persist across restarts, which is what makes that case worth testing rather than assuming.
Worth knowing
DYLD_PRINT_STATISTICS reports nothing under dyld4 on iOS 26 — the variable is read and
produces no output, so an empty result is not a measurement.
And xcrun simctl launch <sim> <bundle> KEY=VALUE passes those as arguments to the
app, not environment variables. Environment needs the SIMCTL_CHILD_ prefix on the
simctl invocation itself: SIMCTL_CHILD_FOO=1 xcrun simctl launch ….
What does work for measuring the SDK's launch cost is reading its own log timestamps: it logs generously, and the deltas are stable across runs.
The PIN is shared across every managed app on the device and is not requested on every launch. A later launch not prompting is that timer, not a regression. Restarting the device resets it.
Creating or changing a policy and then testing immediately proves nothing. Waiting is often the fix, and 30 minutes is a reasonable floor before treating anything as a defect.
Not a crash. Anything that has to happen after an unregister cannot be written after the call — this library uses a journal that survives the process and resumes on the next launch.
Outlook, Teams and OneDrive share policy state and the PIN timer. Microsoft's own test guidance is to remove them before drawing conclusions.
It lands in android/app/build/outputs/intune/<variant>/logs/IntuneMAMBuildReport.html,
with one file per rewritten class beside it. Microsoft's documentation says
build/outputs/logs. Enable it with report = true and read it when a third-party library
starts misbehaving after the plugin is applied.
The SDK enforces copy-and-paste restrictions internally and exposes no flag for them, so
getPolicy() cannot tell you whether copy-out is blocked. Do not build UI that claims to
know.
Info.plistADALCacheKeychainGroupOverride and MaxFileProtectionLevel have no runtime equivalent —
the SDK reads them at launch. configure() therefore rejects the matching options when the
plist disagrees, rather than accepting a value it cannot apply.
This library never has two accounts signed in at once. There is no identity tagging, no per-identity storage and no multi-identity handling, and adding any of it would change the security model rather than extend it.