react-native-intune 0.1.2 → 1.0.0-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 +8 -4
- package/RNIntune.podspec +3 -3
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/reactnativeintune/RNIntuneResetJournal.kt +1 -1
- package/ios/RNIntunePlistGuard.m +1 -1
- package/ios/RNIntuneResetJournal.m +1 -1
- package/lib/module/api/tokenProvider.js +3 -0
- package/lib/module/api/tokenProvider.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/internal/decode.js +23 -2
- package/lib/module/internal/decode.js.map +1 -1
- package/lib/module/types.js +37 -6
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/api/tokenProvider.d.ts +3 -0
- package/lib/typescript/src/api/tokenProvider.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/internal/decode.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +61 -11
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +4 -6
- package/plugin/transforms.js +110 -3
- package/scripts/doctor.mjs +7 -3
- package/scripts/lib/checks.mjs +329 -28
- package/scripts/lib/edits.mjs +329 -0
- package/scripts/lib/project.mjs +13 -0
- package/scripts/setup.mjs +59 -9
- package/sdk-lock.json +9 -8
- package/sdk-versions.json +125 -28
- package/src/api/tokenProvider.ts +3 -0
- package/src/index.ts +1 -0
- package/src/internal/decode.ts +27 -8
- package/src/types.ts +66 -15
package/plugin/transforms.js
CHANGED
|
@@ -27,9 +27,20 @@ intunemam {
|
|
|
27
27
|
}
|
|
28
28
|
`;
|
|
29
29
|
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Applies the MAM plugin to android/app/build.gradle.
|
|
32
|
+
*
|
|
33
|
+
* The guard looks for an actual application at the start of a line, not the plugin id
|
|
34
|
+
* anywhere: commenting the apply out is the first thing anyone does when the bytecode
|
|
35
|
+
* rewrite breaks a library, and a guard that read the comment as applied would leave
|
|
36
|
+
* the app unprotected while `setup` reported success.
|
|
37
|
+
*/
|
|
31
38
|
function withMamPluginApplied(contents) {
|
|
32
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
/^[ \t]*(?:apply\s+plugin\s*:\s*|id\s*\(?\s*)["']com\.microsoft\.intune\.mam["']/m.test(
|
|
41
|
+
contents
|
|
42
|
+
)
|
|
43
|
+
) {
|
|
33
44
|
return contents;
|
|
34
45
|
}
|
|
35
46
|
return `${contents.trimEnd()}\n${MAM_PLUGIN_BLOCK}`;
|
|
@@ -212,7 +223,7 @@ function withMsalResponseHandler(contents, language) {
|
|
|
212
223
|
? /(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
224
|
: /(-\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
225
|
if (!handler.test(contents)) {
|
|
215
|
-
return
|
|
226
|
+
return withNewMsalResponseHandler(contents, isSwift);
|
|
216
227
|
}
|
|
217
228
|
const forward = isSwift
|
|
218
229
|
? 'MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[.sourceApplication] as? String) || '
|
|
@@ -232,6 +243,102 @@ function withMsalResponseHandler(contents, language) {
|
|
|
232
243
|
return next;
|
|
233
244
|
}
|
|
234
245
|
|
|
246
|
+
/**
|
|
247
|
+
* React Native's own templates have no open-URL handler at all — only Expo's does — so
|
|
248
|
+
* for them the forward is a whole method. It goes at the end of the AppDelegate class,
|
|
249
|
+
* found by matching braces from the class declaration; string literals and comments are
|
|
250
|
+
* skipped so a brace in either cannot end the class early.
|
|
251
|
+
*
|
|
252
|
+
* No `override`: in these templates the class is a UIResponder conforming to
|
|
253
|
+
* UIApplicationDelegate, and the method is the protocol's, not a superclass's.
|
|
254
|
+
*/
|
|
255
|
+
function withNewMsalResponseHandler(contents, isSwift) {
|
|
256
|
+
const importLine = isSwift ? 'import MSAL' : '#import <MSAL/MSAL.h>';
|
|
257
|
+
let next = contents;
|
|
258
|
+
|
|
259
|
+
if (isSwift) {
|
|
260
|
+
const declaration = /class\s+AppDelegate\b[^{]*\{/.exec(next);
|
|
261
|
+
if (!declaration) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const end = matchingBrace(
|
|
265
|
+
next,
|
|
266
|
+
declaration.index + declaration[0].length - 1
|
|
267
|
+
);
|
|
268
|
+
if (end === null) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const method = `
|
|
272
|
+
/// Hands the sign-in result back to MSAL. Without it the broker's reply reaches the app
|
|
273
|
+
/// and is dropped, and sign-in never completes. Added by react-native-intune setup.
|
|
274
|
+
func application(
|
|
275
|
+
_ app: UIApplication,
|
|
276
|
+
open url: URL,
|
|
277
|
+
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
|
278
|
+
) -> Bool {
|
|
279
|
+
MSALPublicClientApplication.handleMSALResponse(
|
|
280
|
+
url,
|
|
281
|
+
sourceApplication: options[.sourceApplication] as? String
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
`;
|
|
285
|
+
next = `${next.slice(0, end).replace(/\s*$/, '\n')}${method}${next.slice(end)}`;
|
|
286
|
+
} else {
|
|
287
|
+
const implementation = /@implementation\s+AppDelegate\b[^\n]*\n/.exec(next);
|
|
288
|
+
if (!implementation) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const at = implementation.index + implementation[0].length;
|
|
292
|
+
const method = `
|
|
293
|
+
// Hands the sign-in result back to MSAL. Without it the broker's reply reaches the app and
|
|
294
|
+
// is dropped, and sign-in never completes. Added by react-native-intune setup.
|
|
295
|
+
- (BOOL)application:(UIApplication *)application
|
|
296
|
+
openURL:(NSURL *)url
|
|
297
|
+
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
|
|
298
|
+
{
|
|
299
|
+
return [MSALPublicClientApplication handleMSALResponse:url
|
|
300
|
+
sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]];
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
next = `${next.slice(0, at)}${method}${next.slice(at)}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const imports = isSwift
|
|
307
|
+
? /^(?:\w+\s+)?import\s+\w+.*$/gm
|
|
308
|
+
: /^#import\s+.*$/gm;
|
|
309
|
+
const lastImport = [...next.matchAll(imports)].pop();
|
|
310
|
+
if (!lastImport) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const at = lastImport.index + lastImport[0].length;
|
|
314
|
+
return `${next.slice(0, at)}\n${importLine}${next.slice(at)}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The index of the brace closing the one at `open`, skipping strings and comments. */
|
|
318
|
+
function matchingBrace(src, open) {
|
|
319
|
+
let depth = 0;
|
|
320
|
+
for (let i = open; i < src.length; i += 1) {
|
|
321
|
+
const c = src[i];
|
|
322
|
+
if (c === '/' && src[i + 1] === '/') {
|
|
323
|
+
i = src.indexOf('\n', i);
|
|
324
|
+
if (i === -1) return null;
|
|
325
|
+
} else if (c === '/' && src[i + 1] === '*') {
|
|
326
|
+
i = src.indexOf('*/', i + 2) + 1;
|
|
327
|
+
if (i === 0) return null;
|
|
328
|
+
} else if (c === '"') {
|
|
329
|
+
for (i += 1; i < src.length && src[i] !== '"'; i += 1) {
|
|
330
|
+
if (src[i] === '\\') i += 1;
|
|
331
|
+
}
|
|
332
|
+
} else if (c === '{') {
|
|
333
|
+
depth += 1;
|
|
334
|
+
} else if (c === '}') {
|
|
335
|
+
depth -= 1;
|
|
336
|
+
if (depth === 0) return i;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
235
342
|
module.exports = {
|
|
236
343
|
MARKER,
|
|
237
344
|
withMamPluginApplied,
|
package/scripts/doctor.mjs
CHANGED
|
@@ -116,7 +116,8 @@ function main() {
|
|
|
116
116
|
2
|
|
117
117
|
) + '\n'
|
|
118
118
|
);
|
|
119
|
-
process.
|
|
119
|
+
process.exitCode = s.blocking.length > 0 ? 1 : 0;
|
|
120
|
+
return;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
process.stdout.write(`\nreact-native-intune doctor\n${project.root}\n`);
|
|
@@ -141,7 +142,7 @@ function main() {
|
|
|
141
142
|
`licensed for Intune, that an App Protection Policy targets this app, and that\n` +
|
|
142
143
|
`a broker is installed on the device. Enrollment needs all three.\n\n`
|
|
143
144
|
);
|
|
144
|
-
|
|
145
|
+
return;
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
if (s.silent.length > 0) {
|
|
@@ -182,7 +183,10 @@ function main() {
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
process.stdout.write('\n');
|
|
185
|
-
process.exit(
|
|
186
|
+
// An exit code, not process.exit(): on macOS a write to a pipe is asynchronous, and
|
|
187
|
+
// exiting straight after a long report cut it off mid-way whenever doctor's output was
|
|
188
|
+
// piped — into a CI log, a file, or `| less`.
|
|
189
|
+
process.exitCode = s.blocking.length > 0 ? 1 : 0;
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
/**
|
package/scripts/lib/checks.mjs
CHANGED
|
@@ -25,8 +25,25 @@
|
|
|
25
25
|
import { execFileSync } from 'node:child_process';
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import path from 'node:path';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import {
|
|
30
|
+
entitlementsWithKeychainGroups,
|
|
31
|
+
pbxprojAddConfiguratorPhase,
|
|
32
|
+
pbxprojSetDeploymentTarget,
|
|
33
|
+
pbxprojSetEntitlements,
|
|
34
|
+
plistEnsureStrings,
|
|
35
|
+
plistEnsureUrlScheme,
|
|
36
|
+
plistRemoveKeys,
|
|
37
|
+
} from './edits.mjs';
|
|
28
38
|
import { read, rel } from './project.mjs';
|
|
29
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The Expo plugin's transforms, used here for the same edits in a bare project. One
|
|
42
|
+
* implementation of each, already tested and already run against real builds, rather
|
|
43
|
+
* than a second one that drifts.
|
|
44
|
+
*/
|
|
45
|
+
const transforms = createRequire(import.meta.url)('../../plugin/transforms.js');
|
|
46
|
+
|
|
30
47
|
const MARKER = 'react-native-intune: managed block';
|
|
31
48
|
|
|
32
49
|
export const BROKER_PACKAGES = [
|
|
@@ -116,6 +133,12 @@ const androidChecks = [
|
|
|
116
133
|
verify = true // catches plugin-induced runtime failures
|
|
117
134
|
incremental = true // the default is false
|
|
118
135
|
}`,
|
|
136
|
+
apply(project) {
|
|
137
|
+
const file = project.android.appBuildGradle;
|
|
138
|
+
const src = read(file);
|
|
139
|
+
// The Kotlin DSL is not edited — reported instead, like it is inspected.
|
|
140
|
+
return src ? { file, contents: transforms.withMamPluginApplied(src) } : null;
|
|
141
|
+
},
|
|
119
142
|
},
|
|
120
143
|
|
|
121
144
|
{
|
|
@@ -141,6 +164,57 @@ const androidChecks = [
|
|
|
141
164
|
And javassist, whose version must match the SDK exactly:
|
|
142
165
|
|
|
143
166
|
classpath "org.javassist:javassist:3.29.2-GA"`,
|
|
167
|
+
apply(project) {
|
|
168
|
+
const file = project.android.rootBuildGradle;
|
|
169
|
+
const src = read(file);
|
|
170
|
+
const contents = src && transforms.withMamClasspath(src);
|
|
171
|
+
return contents ? { file, contents } : null;
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
id: 'android-min-sdk',
|
|
177
|
+
platform: 'android',
|
|
178
|
+
title: 'minSdk 24 or later',
|
|
179
|
+
severity: 'loud',
|
|
180
|
+
/**
|
|
181
|
+
* MSAL requires 24 and React Native 0.74's template ships 23. Loud rather than silent
|
|
182
|
+
* — the manifest merger fails and names MSAL — but it is the first thing a 0.74
|
|
183
|
+
* project hits, and cheaper found here than after a Gradle run.
|
|
184
|
+
*/
|
|
185
|
+
inspect(project) {
|
|
186
|
+
const root = read(project.android.rootBuildGradle);
|
|
187
|
+
const properties = read(path.join(project.android.dir, 'gradle.properties'));
|
|
188
|
+
const value =
|
|
189
|
+
firstMatch(root, /minSdkVersion\s*=\s*(\d+)/) ??
|
|
190
|
+
firstMatch(properties, /^android\.minSdkVersion\s*=\s*(\d+)/m) ??
|
|
191
|
+
firstMatch(read(project.android.appBuildGradle), /minSdk(?:Version)?\s*=?\s*(\d+)/);
|
|
192
|
+
if (!value) {
|
|
193
|
+
return { state: 'unknown', detail: 'no literal minSdkVersion in the build files' };
|
|
194
|
+
}
|
|
195
|
+
return Number(value) >= 24
|
|
196
|
+
? { state: 'ok', detail: value }
|
|
197
|
+
: { state: 'wrong', detail: `minSdkVersion ${value}` };
|
|
198
|
+
},
|
|
199
|
+
why:
|
|
200
|
+
'MSAL declares minSdk 24. Below it the manifest merger refuses the build, and the ' +
|
|
201
|
+
'fix is one number.',
|
|
202
|
+
instruction: () =>
|
|
203
|
+
`In android/build.gradle:
|
|
204
|
+
|
|
205
|
+
buildscript {
|
|
206
|
+
ext {
|
|
207
|
+
minSdkVersion = 24
|
|
208
|
+
}
|
|
209
|
+
}`,
|
|
210
|
+
apply(project) {
|
|
211
|
+
const file = project.android.rootBuildGradle;
|
|
212
|
+
const src = read(file);
|
|
213
|
+
const m = src && src.match(/(minSdkVersion\s*=\s*)(\d+)/);
|
|
214
|
+
return m && Number(m[2]) < 24
|
|
215
|
+
? { file, contents: src.replace(m[0], `${m[1]}24`) }
|
|
216
|
+
: null;
|
|
217
|
+
},
|
|
144
218
|
},
|
|
145
219
|
|
|
146
220
|
{
|
|
@@ -330,6 +404,23 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
|
|
|
330
404
|
RNIntuneAuthCallback.register(this)
|
|
331
405
|
|
|
332
406
|
Code inside your own class, so setup reports it rather than editing it.`,
|
|
407
|
+
/**
|
|
408
|
+
* An edit to the developer's own source, which setup otherwise avoids — but the change
|
|
409
|
+
* is one line after `super.onCreate()`, it is shown as a diff before anything is
|
|
410
|
+
* written, and the tree has to be clean so it is one revert away. Leaving it manual
|
|
411
|
+
* leaves the one silent omission here that nothing else would catch.
|
|
412
|
+
*/
|
|
413
|
+
apply(project) {
|
|
414
|
+
const [file] = project.android.applicationSources;
|
|
415
|
+
if (!file) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const contents = transforms.withMamApplication(
|
|
419
|
+
read(file),
|
|
420
|
+
file.endsWith('.kt') ? 'kt' : 'java'
|
|
421
|
+
);
|
|
422
|
+
return contents ? { file, contents } : null;
|
|
423
|
+
},
|
|
333
424
|
},
|
|
334
425
|
|
|
335
426
|
{
|
|
@@ -381,12 +472,51 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
|
|
|
381
472
|
| openssl sha1 -binary | openssl base64`
|
|
382
473
|
}`;
|
|
383
474
|
},
|
|
475
|
+
/**
|
|
476
|
+
* Written with the hash of android/app/debug.keystore, computed here — the key a
|
|
477
|
+
* debug build is signed with, and the one most apps are first run with. A release
|
|
478
|
+
* signed with another key needs that key's hash as a second <data> entry, which
|
|
479
|
+
* setup cannot know; the comment it writes says so.
|
|
480
|
+
*/
|
|
481
|
+
apply(project) {
|
|
482
|
+
const file = project.android.manifest;
|
|
483
|
+
const src = read(file);
|
|
484
|
+
const hash = signatureHash(project);
|
|
485
|
+
const applicationId = read(project.android.appBuildGradle).match(
|
|
486
|
+
/applicationId\s*=?\s*["']([\w.]+)["']/
|
|
487
|
+
)?.[1];
|
|
488
|
+
if (!src || !hash || !applicationId || !/<\/application>/.test(src)) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const activity = `
|
|
492
|
+
<!-- ${MARKER}: the MSAL redirect. The path is the hash of the debug keystore;
|
|
493
|
+
a release build signed with another key needs that key's hash as a second
|
|
494
|
+
<data> element, and the matching redirect URI registered in Entra. -->
|
|
495
|
+
<activity
|
|
496
|
+
android:name="com.microsoft.identity.client.BrowserTabActivity"
|
|
497
|
+
android:exported="true">
|
|
498
|
+
<intent-filter>
|
|
499
|
+
<action android:name="android.intent.action.VIEW" />
|
|
500
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
501
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
502
|
+
<data
|
|
503
|
+
android:scheme="msauth"
|
|
504
|
+
android:host="${applicationId}"
|
|
505
|
+
android:path="/${hash}" />
|
|
506
|
+
</intent-filter>
|
|
507
|
+
</activity>
|
|
508
|
+
`;
|
|
509
|
+
return {
|
|
510
|
+
file,
|
|
511
|
+
contents: src.replace(/(\n?)([ \t]*)<\/application>/, `${activity}$2</application>`),
|
|
512
|
+
};
|
|
513
|
+
},
|
|
384
514
|
},
|
|
385
515
|
|
|
386
516
|
{
|
|
387
517
|
id: 'android-toolchain',
|
|
388
518
|
platform: 'android',
|
|
389
|
-
title: 'App toolchain
|
|
519
|
+
title: 'App toolchain is a tested combination',
|
|
390
520
|
/**
|
|
391
521
|
* Advisory, and deliberately so. Mixing matrix rows is *untested* by Microsoft, not
|
|
392
522
|
* known-broken — plenty of combinations work. But when one does not, the failure lands
|
|
@@ -421,26 +551,63 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
|
|
|
421
551
|
firstMatch(src, /kotlinVersion["' ]*[:=]["' ]*([\d.]+)/),
|
|
422
552
|
gradle: gradleWrapperVersion(project),
|
|
423
553
|
};
|
|
554
|
+
// React Native's templates leave AGP and Kotlin to React Native's own version
|
|
555
|
+
// catalog — `classpath("com.android.tools.build:gradle")` with no version, which
|
|
556
|
+
// the React Native Gradle plugin resolves from it. So that catalog is where the
|
|
557
|
+
// versions the app actually builds with are written.
|
|
558
|
+
const catalog = read(
|
|
559
|
+
path.join(project.root, 'node_modules/react-native/gradle/libs.versions.toml')
|
|
560
|
+
);
|
|
561
|
+
found.agp ||= firstMatch(catalog, /^agp\s*=\s*"([\d.]+)"/m);
|
|
562
|
+
found.kotlin ||= firstMatch(catalog, /^kotlin\s*=\s*"([\d.]+)"/m);
|
|
563
|
+
const rn = (() => {
|
|
564
|
+
try {
|
|
565
|
+
return JSON.parse(
|
|
566
|
+
read(path.join(project.root, 'node_modules/react-native/package.json')) || '{}'
|
|
567
|
+
).version;
|
|
568
|
+
} catch {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
})();
|
|
424
572
|
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
.filter(([k, want]) => found[k] !== want)
|
|
428
|
-
.map(([k, want]) => `${k} ${found[k]} vs ${want}`);
|
|
429
|
-
|
|
430
|
-
const unseen = Object.keys(REQUIRED_TOOLCHAIN).filter((k) => !found[k]);
|
|
431
|
-
|
|
432
|
-
if (mismatched.length > 0) {
|
|
433
|
-
return { state: 'wrong', detail: mismatched.join(', ') };
|
|
434
|
-
}
|
|
435
|
-
if (unseen.length === Object.keys(REQUIRED_TOOLCHAIN).length) {
|
|
573
|
+
const seen = Object.keys(REQUIRED_TOOLCHAIN).filter((k) => found[k]);
|
|
574
|
+
if (seen.length === 0) {
|
|
436
575
|
return {
|
|
437
576
|
state: 'unknown',
|
|
438
577
|
detail: 'could not read any version from the build files',
|
|
439
578
|
};
|
|
440
579
|
}
|
|
580
|
+
const unread = Object.keys(REQUIRED_TOOLCHAIN).filter((k) => !found[k]);
|
|
581
|
+
const partly = unread.length > 0 ? `; ${unread.join(', ')} not read` : '';
|
|
582
|
+
const agrees = (row) =>
|
|
583
|
+
seen.every((k) => row[k] === undefined || row[k] === found[k]) &&
|
|
584
|
+
seen.some((k) => row[k] !== undefined);
|
|
585
|
+
|
|
586
|
+
if (agrees(REQUIRED_TOOLCHAIN)) {
|
|
587
|
+
return { state: 'ok', detail: `on Microsoft's tested row${partly}` };
|
|
588
|
+
}
|
|
589
|
+
// Off Microsoft's row, but a combination this project has built and run — which is
|
|
590
|
+
// worth saying so, and worth saying whose claim it is.
|
|
591
|
+
// The row for this project's own React Native version first, so a report names the
|
|
592
|
+
// combination that was actually built rather than one that happens to share a
|
|
593
|
+
// Gradle version.
|
|
594
|
+
const candidates = VERIFIED_TOOLCHAINS.filter(agrees);
|
|
595
|
+
const ours =
|
|
596
|
+
candidates.find((v) => rn && String(v.reactNative).includes(rn)) ?? candidates[0];
|
|
597
|
+
if (ours) {
|
|
598
|
+
return {
|
|
599
|
+
state: 'ok',
|
|
600
|
+
detail:
|
|
601
|
+
`${seen.map((k) => `${k} ${found[k]}`).join(', ')} — verified by this project ` +
|
|
602
|
+
`on ${ours.date} (React Native ${ours.reactNative}), not by Microsoft${partly}`,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
441
605
|
return {
|
|
442
|
-
state: '
|
|
443
|
-
detail:
|
|
606
|
+
state: 'wrong',
|
|
607
|
+
detail:
|
|
608
|
+
seen.map((k) => `${k} ${found[k]}`).join(', ') +
|
|
609
|
+
' — neither Microsoft\u2019s row nor a combination verified here' +
|
|
610
|
+
partly,
|
|
444
611
|
};
|
|
445
612
|
},
|
|
446
613
|
why:
|
|
@@ -449,15 +616,20 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
|
|
|
449
616
|
'rewrites bytecode in your app module, so when a mixed row does fail it fails ' +
|
|
450
617
|
'inside javassist and the error names one of your classes, never a version.',
|
|
451
618
|
instruction: () =>
|
|
452
|
-
`
|
|
619
|
+
`Microsoft's tested row for the pinned MAM SDK:
|
|
620
|
+
|
|
621
|
+
Gradle ${REQUIRED_TOOLCHAIN.gradle} AGP ${REQUIRED_TOOLCHAIN.agp} Kotlin ${REQUIRED_TOOLCHAIN.kotlin} Java 17
|
|
622
|
+
|
|
623
|
+
Built and run by this project, off that row:
|
|
453
624
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
Kotlin
|
|
457
|
-
|
|
625
|
+
${VERIFIED_TOOLCHAINS.map(
|
|
626
|
+
(v) =>
|
|
627
|
+
` Gradle ${v.gradle}${v.agp ? ` AGP ${v.agp}` : ''} Kotlin ${v.kotlin} ` +
|
|
628
|
+
`(React Native ${v.reactNative}, ${v.date})`
|
|
629
|
+
).join('\n')}
|
|
458
630
|
|
|
459
|
-
|
|
460
|
-
|
|
631
|
+
Anything else is untested rather than broken. If a build fails inside the MAM plugin
|
|
632
|
+
with an error naming one of your classes, move to one of these first.`,
|
|
461
633
|
},
|
|
462
634
|
];
|
|
463
635
|
|
|
@@ -465,7 +637,7 @@ pinning an older MAM SDK whose row you do match — see sdk-versions.json.`,
|
|
|
465
637
|
|
|
466
638
|
/**
|
|
467
639
|
* The lowest iOS the vendored MAM SDK runs on. Read from the binary (`otool -l` on
|
|
468
|
-
* IntuneMAMSwift.framework in
|
|
640
|
+
* IntuneMAMSwift.framework reports `minos 17.0` in both 21.8.0 and 21.9.0), not from
|
|
469
641
|
* Microsoft's release notes — the SDK should move this, not a guess.
|
|
470
642
|
*/
|
|
471
643
|
const MIN_IOS = [17, 0];
|
|
@@ -527,6 +699,26 @@ function appDeploymentTargets(pbxproj) {
|
|
|
527
699
|
/** Ruby comments — whole-line or trailing — so a commented-out platform line is ignored. */
|
|
528
700
|
const stripRubyComments = (src) => src.replace(/(^|\s)#.*$/gm, '$1');
|
|
529
701
|
|
|
702
|
+
/**
|
|
703
|
+
* The Run Script phase setup adds. It fails the build when the tool is missing rather
|
|
704
|
+
* than skipping it, because a skipped run leaves a plist without the schemes the SDK
|
|
705
|
+
* needs, and that fails at sign-in with an empty log instead of here with a reason.
|
|
706
|
+
*/
|
|
707
|
+
const CONFIGURATOR_SCRIPT = `# ${MARKER}: Microsoft's IntuneMAMConfigurator, run on this app's Info.plist and
|
|
708
|
+
# entitlements. A build phase rather than a manual step, because what it writes is not a
|
|
709
|
+
# fixed list and changes between SDK versions. It is idempotent.
|
|
710
|
+
set -euo pipefail
|
|
711
|
+
|
|
712
|
+
CONFIGURATOR="$SRCROOT/../node_modules/react-native-intune/vendor/ios/IntuneMAMConfigurator"
|
|
713
|
+
|
|
714
|
+
if [ ! -x "$CONFIGURATOR" ]; then
|
|
715
|
+
echo "error: IntuneMAMConfigurator is missing — run: node node_modules/react-native-intune/scripts/fetch-sdks.mjs"
|
|
716
|
+
exit 1
|
|
717
|
+
fi
|
|
718
|
+
|
|
719
|
+
"$CONFIGURATOR" -i "$SRCROOT/$INFOPLIST_FILE" -e "$SRCROOT/$CODE_SIGN_ENTITLEMENTS"
|
|
720
|
+
`;
|
|
721
|
+
|
|
530
722
|
const iosChecks = [
|
|
531
723
|
{
|
|
532
724
|
id: 'ios-deployment-target',
|
|
@@ -616,6 +808,29 @@ const iosChecks = [
|
|
|
616
808
|
(IPHONEOS_DEPLOYMENT_TARGET), then run pod install again.
|
|
617
809
|
|
|
618
810
|
Expo: set it with expo-build-properties — { "ios": { "deploymentTarget": "17.0" } }.`,
|
|
811
|
+
/**
|
|
812
|
+
* The Podfile and the app target together — raising one without the other is the case
|
|
813
|
+
* this check exists to catch. An Expo Podfile is left alone: prebuild regenerates it,
|
|
814
|
+
* and the value belongs in expo-build-properties.
|
|
815
|
+
*/
|
|
816
|
+
apply(project) {
|
|
817
|
+
const podfile = project.ios.podfile;
|
|
818
|
+
const src = read(podfile);
|
|
819
|
+
const line = src.match(/^(\s*platform\s+:ios\s*,\s*)(.+)$/m);
|
|
820
|
+
if (!line || /ios\.deploymentTarget/.test(line[2])) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const version = MIN_IOS.join('.');
|
|
824
|
+
const edits = [{ file: podfile, contents: src.replace(line[0], `${line[1]}'${version}'`) }];
|
|
825
|
+
const pbxproj = read(project.ios.pbxproj);
|
|
826
|
+
const next =
|
|
827
|
+
pbxproj &&
|
|
828
|
+
pbxprojSetDeploymentTarget(pbxproj, version, (v) => versionAtLeast(v, MIN_IOS));
|
|
829
|
+
if (next) {
|
|
830
|
+
edits.push({ file: project.ios.pbxproj, contents: next });
|
|
831
|
+
}
|
|
832
|
+
return edits;
|
|
833
|
+
},
|
|
619
834
|
},
|
|
620
835
|
|
|
621
836
|
{
|
|
@@ -679,6 +894,40 @@ ${KEYCHAIN_GROUPS.map((g) => ` <string>${g}</string>`).join('\n')}
|
|
|
679
894
|
The app's own group first, then the SDK's, then MSAL's cache. The first entry may be
|
|
680
895
|
written as the literal bundle id or as $(PRODUCT_BUNDLE_IDENTIFIER) — both work.`;
|
|
681
896
|
},
|
|
897
|
+
/**
|
|
898
|
+
* React Native's template has no entitlements file, so this usually creates one and
|
|
899
|
+
* points the app target at it. An app target that already names a differently-called
|
|
900
|
+
* entitlements file is left to be edited by hand: creating a second file it never
|
|
901
|
+
* reads would look fixed and change nothing.
|
|
902
|
+
*/
|
|
903
|
+
apply(project) {
|
|
904
|
+
const ios = project.ios;
|
|
905
|
+
const pbxproj = read(ios.pbxproj);
|
|
906
|
+
if (!ios.appName || !pbxproj) {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
if (!ios.entitlements && /CODE_SIGN_ENTITLEMENTS\s*=/.test(pbxproj)) {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
const file =
|
|
913
|
+
ios.entitlements ?? path.join(ios.dir, ios.appName, `${ios.appName}.entitlements`);
|
|
914
|
+
const contents = entitlementsWithKeychainGroups(read(file), [
|
|
915
|
+
'$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)',
|
|
916
|
+
...KEYCHAIN_GROUPS,
|
|
917
|
+
]);
|
|
918
|
+
if (!contents) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
const edits = [{ file, contents }];
|
|
922
|
+
const next = pbxprojSetEntitlements(
|
|
923
|
+
pbxproj,
|
|
924
|
+
`${ios.appName}/${ios.appName}.entitlements`
|
|
925
|
+
);
|
|
926
|
+
if (next) {
|
|
927
|
+
edits.push({ file: ios.pbxproj, contents: next });
|
|
928
|
+
}
|
|
929
|
+
return edits;
|
|
930
|
+
},
|
|
682
931
|
},
|
|
683
932
|
|
|
684
933
|
{
|
|
@@ -713,6 +962,12 @@ ${KEYCHAIN_GROUPS.map((g) => ` <string>${g}</string>`).join('\n')}
|
|
|
713
962
|
</array>
|
|
714
963
|
</dict>
|
|
715
964
|
</array>`,
|
|
965
|
+
apply(project) {
|
|
966
|
+
const file = project.ios.infoPlist;
|
|
967
|
+
const src = read(file);
|
|
968
|
+
const contents = src && plistEnsureUrlScheme(src, 'msauth.$(PRODUCT_BUNDLE_IDENTIFIER)');
|
|
969
|
+
return contents ? { file, contents } : null;
|
|
970
|
+
},
|
|
716
971
|
},
|
|
717
972
|
|
|
718
973
|
{
|
|
@@ -742,6 +997,13 @@ ${KEYCHAIN_GROUPS.map((g) => ` <string>${g}</string>`).join('\n')}
|
|
|
742
997
|
${MSAL_QUERY_SCHEMES.map((s) => ` <string>${s}</string>`).join('\n')}
|
|
743
998
|
<string>companyportal</string>
|
|
744
999
|
</array>`,
|
|
1000
|
+
apply(project) {
|
|
1001
|
+
const file = project.ios.infoPlist;
|
|
1002
|
+
const src = read(file);
|
|
1003
|
+
const contents =
|
|
1004
|
+
src && plistEnsureStrings(src, 'LSApplicationQueriesSchemes', MSAL_QUERY_SCHEMES);
|
|
1005
|
+
return contents ? { file, contents } : null;
|
|
1006
|
+
},
|
|
745
1007
|
},
|
|
746
1008
|
|
|
747
1009
|
{
|
|
@@ -772,6 +1034,16 @@ ${MSAL_QUERY_SCHEMES.map((s) => ` <string>${s}</string>`).join('\n')}
|
|
|
772
1034
|
|
|
773
1035
|
Note: ADALCacheKeychainGroupOverride is a different key and is NOT removed — it has
|
|
774
1036
|
no runtime equivalent and is the only way to set the keychain group.`,
|
|
1037
|
+
apply(project) {
|
|
1038
|
+
const file = project.ios.infoPlist;
|
|
1039
|
+
const src = read(file);
|
|
1040
|
+
return src
|
|
1041
|
+
? {
|
|
1042
|
+
file,
|
|
1043
|
+
contents: plistRemoveKeys(src, ['ADALClientId', 'ADALAuthority', 'ADALRedirectUri']),
|
|
1044
|
+
}
|
|
1045
|
+
: null;
|
|
1046
|
+
},
|
|
775
1047
|
},
|
|
776
1048
|
|
|
777
1049
|
{
|
|
@@ -804,6 +1076,18 @@ ${MSAL_QUERY_SCHEMES.map((s) => ` <string>${s}</string>`).join('\n')}
|
|
|
804
1076
|
}
|
|
805
1077
|
|
|
806
1078
|
Code in your own AppDelegate, so setup reports it rather than editing it.`,
|
|
1079
|
+
/** The Expo plugin's transform, which adds the method when a template has none. */
|
|
1080
|
+
apply(project) {
|
|
1081
|
+
const file = project.ios.appDelegate;
|
|
1082
|
+
if (!file) {
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
const contents = transforms.withMsalResponseHandler(
|
|
1086
|
+
read(file),
|
|
1087
|
+
file.endsWith('.swift') ? 'swift' : 'objcpp'
|
|
1088
|
+
);
|
|
1089
|
+
return contents ? { file, contents } : null;
|
|
1090
|
+
},
|
|
807
1091
|
},
|
|
808
1092
|
|
|
809
1093
|
{
|
|
@@ -862,8 +1146,14 @@ ${MSAL_QUERY_SCHEMES.map((s) => ` <string>${s}</string>`).join('\n')}
|
|
|
862
1146
|
-i "$SRCROOT/$INFOPLIST_FILE" \\
|
|
863
1147
|
-e "$SRCROOT/${'$'}{CODE_SIGN_ENTITLEMENTS}"
|
|
864
1148
|
|
|
865
|
-
|
|
866
|
-
|
|
1149
|
+
setup adds it for you. It runs Microsoft's tool as it is — setup writes the phase,
|
|
1150
|
+
not a wrapper around what the tool does.`,
|
|
1151
|
+
apply(project) {
|
|
1152
|
+
const file = project.ios.pbxproj;
|
|
1153
|
+
const src = read(file);
|
|
1154
|
+
const contents = src && pbxprojAddConfiguratorPhase(src, CONFIGURATOR_SCRIPT);
|
|
1155
|
+
return contents ? { file, contents } : null;
|
|
1156
|
+
},
|
|
867
1157
|
},
|
|
868
1158
|
];
|
|
869
1159
|
|
|
@@ -940,16 +1230,27 @@ export function signatureHash(project) {
|
|
|
940
1230
|
* there is one copy. Duplicating it here is how the two drift and the check starts
|
|
941
1231
|
* reporting a row nobody targets.
|
|
942
1232
|
*/
|
|
943
|
-
const
|
|
1233
|
+
const TOOLCHAIN = (() => {
|
|
944
1234
|
try {
|
|
945
1235
|
const url = new URL('../../sdk-versions.json', import.meta.url);
|
|
946
|
-
|
|
947
|
-
const { requires } = toolchain;
|
|
948
|
-
return { gradle: requires.gradle, agp: requires.agp, kotlin: requires.kotlin };
|
|
1236
|
+
return JSON.parse(fs.readFileSync(url, 'utf8')).toolchain ?? {};
|
|
949
1237
|
} catch {
|
|
950
1238
|
return {};
|
|
951
1239
|
}
|
|
952
1240
|
})();
|
|
1241
|
+
const REQUIRED_TOOLCHAIN = TOOLCHAIN.requires
|
|
1242
|
+
? {
|
|
1243
|
+
gradle: TOOLCHAIN.requires.gradle,
|
|
1244
|
+
agp: TOOLCHAIN.requires.agp,
|
|
1245
|
+
kotlin: TOOLCHAIN.requires.kotlin,
|
|
1246
|
+
}
|
|
1247
|
+
: {};
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Combinations this project has built and run itself, off Microsoft's row — see the
|
|
1251
|
+
* comment in sdk-versions.json for what qualifies one.
|
|
1252
|
+
*/
|
|
1253
|
+
const VERIFIED_TOOLCHAINS = TOOLCHAIN.verified ?? [];
|
|
953
1254
|
|
|
954
1255
|
function firstMatch(src, re) {
|
|
955
1256
|
const m = src.match(re);
|