react-native-intune 0.1.1 → 0.1.2

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.
@@ -93,33 +93,54 @@ gradle.taskGraph.whenReady { graph ->
93
93
  }
94
94
 
95
95
  /**
96
- * Disable AAR packaging for this library, with a message that says why.
96
+ * The MAM SDK is resolved through a repository rather than as a file.
97
97
  *
98
- * `./gradlew assembleDebug` from a consumer's android/ directory assembles every module,
99
- * including this one, and `:react-native-intune:bundleDebugAar` then fails with "Direct
100
- * local .aar file dependencies are not supported when building an AAR". People run that
101
- * command, and the error names our library, so it reads as our bug.
98
+ * `implementation files(mamAar)` is what Microsoft's guide shows for an *app* module, and
99
+ * in a *library* module AGP rejects it wherever it has to package the library: "Direct
100
+ * local .aar file dependencies are not supported when building an AAR". This used to be
101
+ * worked around by disabling the bundle tasks by name, and that could not be made right
102
+ * across AGP versions — the lint AAR an app's `lintVitalAnalyzeRelease` consumes is built
103
+ * by one of those tasks. Disabling it broke every clean release build on AGP 8.12;
104
+ * leaving it enabled breaks the same build on the AGP of React Native 0.74, which applies
105
+ * the local-.aar rule to the lint AAR too.
102
106
  *
103
- * It is not a workaround for a problem we could otherwise solve. AGP's own message says
104
- * the AAR "would be broken because the classes and Android resources from any local .aar
105
- * file dependencies would not be packaged" — so the output being skipped is an output
106
- * that could never have been correct. The MAM SDK is distributed as a file from
107
- * Microsoft's GitHub, not from Maven Central, so there is no coordinate to depend on
108
- * instead.
107
+ * Resolved from a repository, the SDK is an ordinary module dependency and AGP stops
108
+ * treating this library as a special case anywhere.
109
109
  *
110
- * Nothing is lost: autolinking consumes this library as a Gradle *project*, so its AAR is
111
- * never built for a consumer. `:app:assembleDebug` was always the working form; now the
112
- * root form works too.
110
+ * An Ivy repository with an `[artifact].[ext]` pattern, because it points at the file
111
+ * fetch-sdks already wrote, under the exact name Microsoft distributes —
112
+ * `Microsoft.Intune.MAM.SDK.aar` — with nothing copied or renamed and no metadata to
113
+ * generate. A flat directory would do the same and prints "should be avoided because it
114
+ * doesn't support any meta-data formats" on every consumer's build, naming this library.
115
+ * The version in the coordinate is not in the pattern, so it is informational: the pin in
116
+ * sdk-versions.json, the one fetch-sdks downloaded.
117
+ *
118
+ * The repository has to be visible to the app, not only to this project: the app resolves
119
+ * its runtime classpath with its own repositories, so a repository declared here alone
120
+ * would fail there. It is added to every project and made exclusive to the one module, so
121
+ * nothing else is ever looked up in it and this module is never looked up anywhere else.
113
122
  */
