react-native-intune 0.1.0-alpha.1 → 0.1.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,16 +1,30 @@
1
- # react-native-intune
2
-
3
- Microsoft Intune app protection for React Native — **app-level data protection, not device
4
- management.** MSAL with broker support is included, so sign-in and enrollment work without
5
- a second SDK.
6
-
7
- **[Documentation](https://bayazetyan.github.io/react-native-intune/)** ·
8
- [Getting started](https://bayazetyan.github.io/react-native-intune/docs/getting-started) ·
9
- [Traps](https://bayazetyan.github.io/react-native-intune/docs/notes/traps) ·
10
- [API reference](https://bayazetyan.github.io/react-native-intune/docs/reference)
11
-
12
- > **Status: pre-alpha.** Implemented and verified on real devices against a real tenant,
13
- > but not released. Expect breaking changes until 1.0.
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/bayazetyan/react-native-intune/main/.github/assets/banner-dark.png">
4
+ <img alt="react-native-intune — Microsoft Intune app protection for React Native" src="https://raw.githubusercontent.com/bayazetyan/react-native-intune/main/.github/assets/banner-light.png" width="820">
5
+ </picture>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/react-native-intune"><img alt="npm" src="https://img.shields.io/npm/v/react-native-intune?color=4B5AE4&labelColor=1B2027"></a>
10
+ <a href="https://github.com/bayazetyan/react-native-intune/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/bayazetyan/react-native-intune/actions/workflows/ci.yml/badge.svg?branch=main"></a>
11
+ <a href="./LICENSE"><img alt="MIT" src="https://img.shields.io/badge/licence-MIT-4B5AE4?labelColor=1B2027"></a>
12
+ <img alt="platforms" src="https://img.shields.io/badge/platforms-iOS%2017%2B%20%C2%B7%20Android%2024%2B-5A6478?labelColor=1B2027">
13
+ <img alt="React Native" src="https://img.shields.io/badge/React%20Native-0.74%2B%20new%20arch-5A6478?labelColor=1B2027">
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://bayazetyan.github.io/react-native-intune/"><b>Documentation</b></a> ·
18
+ <a href="https://bayazetyan.github.io/react-native-intune/docs/getting-started">Getting started</a> ·
19
+ <a href="https://bayazetyan.github.io/react-native-intune/docs/notes/traps">Traps</a> ·
20
+ <a href="https://bayazetyan.github.io/react-native-intune/docs/reference">API reference</a>
21
+ </p>
22
+
23
+ ---
24
+
25
+ > **Status: pre-alpha.** Implemented and verified on real devices against a real tenant.
26
+ > The public types changed twice in the fortnight before release, so expect breaking
27
+ > changes until 1.0 and pin an exact version.
14
28
 
15
29
  *Not affiliated with, endorsed by, or sponsored by Microsoft. Microsoft, Intune and Entra
16
30
  are trademarks of the Microsoft group of companies.*
@@ -76,7 +90,7 @@ app project** — `Info.plist`, entitlements, `AndroidManifest.xml`, your app mo
76
90
  `build.gradle`, your `Application` class — and autolinking does not touch any of them.
77
91
 
78
92
  **Expo:** add the config plugin and run `expo prebuild`. Nearly everything is applied for
79
- you, including the `MAMApplication` change.
93
+ you, including the auth callback in your `Application` class.
80
94
 
81
95
  ```json
82
96
  { "expo": { "plugins": [["react-native-intune", { "androidSignatureHash": "…" }]] } }
@@ -13,20 +13,25 @@ import java.util.concurrent.atomic.AtomicLong
13
13
  /**
14
14
  * Supplies the MAM service token (SPEC §13.4, §6.1.2).
15
15
  *
16
- * **The consumer registers this from `Application.onMAMCreate()`,** not from JS. The SDK
16
+ * **The consumer registers this from `Application.onCreate()`,** not from JS. The SDK
17
17
  * can ask for a token before any React context exists — it has its own enrollment retry
18
18
  * schedule that survives restarts — so registration has to happen earlier than JS is
19
19
  * able to run. The module supplies the class; the wiring is one line in the host app:
20
20
  *
21
21
  * ```kotlin
22
- * class MainApplication : MAMApplication() {
23
- * override fun onMAMCreate() {
24
- * super.onMAMCreate()
22
+ * class MainApplication : Application(), ReactApplication {
23
+ * override fun onCreate() {
24
+ * super.onCreate()
25
25
  * RNIntuneAuthCallback.register(this)
26
26
  * }
27
27
  * }
28
28
  * ```
29
29
  *
30
+ * `Application`, not `MAMApplication`, and `onCreate`, not `onMAMCreate`: the MAM Gradle
31
+ * plugin rewrites both at build time. Written in source, `MAMApplication` does not
32
+ * compile — this module declares the SDK as `implementation`, which is not transitive,
33
+ * so it is not on the app module's classpath (issue #5).
34
+ *
30
35
  * `acquireToken` is called **on a background thread and must return synchronously**, so
31
36
  * this blocks that thread while JS answers. That is why the timeout is not optional: the
32
37
  * SDK's thread is held for its duration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-intune",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.1-rc.1",
4
4
  "description": "Microsoft Intune app protection policies (MAM) for React Native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -22,6 +22,7 @@
22
22
  "app.plugin.js",
23
23
  "plugin",
24
24
  "scripts",
25
+ "!scripts/verify-expo.mjs",
25
26
  "sdk-versions.json",
26
27
  "sdk-lock.json",
27
28
  "!vendor",
@@ -43,12 +44,13 @@
43
44
  ],
44
45
  "scripts": {
45
46
  "fetch-sdks": "node scripts/fetch-sdks.mjs",
47
+ "verify:expo": "node scripts/verify-expo.mjs",
46
48
  "postinstall": "node scripts/fetch-sdks.mjs --soft --quiet",
47
49
  "example": "yarn workspace react-native-intune-example",
48
50
  "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
49
51
  "prepare": "bob build",
50
52
  "typecheck": "yarn workspace react-native-intune-example ensure-tenant && tsc",
51
- "test": "jest",
53
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest",
52
54
  "release": "release-it --only-version",
53
55
  "lint": "eslint \"**/*.{js,ts,tsx}\""
54
56
  },
@@ -145,6 +147,11 @@
145
147
  "modulePathIgnorePatterns": [
146
148
  "<rootDir>/example/node_modules",
147
149
  "<rootDir>/lib/"
150
+ ],
151
+ "testMatch": [
152
+ "**/__tests__/**/*.[jt]s?(x)",
153
+ "**/?(*.)+(spec|test).[jt]s?(x)",
154
+ "**/__tests__/**/*.test.mjs"
148
155
  ]
149
156
  },
150
157
  "commitlint": {
@@ -190,9 +197,7 @@
190
197
  ],
191
198
  "version": "0.63.0"
192
199
  },
193
- "bin": {
194
- "react-native-intune": "scripts/cli.mjs"
195
- },
200
+ "bin": "scripts/cli.mjs",
196
201
  "engines": {
197
202
  "node": ">=18"
198
203
  },
@@ -63,14 +63,18 @@ function withMamClasspath(contents, { javassist = '3.29.2-GA' } = {}) {
63
63
  }
64
64
 
65
65
  /**
66
- * Makes the generated Application class derive from MAMApplication and register the auth
67
- * callback.
66
+ * Registers the auth callback in the generated Application class.
68
67
  *
69
- * This is the difference Expo makes. On bare React Native this file is the developer's
70
- * own source, so `doctor` can only report it; under prebuild the file is generated, so
71
- * there is nothing of theirs to overwrite. It is also one of the three omissions that
72
- * build and run while protecting nothing, which is why automating it is worth the
73
- * string surgery.
68
+ * **It deliberately does not touch the superclass.** An earlier version rewrote
69
+ * `: Application()` to `: MAMApplication()` in source, which does not compile in a
70
+ * consumer app: this package declares the MAM AAR as `implementation files(...)`, and
71
+ * `implementation` is not transitive, so the SDK's classes are not on the app module's
72
+ * compile classpath. The superclass resolves to an error type and every member
73
+ * inheriting from it fails with it — see issue #5.
74
+ *
75
+ * The superclass is the MAM Gradle plugin's job, in bytecode, which is what Microsoft's
76
+ * own guidance describes and what this repository's example app has always relied on.
77
+ * Rewriting it here was both broken and redundant.
74
78
  *
75
79
  * The callback goes in `onCreate` deliberately: the MAM plugin rewrites `onCreate` into
76
80
  * `onMAMCreate` at build time, so writing `onMAMCreate` here would leave a method the
@@ -80,69 +84,107 @@ function withMamClasspath(contents, { javassist = '3.29.2-GA' } = {}) {
80
84
  * act on beats a generated file that does not compile.
81
85
  */
82
86
  function withMamApplication(contents, language) {
83
- if (contents.includes('MAMApplication')) {
84
- return contents;
87
+ const isKotlin = language === 'kt';
88
+
89
+ const migrated = revertLegacySuperclass(contents, isKotlin);
90
+ if (migrated === null) {
91
+ return null;
92
+ }
93
+ if (/RNIntuneAuthCallback\s*\.\s*register/.test(migrated)) {
94
+ return migrated;
85
95
  }
86
96
 
87
- const isKotlin = language === 'kt';
88
- const superclass = isKotlin
89
- ? /(class\s+\w+\s*:\s*)Application(\s*\(\s*\))?/
90
- : /(class\s+\w+\s+extends\s+)Application/;
91
- if (!superclass.test(contents)) {
97
+ // `MAMApplication` is accepted as well as `Application`: an app that has wired the SDK
98
+ // into its own module may extend it directly, and the plugin's rewrite is then a no-op
99
+ // rather than an error. What matters is that there is an Application subclass at all —
100
+ // with none, the Gradle plugin has nothing to transform.
101
+ const appClass = isKotlin
102
+ ? /class\s+\w+\s*:\s*(?:MAM)?Application\s*\(\s*\)/
103
+ : /class\s+\w+\s+extends\s+(?:MAM)?Application\b/;
104
+ if (!appClass.test(migrated)) {
92
105
  return null;
93
106
  }
94
107
 
95
- let next = contents.replace(superclass, '$1MAMApplication()');
96
- if (!isKotlin) {
97
- next = next.replace(
98
- /(class\s+\w+\s+extends\s+)MAMApplication\(\)/,
99
- '$1MAMApplication'
100
- );
108
+ const next = addImports(migrated, [
109
+ `import com.reactnativeintune.RNIntuneAuthCallback${isKotlin ? '' : ';'}`,
110
+ ]);
111
+ if (next === null) {
112
+ return null;
101
113
  }
102
114
 
103
- // Imports. `android.app.Application` may still be referenced elsewhere in the file, so
104
- // it is added alongside rather than replacing anything.
105
- const imports = isKotlin
106
- ? [
107
- 'import com.microsoft.intune.mam.client.app.MAMApplication',
108
- 'import com.reactnativeintune.RNIntuneAuthCallback',
109
- ]
110
- : [
111
- 'import com.microsoft.intune.mam.client.app.MAMApplication;',
112
- 'import com.reactnativeintune.RNIntuneAuthCallback;',
113
- ];
114
- const missing = imports.filter((i) => !next.includes(i));
115
- if (missing.length > 0) {
116
- const lastImport = [...next.matchAll(/^import .*$/gm)].pop();
117
- if (!lastImport) {
118
- return null;
119
- }
120
- const at = lastImport.index + lastImport[0].length;
121
- next = `${next.slice(0, at)}\n${missing.join('\n')}${next.slice(at)}`;
115
+ // After `super.onCreate()`, not at the top of the method. The superclass is what
116
+ // initialises the MAM machinery the callback registers with, so registering first is
117
+ // registering against nothing — and it fails the way everything here fails, by
118
+ // reporting a licensing-shaped enrollment status that retries quietly.
119
+ const onCreate = isKotlin
120
+ ? /(override\s+fun\s+onCreate\s*\(\s*\)\s*\{[^\n]*\n\s*super\.onCreate\s*\(\s*\)\s*\n)/
121
+ : /(public\s+void\s+onCreate\s*\(\s*\)\s*\{[^\n]*\n\s*super\.onCreate\s*\(\s*\)\s*;\s*\n)/;
122
+ if (!onCreate.test(next)) {
123
+ return null;
122
124
  }
125
+ const end = isKotlin ? '' : ';';
126
+ const call =
127
+ ` ${CALLBACK_COMMENT}\n` +
128
+ ` // time, so this belongs here rather than in an onMAMCreate we write.\n` +
129
+ ` RNIntuneAuthCallback.register(this)${end}\n`;
130
+
131
+ return next.replace(onCreate, `$1${call}`);
132
+ }
133
+
134
+ /**
135
+ * The first line of the comment this transform writes above the callback. It is also
136
+ * how a file this transform has already touched is recognised — see below.
137
+ */
138
+ const CALLBACK_COMMENT = `// ${MARKER}: the MAM plugin rewrites onCreate into onMAMCreate at build`;
123
139
 
124
- if (!/RNIntuneAuthCallback\s*\.\s*register/.test(next)) {
125
- // After `super.onCreate()`, not at the top of the method. The superclass is what
126
- // initialises the MAM machinery the callback registers with, so registering first
127
- // is registering against nothing — and it fails the way everything here fails, by
128
- // reporting a licensing-shaped enrollment status that retries quietly.
129
- const onCreate = isKotlin
130
- ? /(override\s+fun\s+onCreate\s*\(\s*\)\s*\{[^\n]*\n\s*super\.onCreate\s*\(\s*\)\s*\n)/
131
- : /(public\s+void\s+onCreate\s*\(\s*\)\s*\{[^\n]*\n\s*super\.onCreate\s*\(\s*\)\s*;\s*\n)/;
132
- if (!onCreate.test(next)) {
133
- return null;
134
- }
135
- const call = isKotlin
136
- ? ` // ${MARKER}: the MAM plugin rewrites onCreate into onMAMCreate at build\n` +
137
- ` // time, so this belongs here rather than in an onMAMCreate we write.\n` +
138
- ' RNIntuneAuthCallback.register(this)\n'
139
- : ` // ${MARKER}: the MAM plugin rewrites onCreate into onMAMCreate at build\n` +
140
- ` // time, so this belongs here rather than in an onMAMCreate we write.\n` +
141
- ' RNIntuneAuthCallback.register(this);\n';
142
- next = next.replace(onCreate, `$1${call}`);
140
+ /**
141
+ * Undoes what 0.1.0 wrote, so the fix reaches people the defect already reached.
142
+ *
143
+ * 0.1.0 rewrote the superclass to `MAMApplication` in source and added its import. The
144
+ * idempotency guard above would leave such a file untouched — it already registers the
145
+ * callback — so `expo prebuild` without `--clean` would keep a file that does not
146
+ * compile, on the version that is supposed to fix it.
147
+ *
148
+ * Only a file carrying this transform's own comment is changed. That comment is proof
149
+ * the superclass was ours to write; an app that extends `MAMApplication` deliberately,
150
+ * with the SDK on its own classpath, is left alone.
151
+ */
152
+ function revertLegacySuperclass(contents, isKotlin) {
153
+ if (!contents.includes(CALLBACK_COMMENT)) {
154
+ return contents;
155
+ }
156
+ const superclass = isKotlin
157
+ ? /(class\s+\w+\s*:\s*)MAMApplication(\s*\(\s*\))/
158
+ : /(class\s+\w+\s+extends\s+)MAMApplication\b/;
159
+ if (!superclass.test(contents)) {
160
+ return contents;
143
161
  }
162
+ const reverted = contents
163
+ .replace(superclass, isKotlin ? '$1Application$2' : '$1Application')
164
+ .replace(
165
+ /^import com\.microsoft\.intune\.mam\.client\.app\.MAMApplication;?\n/m,
166
+ ''
167
+ );
168
+ return addImports(reverted, [
169
+ `import android.app.Application${isKotlin ? '' : ';'}`,
170
+ ]);
171
+ }
144
172
 
145
- return next;
173
+ /**
174
+ * Adds each missing import after the last existing one. Null when the file has no
175
+ * import to anchor on, which a generated Application class always has.
176
+ */
177
+ function addImports(contents, imports) {
178
+ const missing = imports.filter((i) => !contents.includes(i));
179
+ if (missing.length === 0) {
180
+ return contents;
181
+ }
182
+ const lastImport = [...contents.matchAll(/^import .*$/gm)].pop();
183
+ if (!lastImport) {
184
+ return null;
185
+ }
186
+ const at = lastImport.index + lastImport[0].length;
187
+ return `${contents.slice(0, at)}\n${missing.join('\n')}${contents.slice(at)}`;
146
188
  }
147
189
 
148
190
  module.exports = {
@@ -285,9 +285,9 @@ const withMamApplicationClass = (config) =>
285
285
  // leaving the app unprotected. A quiet skip here is the worst outcome available.
286
286
  WarningAggregator.addWarningAndroid(
287
287
  MARKER,
288
- 'Could not make MainApplication extend MAMApplication — its shape was not ' +
289
- 'recognised. THE APP WILL BUILD AND RUN WITHOUT PROTECTION until this is ' +
290
- 'done by hand. Run `npx react-native-intune doctor` for the exact change.'
288
+ 'Could not register the MAM auth callback in MainApplication — its shape was ' +
289
+ 'not recognised. THE APP WILL BUILD AND RUN WITHOUT PROTECTION until this ' +
290
+ 'is done by hand. Run `npx react-native-intune doctor` for the exact change.'
291
291
  );
292
292
  return c;
293
293
  }
@@ -128,7 +128,9 @@ function main() {
128
128
  }
129
129
  process.stdout.write(`\n${platform === 'ios' ? 'iOS' : 'Android'}\n`);
130
130
  for (const r of forPlatform) {
131
- process.stdout.write(` ${glyph(r.state)} ${r.check.title}${suffix(r)}\n`);
131
+ process.stdout.write(
132
+ ` ${glyph(r.state, r.check.severity)} ${r.check.title}${suffix(r)}\n`
133
+ );
132
134
  }
133
135
  }
134
136
 
@@ -183,7 +185,15 @@ function main() {
183
185
  process.exit(s.blocking.length > 0 ? 1 : 0);
184
186
  }
185
187
 
186
- function glyph(state) {
188
+ /**
189
+ * An advisory never reads as MISS or WRONG. It does not fail the exit code by design,
190
+ * and a line that shouts while the detail underneath says "advisory" teaches people to
191
+ * discount the ones that do matter.
192
+ */
193
+ function glyph(state, severity) {
194
+ if (severity === 'advisory' && state !== 'ok' && state !== 'skip') {
195
+ return 'note';
196
+ }
187
197
  return { ok: 'ok ', missing: 'MISS', wrong: 'WRONG', unknown: '? ', skip: '-- ' }[
188
198
  state
189
199
  ];
@@ -49,30 +49,58 @@ export const KEYCHAIN_GROUPS = [
49
49
 
50
50
  // ---------------------------------------------------------------- android
51
51
 
52
+ /**
53
+ * Source text with `//` and block comments removed, for Gradle, Kotlin and Java alike.
54
+ *
55
+ * Every check that reads source for a declaration goes through this, because matching
56
+ * text that includes comments is how `android-mam-application` reported this
57
+ * repository's own example as passing for as long as it existed (issue #5). A `//` is
58
+ * only a comment after whitespace or at the start of a line, so `https://` in a
59
+ * repository URL survives.
60
+ */
61
+ function stripComments(src) {
62
+ return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1');
63
+ }
64
+
65
+ /**
66
+ * An actual application of the plugin: `apply plugin: "…"`, `id "…"` or `id("…")`. The
67
+ * closing quote is required, so the `com.microsoft.intune.mam.build` classpath entry in
68
+ * the root build file does not count.
69
+ */
70
+ const MAM_PLUGIN_APPLIED =
71
+ /(?:apply\s+plugin\s*:\s*|\bid\s*\(?\s*)['"]com\.microsoft\.intune\.mam['"]/;
72
+
73
+ /**
74
+ * Whether the MAM Gradle plugin is applied to the app module. Shared by the plugin check
75
+ * and the Application check, which both depend on the answer and must never disagree
76
+ * about it.
77
+ */
78
+ function mamPluginApplied(project) {
79
+ const a = project.android;
80
+ if (a.appBuildGradleKts && !a.appBuildGradle) {
81
+ return {
82
+ state: 'unknown',
83
+ detail:
84
+ 'app/build.gradle.kts — the Kotlin DSL is not inspected. Check by hand that ' +
85
+ 'the plugin is applied.',
86
+ };
87
+ }
88
+ const src = read(a.appBuildGradle);
89
+ if (!src) {
90
+ return { state: 'unknown', detail: 'android/app/build.gradle not found' };
91
+ }
92
+ return MAM_PLUGIN_APPLIED.test(stripComments(src))
93
+ ? { state: 'ok' }
94
+ : { state: 'missing' };
95
+ }
96
+
52
97
  const androidChecks = [
53
98
  {
54
99
  id: 'android-mam-plugin',
55
100
  platform: 'android',
56
101
  title: 'MAM Gradle plugin applied to the app module',
57
102
  severity: 'silent',
58
- inspect(project) {
59
- const a = project.android;
60
- if (a.appBuildGradleKts && !a.appBuildGradle) {
61
- return {
62
- state: 'unknown',
63
- detail:
64
- 'app/build.gradle.kts — the Kotlin DSL is not inspected. Check by hand ' +
65
- 'that the plugin is applied.',
66
- };
67
- }
68
- const src = read(a.appBuildGradle);
69
- if (!src) {
70
- return { state: 'unknown', detail: 'android/app/build.gradle not found' };
71
- }
72
- return /com\.microsoft\.intune\.mam/.test(src)
73
- ? { state: 'ok' }
74
- : { state: 'missing' };
75
- },
103
+ inspect: mamPluginApplied,
76
104
  why:
77
105
  'The plugin rewrites bytecode across the app and every dependency. Applied to ' +
78
106
  'the library instead of the app module it rewrites only the library, builds ' +
@@ -174,8 +202,18 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
174
202
  {
175
203
  id: 'android-mam-application',
176
204
  platform: 'android',
177
- title: 'Application class derives from MAMApplication',
205
+ title: 'Application class is transformed into a MAMApplication',
178
206
  severity: 'silent',
207
+ /**
208
+ * What makes the app a MAMApplication is the Gradle plugin's bytecode rewrite, not
209
+ * the text of the source file — so this reads both, and a plain `Application`
210
+ * subclass with the plugin applied is the correct, expected shape.
211
+ *
212
+ * The previous version matched the substring `MAMApplication` anywhere in the
213
+ * source. It reported this repository's own example as passing because the word
214
+ * appears in a *comment* there, which is why nobody noticed it was asserting the
215
+ * wrong thing entirely. Issue #5.
216
+ */
179
217
  inspect(project) {
180
218
  const sources = project.android.applicationSources;
181
219
  if (sources.length === 0) {
@@ -184,26 +222,78 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
184
222
  detail: 'no Application subclass found in android/app/src/main',
185
223
  };
186
224
  }
187
- const hit = sources.find((f) => /MAMApplication/.test(read(f)));
188
- if (hit) {
189
- return { state: 'ok', detail: rel(project, hit) };
225
+
226
+ // A class declaration, never a mention: comments are stripped first, so neither
227
+ // an explanatory comment nor a commented-out example counts.
228
+ const declared = /class\s+\w+\s*(?::\s*|\s+extends\s+)MAMApplication\b/;
229
+ const explicit = sources.find((f) => declared.test(stripComments(read(f))));
230
+ const a = project.android;
231
+
232
+ if (explicit) {
233
+ // Legitimate only when the app module has the SDK on its own compile classpath.
234
+ // This package declares the AAR as `implementation`, which is not transitive,
235
+ // so without that the supertype does not resolve — exactly the shape a 0.1.0
236
+ // Expo prebuild wrote, which must not read as protected (issue #5).
237
+ if (a.appBuildGradleKts && !a.appBuildGradle) {
238
+ return {
239
+ state: 'unknown',
240
+ detail:
241
+ `${rel(project, explicit)} extends MAMApplication directly, and the ` +
242
+ 'Kotlin DSL build file is not inspected — check by hand that the MAM SDK ' +
243
+ 'is on the app module\u2019s classpath, or this does not compile.',
244
+ };
245
+ }
246
+ const gradle = stripComments(read(a.appBuildGradle));
247
+ if (/Microsoft\.Intune\.MAM\.SDK/.test(gradle)) {
248
+ return {
249
+ state: 'ok',
250
+ detail: `${rel(project, explicit)} extends MAMApplication directly`,
251
+ };
252
+ }
253
+ return {
254
+ state: 'wrong',
255
+ detail:
256
+ `${rel(project, explicit)} extends MAMApplication in source, but the MAM ` +
257
+ 'SDK is not on the app module\u2019s classpath, so it does not compile. If ' +
258
+ 'expo prebuild wrote it, re-run prebuild with this version.',
259
+ };
260
+ }
261
+
262
+ const plugin = mamPluginApplied(project);
263
+ if (plugin.state === 'unknown') {
264
+ return {
265
+ state: 'unknown',
266
+ detail: `whether the plugin transforms the class cannot be read: ${plugin.detail}`,
267
+ };
268
+ }
269
+ if (plugin.state === 'ok') {
270
+ return {
271
+ state: 'ok',
272
+ detail: `${rel(project, sources[0])} — superclass rewritten by the Gradle plugin`,
273
+ };
190
274
  }
191
275
  return {
192
276
  state: 'wrong',
193
- detail: `found ${sources.map((f) => rel(project, f)).join(', ')} — none extends MAMApplication`,
277
+ detail:
278
+ `found ${sources.map((f) => rel(project, f)).join(', ')}, and the MAM ` +
279
+ 'Gradle plugin is not applied to the app module — so nothing rewrites it',
194
280
  };
195
281
  },
196
282
  why:
197
283
  'One of the three omissions that build and run while protecting nothing. The ' +
198
- 'plugin transforms an Application subclass; with none to transform, or one that ' +
199
- 'does not derive from MAMApplication, the SDK is present and inert.',
284
+ 'Gradle plugin transforms an Application subclass into a MAMApplication; with ' +
285
+ 'none to transform, or with the plugin not applied, the SDK is present and inert.',
200
286
  instruction: () =>
201
- `Your Application class must extend MAMApplication, and the auth callback is
202
- registered from onMAMCreate — the plugin rewrites onCreate into it:
287
+ `Keep your Application class extending android.app.Application — the MAM Gradle
288
+ plugin rewrites the superclass at build time, and writing MAMApplication in source
289
+ does not compile unless you put the SDK on your app module's classpath yourself.
290
+
291
+ Apply the plugin (see the "MAM Gradle plugin applied to the app module" check), and
292
+ register the auth callback from onCreate, which the plugin rewrites into onMAMCreate:
203
293
 
204
- class MainApplication : MAMApplication(), ReactApplication {
205
- override fun onMAMCreate() {
206
- super.onMAMCreate()
294
+ class MainApplication : Application(), ReactApplication {
295
+ override fun onCreate() {
296
+ super.onCreate()
207
297
  RNIntuneAuthCallback.register(this)
208
298
  // ... the rest of your existing onCreate body
209
299
  }
@@ -222,7 +312,9 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
222
312
  if (sources.length === 0) {
223
313
  return { state: 'missing', detail: 'no Application subclass found' };
224
314
  }
225
- return sources.some((f) => /RNIntuneAuthCallback\s*\.\s*register/.test(read(f)))
315
+ return sources.some((f) =>
316
+ /RNIntuneAuthCallback\s*\.\s*register/.test(stripComments(read(f)))
317
+ )
226
318
  ? { state: 'ok' }
227
319
  : { state: 'missing' };
228
320
  },
package/sdk-lock.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "ios": {
3
3
  "repo": "microsoftconnect/ms-intune-app-sdk-ios",
4
4
  "tag": "21.8.0",
5
- "fetchedAt": "2026-09-03T10:39:33.572Z",
5
+ "fetchedAt": "2026-09-28T11:15:17.927Z",
6
6
  "files": {
7
7
  "ios/IntuneMAMSwift.xcframework": "f38f0f130bd2adf0dd936acb26958f6c6ebc0b2d5a4bf886d4c8c7d4ae0870ab",
8
8
  "ios/IntuneMAMSwiftStub.xcframework": "a03e563feec36f719bfca34b061e590e1c54a03650a3855a8298988e49b9ea9c",
@@ -17,7 +17,7 @@
17
17
  "android": {
18
18
  "repo": "microsoftconnect/ms-intune-app-sdk-android",
19
19
  "tag": "12.4.0",
20
- "fetchedAt": "2026-09-03T10:39:50.835Z",
20
+ "fetchedAt": "2026-09-28T11:15:18.852Z",
21
21
  "files": {
22
22
  "android/Microsoft.Intune.MAM.SDK.aar": "b7e5d0bb86d18f054d8901f168f82e64c79a57334fbb6227136b108b0514e998",
23
23
  "android/GradlePlugin/com.microsoft.intune.mam.build.jar": "04ae1e0fbb6b5dfcbf8bd42e8d4f41ad758332ee6c238780a764a9ab1b67446d",