114
- tasks.configureEach { task ->
115
- if (task.name ==~ /bundle.*Aar/) {
116
- task.enabled = false
117
- task.doFirst {
118
- logger.lifecycle(
119
- "[react-native-intune] Skipping ${task.name}: this library depends on the MAM " +
120
- "SDK as a local .aar, so AGP cannot package a valid AAR for it. Consumers " +
121
- "autolink it as a Gradle project and never need one."
122
- )
123
+ def mamVersion = new groovy.json.JsonSlurper()
124
+ .parse(file("$projectDir/../sdk-versions.json")).android.tag
125
+ def mamRepoDir = mamAar.parentFile
126
+ rootProject.allprojects {
127
+ repositories {
128
+ exclusiveContent {
129
+ forRepository {
130
+ ivy {
131
+ name = "react-native-intune-mam-sdk"
132
+ url = mamRepoDir.toURI()
133
+ patternLayout {
134
+ artifact "[artifact].[ext]"
135
+ }
136
+ metadataSources {
137
+ artifact()
138
+ }
139
+ }
140
+ }
141
+ filter {
142
+ includeModule("react-native-intune", "Microsoft.Intune.MAM.SDK")
143
+ }
123
144
  }
124
145
  }
125
146
  }
@@ -127,7 +148,7 @@ tasks.configureEach { task ->
127
148
  dependencies {
128
149
  implementation "com.facebook.react:react-android"
129
150
 
130
- implementation files(mamAar)
151
+ implementation("react-native-intune:Microsoft.Intune.MAM.SDK:${mamVersion}@aar")
131
152
 
132
153
  // MSAL. Mirrored in sdk-versions.json -> toolchain.msal_android; change both together.
133
154
  //
@@ -16,17 +16,22 @@ class RNIntunePackage : BaseReactPackage() {
16
16
  }
17
17
 
18
18
  override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
19
+ // Positional, not named. In React Native 0.74 `ReactModuleInfo` is still a Java
20
+ // class, and Kotlin cannot pass named arguments to a Java constructor — the named
21
+ // form compiles only on versions where the class was rewritten in Kotlin, so it
22
+ // failed on 0.74 while building everywhere this was developed. The six parameters
23
+ // are in the same order in both.
19
24
  mapOf(
20
25
  RNIntuneModule.NAME to ReactModuleInfo(
21
- name = RNIntuneModule.NAME,
22
- className = RNIntuneModule.NAME,
23
- canOverrideExistingModule = false,
26
+ RNIntuneModule.NAME, // name
27
+ RNIntuneModule.NAME, // className
28
+ false, // canOverrideExistingModule
24
29
  // The event emitter must be live from construction, not from the first
25
30
  // enroll() — a service-initiated wipe can arrive before JS calls anything
26
31
  // (SPEC §4.4, §12.4). Revisit this flag when the emitter lands.
27
- needsEagerInit = false,
28
- isCxxModule = false,
29
- isTurboModule = true
32
+ false, // needsEagerInit
33
+ false, // isCxxModule
34
+ true, // isTurboModule
30
35
  )
31
36
  )
32
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-intune",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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,7 +22,7 @@
22
22
  "app.plugin.js",
23
23
  "plugin",
24
24
  "scripts",
25
- "!scripts/verify-expo.mjs",
25
+ "!scripts/verify",
26
26
  "sdk-versions.json",
27
27
  "sdk-lock.json",
28
28
  "!vendor",
@@ -44,7 +44,8 @@
44
44
  ],
45
45
  "scripts": {
46
46
  "fetch-sdks": "node scripts/fetch-sdks.mjs",
47
- "verify:expo": "node scripts/verify-expo.mjs",
47
+ "verify:expo": "node scripts/verify/expo.mjs",
48
+ "verify:bare": "node scripts/verify/bare.mjs",
48
49
  "postinstall": "node scripts/fetch-sdks.mjs --soft --quiet",
49
50
  "example": "yarn workspace react-native-intune-example",
50
51
  "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
@@ -187,9 +187,55 @@ function addImports(contents, imports) {
187
187
  return `${contents.slice(0, at)}\n${missing.join('\n')}${contents.slice(at)}`;
188
188
  }
189
189
 
190
+ /**
191
+ * Forwards the sign-in redirect to MSAL from the generated AppDelegate.
192
+ *
193
+ * The URL type alone is not enough. When the broker — Authenticator or Company Portal —
194
+ * finishes, it opens the app with the result, and unless the AppDelegate hands that URL
195
+ * to MSAL it arrives and is dropped: the sign-in never completes and nothing reports
196
+ * why. Both halves were needed on a device, and the bare example has always had this
197
+ * one; the Expo plugin wrote the URL type and not the forward.
198
+ *
199
+ * MSAL goes first in the `||` chain, so a redirect meant for it is consumed before
200
+ * Linking sees it; any other URL falls through unchanged.
201
+ *
202
+ * Returns null when there is no `open url` handler to extend — a warning beats an
203
+ * AppDelegate edited in the wrong place.
204
+ */
205
+ function withMsalResponseHandler(contents, language) {
206
+ if (/handleMSALResponse/.test(contents)) {
207
+ return contents;
208
+ }
209
+ const isSwift = language === 'swift';
210
+
211
+ const handler = isSwift
212
+ ? /(func\s+application\s*\(\s*_\s+\w+\s*:\s*UIApplication\s*,\s*open\s+url\s*:\s*URL\s*,\s*options\s*:[^)]*\)\s*->\s*Bool\s*\{\s*\n(\s*)return\s+)/
213
+ : /(-\s*\(BOOL\)\s*application\s*:\s*\(UIApplication\s*\*\)\s*\w+\s+openURL\s*:\s*\(NSURL\s*\*\)\s*url\s+options\s*:[^{]*\{\s*\n(\s*)return\s+)/;
214
+ if (!handler.test(contents)) {
215
+ return null;
216
+ }
217
+ const forward = isSwift
218
+ ? 'MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[.sourceApplication] as? String) || '
219
+ : '[MSALPublicClientApplication handleMSALResponse:url sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]] || ';
220
+ let next = contents.replace(handler, `$1${forward}`);
221
+
222
+ const importLine = isSwift ? 'import MSAL' : '#import <MSAL/MSAL.h>';
223
+ const imports = isSwift
224
+ ? /^(?:\w+\s+)?import\s+\w+.*$/gm
225
+ : /^#import\s+.*$/gm;
226
+ const lastImport = [...next.matchAll(imports)].pop();
227
+ if (!lastImport) {
228
+ return null;
229
+ }
230
+ const at = lastImport.index + lastImport[0].length;
231
+ next = `${next.slice(0, at)}\n${importLine}${next.slice(at)}`;
232
+ return next;
233
+ }
234
+
190
235
  module.exports = {
191
236
  MARKER,
192
237
  withMamPluginApplied,
193
238
  withMamClasspath,
194
239
  withMamApplication,
240
+ withMsalResponseHandler,
195
241
  };
@@ -37,6 +37,7 @@ const {
37
37
  createRunOncePlugin,
38
38
  withAndroidManifest,
39
39
  withAppBuildGradle,
40
+ withAppDelegate,
40
41
  withEntitlementsPlist,
41
42
  withInfoPlist,
42
43
  withMainApplication,
@@ -48,6 +49,7 @@ const {
48
49
  withMamApplication,
49
50
  withMamClasspath,
50
51
  withMamPluginApplied,
52
+ withMsalResponseHandler,
51
53
  } = require('./transforms');
52
54
 
53
55
  const pkg = require('../package.json');
@@ -169,6 +171,30 @@ const withMsalUrlScheme = (config, { maxFileProtectionLevel, keychainGroup }) =>
169
171
  return c;
170
172
  });
171
173
 
174
+ /**
175
+ * The other half of the URL type. Without it the broker's redirect reaches the app and
176
+ * is dropped, and sign-in never completes — see withMsalResponseHandler.
177
+ */
178
+ const withMsalRedirectForward = (config) =>
179
+ withAppDelegate(config, (c) => {
180
+ const result = withMsalResponseHandler(
181
+ c.modResults.contents,
182
+ c.modResults.language
183
+ );
184
+ if (result === null) {
185
+ WarningAggregator.addWarningIOS(
186
+ MARKER,
187
+ 'Could not forward the MSAL redirect in AppDelegate — no open-URL handler ' +
188
+ 'was recognised. Sign-in through Authenticator or Company Portal will not ' +
189
+ 'return to the app until this is done by hand. Run ' +
190
+ '`npx react-native-intune doctor` for the exact change.'
191
+ );
192
+ return c;
193
+ }
194
+ c.modResults.contents = result;
195
+ return c;
196
+ });
197
+
172
198
  // ---------------------------------------------------------------- android
173
199
 
174
200
  const withBrokerQueries = (config) =>
@@ -305,6 +331,7 @@ const withIntune = (config, props = {}) => {
305
331
  let next = config;
306
332
  next = withKeychainGroups(next, { keychainGroup });
307
333
  next = withMsalUrlScheme(next, { maxFileProtectionLevel, keychainGroup });
334
+ next = withMsalRedirectForward(next);
308
335
  next = withBrokerQueries(next);
309
336
  next = withRedirectActivity(next, { androidSignatureHash });
310
337
  next = withGradle(next);
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { execFileSync } from 'node:child_process';
26
26
  import fs from 'node:fs';
27
+ import path from 'node:path';
27
28
  import { read, rel } from './project.mjs';
28
29
 
29
30
  const MARKER = 'react-native-intune: managed block';
@@ -462,7 +463,161 @@ pinning an older MAM SDK whose row you do match — see sdk-versions.json.`,
462
463
 
463
464
  // ---------------------------------------------------------------- ios
464
465
 
466
+ /**
467
+ * The lowest iOS the vendored MAM SDK runs on. Read from the binary (`otool -l` on
468
+ * IntuneMAMSwift.framework in the 21.8.0 drop reports `minos 17.0`), not from
469
+ * Microsoft's release notes — the SDK should move this, not a guess.
470
+ */
471
+ const MIN_IOS = [17, 0];
472
+
473
+ const versionAtLeast = (value, [major, minor]) => {
474
+ const [a = 0, b = 0] = String(value).split('.').map(Number);
475
+ return a > major || (a === major && b >= minor);
476
+ };
477
+
478
+ /**
479
+ * The deployment target each configuration of each application target actually builds
480
+ * with: the target's own setting, or the project-level one it inherits when it sets
481
+ * none. A value elsewhere in the file — a project-level default the target overrides,
482
+ * another target — says nothing about what the app is built for, and counting those is
483
+ * how an Expo project, whose project level reads 16.4 while the app target reads 17.0,
484
+ * was reported as wrong.
485
+ */
486
+ function appDeploymentTargets(pbxproj) {
487
+ const object = (isa) =>
488
+ new RegExp(
489
+ `(\\w{24})\\s*(?:/\\*[^*]*\\*/)?\\s*=\\s*\\{\\s*isa\\s*=\\s*${isa};([\\s\\S]*?)\\n\\t\\t\\};`,
490
+ 'g'
491
+ );
492
+ const configs = new Map();
493
+ for (const [, id, body] of pbxproj.matchAll(object('XCBuildConfiguration'))) {
494
+ configs.set(id, {
495
+ name: body.match(/\bname\s*=\s*"?([^";]+)"?;/)?.[1],
496
+ value: body.match(/IPHONEOS_DEPLOYMENT_TARGET\s*=\s*"?([\d.]+)"?;/)?.[1],
497
+ });
498
+ }
499
+ const lists = new Map();
500
+ for (const [, id, body] of pbxproj.matchAll(object('XCConfigurationList'))) {
501
+ const ids = body.match(/buildConfigurations\s*=\s*\(([^)]*)\)/)?.[1] ?? '';
502
+ lists.set(id, [...ids.matchAll(/\w{24}/g)].map((m) => m[0]));
503
+ }
504
+ const listOf = (body) => body.match(/buildConfigurationList\s*=\s*(\w{24})/)?.[1];
505
+
506
+ const [, , projectBody = ''] = [...pbxproj.matchAll(object('PBXProject'))][0] ?? [];
507
+ const inherited = new Map(
508
+ (lists.get(listOf(projectBody)) ?? []).map((id) => [
509
+ configs.get(id)?.name,
510
+ configs.get(id)?.value,
511
+ ])
512
+ );
513
+
514
+ const effective = [];
515
+ for (const [, , body] of pbxproj.matchAll(object('PBXNativeTarget'))) {
516
+ if (!/productType\s*=\s*"com\.apple\.product-type\.application"/.test(body)) {
517
+ continue;
518
+ }
519
+ for (const id of lists.get(listOf(body)) ?? []) {
520
+ const c = configs.get(id) ?? {};
521
+ effective.push({ name: c.name, value: c.value ?? inherited.get(c.name) ?? null });
522
+ }
523
+ }
524
+ return effective;
525
+ }
526
+
527
+ /** Ruby comments — whole-line or trailing — so a commented-out platform line is ignored. */
528
+ const stripRubyComments = (src) => src.replace(/(^|\s)#.*$/gm, '$1');
529
+
465
530
  const iosChecks = [
531
+ {
532
+ id: 'ios-deployment-target',
533
+ platform: 'ios',
534
+ title: `Deployment target is iOS ${MIN_IOS.join('.')} or later`,
535
+ severity: 'loud',
536
+ /**
537
+ * React Native's template targets `min_ios_version_supported` — 15.1 — and the SDK
538
+ * is built for 17.0, so a new project fails at `pod install` before anything else
539
+ * here can be tried. The Podfile and the Xcode target are read separately because
540
+ * they disagree independently: a raised Podfile with a 15.1 app target installs the
541
+ * pod and builds, then crashes at launch on any iOS 16 device it was allowed onto.
542
+ */
543
+ inspect(project) {
544
+ const podfile = stripRubyComments(read(project.ios.podfile));
545
+ if (!podfile) {
546
+ return { state: 'unknown', detail: 'ios/Podfile not found' };
547
+ }
548
+ const line = podfile.match(/^\s*platform\s+:ios\s*,\s*(.+)$/m)?.[1]?.trim();
549
+ if (!line) {
550
+ return { state: 'unknown', detail: 'no `platform :ios` line in the Podfile' };
551
+ }
552
+
553
+ let target = line.match(/^['"]([\d.]+)['"]$/)?.[1];
554
+ let source = 'Podfile';
555
+ if (!target && /ios\.deploymentTarget/.test(line)) {
556
+ // Expo's generated Podfile reads Podfile.properties.json first, which is what
557
+ // expo-build-properties writes, and falls back to the template minimum.
558
+ const props = path.join(path.dirname(project.ios.podfile), 'Podfile.properties.json');
559
+ try {
560
+ target = JSON.parse(read(props) || '{}')['ios.deploymentTarget'];
561
+ source = 'Podfile.properties.json';
562
+ } catch {
563
+ return { state: 'unknown', detail: 'Podfile.properties.json is not valid JSON' };
564
+ }
565
+ if (!target) {
566
+ return {
567
+ state: 'wrong',
568
+ detail:
569
+ 'ios.deploymentTarget is not set, so the Podfile falls back to ' +
570
+ 'min_ios_version_supported',
571
+ };
572
+ }
573
+ }
574
+ if (!target) {
575
+ return /min_ios_version_supported/.test(line)
576
+ ? {
577
+ state: 'wrong',
578
+ detail: 'the Podfile uses min_ios_version_supported, which is below 17.0',
579
+ }
580
+ : { state: 'unknown', detail: `cannot read a version from: ${line}` };
581
+ }
582
+ if (!versionAtLeast(target, MIN_IOS)) {
583
+ return { state: 'wrong', detail: `${source} targets ${target}` };
584
+ }
585
+
586
+ const effective = appDeploymentTargets(read(project.ios.pbxproj));
587
+ if (effective.length === 0) {
588
+ return {
589
+ state: 'unknown',
590
+ detail: `${source} targets ${target}; no application target found in the Xcode project`,
591
+ };
592
+ }
593
+ // A configuration that sets it nowhere builds for the SDK's own default, which is
594
+ // the newest iOS — not a problem, so only explicit low values count.
595
+ const low = effective.filter((e) => e.value && !versionAtLeast(e.value, MIN_IOS));
596
+ if (low.length > 0) {
597
+ return {
598
+ state: 'wrong',
599
+ detail:
600
+ `${source} targets ${target}, but the app target builds for ` +
601
+ low.map((e) => `${e.value} (${e.name})`).join(', '),
602
+ };
603
+ }
604
+ return { state: 'ok', detail: `${source}: ${target}` };
605
+ },
606
+ why:
607
+ 'MAM SDK 21.x is built for iOS 17.0. Below it CocoaPods refuses the pod; with only ' +
608
+ 'the Podfile raised, the app builds and then crashes at launch on older devices it ' +
609
+ 'still claims to support.',
610
+ instruction: () =>
611
+ `In ios/Podfile:
612
+
613
+ platform :ios, '17.0'
614
+
615
+ and set Minimum Deployments to 17.0 on your app target in Xcode
616
+ (IPHONEOS_DEPLOYMENT_TARGET), then run pod install again.
617
+
618
+ Expo: set it with expo-build-properties — { "ios": { "deploymentTarget": "17.0" } }.`,
619
+ },
620
+
466
621
  {
467
622
  id: 'ios-keychain-groups',
468
623
  platform: 'ios',