react-native-intune 0.1.1 → 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 +46 -25
- package/android/src/main/java/com/reactnativeintune/RNIntunePackage.kt +11 -6
- 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 +7 -8
- package/plugin/transforms.js +155 -2
- package/plugin/withIntune.js +27 -0
- package/scripts/doctor.mjs +7 -3
- package/scripts/lib/checks.mjs +483 -27
- 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/scripts/lib/checks.mjs
CHANGED
|
@@ -24,8 +24,26 @@
|
|
|
24
24
|
|
|
25
25
|
import { execFileSync } from 'node:child_process';
|
|
26
26
|
import fs from 'node:fs';
|
|
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';
|
|
27
38
|
import { read, rel } from './project.mjs';
|
|
28
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
|
+
|
|
29
47
|
const MARKER = 'react-native-intune: managed block';
|
|
30
48
|
|
|
31
49
|
export const BROKER_PACKAGES = [
|
|
@@ -115,6 +133,12 @@ const androidChecks = [
|
|
|
115
133
|
verify = true // catches plugin-induced runtime failures
|
|
116
134
|
incremental = true // the default is false
|
|
117
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
|
+
},
|
|
118
142
|
},
|
|
119
143
|
|
|
120
144
|
{
|
|
@@ -140,6 +164,57 @@ const androidChecks = [
|
|
|
140
164
|
And javassist, whose version must match the SDK exactly:
|
|
141
165
|
|
|
142
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
|
+
},
|
|
143
218
|
},
|
|
144
219
|
|
|
145
220
|
{
|
|
@@ -329,6 +404,23 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
|
|
|
329
404
|
RNIntuneAuthCallback.register(this)
|
|
330
405
|
|
|
331
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
|
+
},
|
|
332
424
|
},
|
|
333
425
|
|
|
334
426
|
{
|
|
@@ -380,12 +472,51 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
|
|
|
380
472
|
| openssl sha1 -binary | openssl base64`
|
|
381
473
|
}`;
|
|
382
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
|
+
},
|
|
383
514
|
},
|
|
384
515
|
|
|
385
516
|
{
|
|
386
517
|
id: 'android-toolchain',
|
|
387
518
|
platform: 'android',
|
|
388
|
-
title: 'App toolchain
|
|
519
|
+
title: 'App toolchain is a tested combination',
|
|
389
520
|
/**
|
|
390
521
|
* Advisory, and deliberately so. Mixing matrix rows is *untested* by Microsoft, not
|
|
391
522
|
* known-broken — plenty of combinations work. But when one does not, the failure lands
|
|
@@ -420,26 +551,63 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
|
|
|
420
551
|
firstMatch(src, /kotlinVersion["' ]*[:=]["' ]*([\d.]+)/),
|
|
421
552
|
gradle: gradleWrapperVersion(project),
|
|
422
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
|
+
})();
|
|
423
572
|
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
.filter(([k, want]) => found[k] !== want)
|
|
427
|
-
.map(([k, want]) => `${k} ${found[k]} vs ${want}`);
|
|
428
|
-
|
|
429
|
-
const unseen = Object.keys(REQUIRED_TOOLCHAIN).filter((k) => !found[k]);
|
|
430
|
-
|
|
431
|
-
if (mismatched.length > 0) {
|
|
432
|
-
return { state: 'wrong', detail: mismatched.join(', ') };
|
|
433
|
-
}
|
|
434
|
-
if (unseen.length === Object.keys(REQUIRED_TOOLCHAIN).length) {
|
|
573
|
+
const seen = Object.keys(REQUIRED_TOOLCHAIN).filter((k) => found[k]);
|
|
574
|
+
if (seen.length === 0) {
|
|
435
575
|
return {
|
|
436
576
|
state: 'unknown',
|
|
437
577
|
detail: 'could not read any version from the build files',
|
|
438
578
|
};
|
|
439
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
|
+
}
|
|
440
605
|
return {
|
|
441
|
-
state: '
|
|
442
|
-
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,
|
|
443
611
|
};
|
|
444
612
|
},
|
|
445
613
|
why:
|
|
@@ -448,21 +616,223 @@ ${BROKER_PACKAGES.map((p) => ` <package android:name="${p}" />`).join('\n
|
|
|
448
616
|
'rewrites bytecode in your app module, so when a mixed row does fail it fails ' +
|
|
449
617
|
'inside javassist and the error names one of your classes, never a version.',
|
|
450
618
|
instruction: () =>
|
|
451
|
-
`
|
|
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
|
|
452
622
|
|
|
453
|
-
|
|
454
|
-
AGP ${REQUIRED_TOOLCHAIN.agp}
|
|
455
|
-
Kotlin ${REQUIRED_TOOLCHAIN.kotlin}
|
|
456
|
-
Java 17
|
|
623
|
+
Built and run by this project, off that row:
|
|
457
624
|
|
|
458
|
-
|
|
459
|
-
|
|
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')}
|
|
630
|
+
|
|
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.`,
|
|
460
633
|
},
|
|
461
634
|
];
|
|
462
635
|
|
|
463
636
|
// ---------------------------------------------------------------- ios
|
|
464
637
|
|
|
638
|
+
/**
|
|
639
|
+
* The lowest iOS the vendored MAM SDK runs on. Read from the binary (`otool -l` on
|
|
640
|
+
* IntuneMAMSwift.framework reports `minos 17.0` in both 21.8.0 and 21.9.0), not from
|
|
641
|
+
* Microsoft's release notes — the SDK should move this, not a guess.
|
|
642
|
+
*/
|
|
643
|
+
const MIN_IOS = [17, 0];
|
|
644
|
+
|
|
645
|
+
const versionAtLeast = (value, [major, minor]) => {
|
|
646
|
+
const [a = 0, b = 0] = String(value).split('.').map(Number);
|
|
647
|
+
return a > major || (a === major && b >= minor);
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* The deployment target each configuration of each application target actually builds
|
|
652
|
+
* with: the target's own setting, or the project-level one it inherits when it sets
|
|
653
|
+
* none. A value elsewhere in the file — a project-level default the target overrides,
|
|
654
|
+
* another target — says nothing about what the app is built for, and counting those is
|
|
655
|
+
* how an Expo project, whose project level reads 16.4 while the app target reads 17.0,
|
|
656
|
+
* was reported as wrong.
|
|
657
|
+
*/
|
|
658
|
+
function appDeploymentTargets(pbxproj) {
|
|
659
|
+
const object = (isa) =>
|
|
660
|
+
new RegExp(
|
|
661
|
+
`(\\w{24})\\s*(?:/\\*[^*]*\\*/)?\\s*=\\s*\\{\\s*isa\\s*=\\s*${isa};([\\s\\S]*?)\\n\\t\\t\\};`,
|
|
662
|
+
'g'
|
|
663
|
+
);
|
|
664
|
+
const configs = new Map();
|
|
665
|
+
for (const [, id, body] of pbxproj.matchAll(object('XCBuildConfiguration'))) {
|
|
666
|
+
configs.set(id, {
|
|
667
|
+
name: body.match(/\bname\s*=\s*"?([^";]+)"?;/)?.[1],
|
|
668
|
+
value: body.match(/IPHONEOS_DEPLOYMENT_TARGET\s*=\s*"?([\d.]+)"?;/)?.[1],
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const lists = new Map();
|
|
672
|
+
for (const [, id, body] of pbxproj.matchAll(object('XCConfigurationList'))) {
|
|
673
|
+
const ids = body.match(/buildConfigurations\s*=\s*\(([^)]*)\)/)?.[1] ?? '';
|
|
674
|
+
lists.set(id, [...ids.matchAll(/\w{24}/g)].map((m) => m[0]));
|
|
675
|
+
}
|
|
676
|
+
const listOf = (body) => body.match(/buildConfigurationList\s*=\s*(\w{24})/)?.[1];
|
|
677
|
+
|
|
678
|
+
const [, , projectBody = ''] = [...pbxproj.matchAll(object('PBXProject'))][0] ?? [];
|
|
679
|
+
const inherited = new Map(
|
|
680
|
+
(lists.get(listOf(projectBody)) ?? []).map((id) => [
|
|
681
|
+
configs.get(id)?.name,
|
|
682
|
+
configs.get(id)?.value,
|
|
683
|
+
])
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
const effective = [];
|
|
687
|
+
for (const [, , body] of pbxproj.matchAll(object('PBXNativeTarget'))) {
|
|
688
|
+
if (!/productType\s*=\s*"com\.apple\.product-type\.application"/.test(body)) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
for (const id of lists.get(listOf(body)) ?? []) {
|
|
692
|
+
const c = configs.get(id) ?? {};
|
|
693
|
+
effective.push({ name: c.name, value: c.value ?? inherited.get(c.name) ?? null });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return effective;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Ruby comments — whole-line or trailing — so a commented-out platform line is ignored. */
|
|
700
|
+
const stripRubyComments = (src) => src.replace(/(^|\s)#.*$/gm, '$1');
|
|
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
|
+
|
|
465
722
|
const iosChecks = [
|
|
723
|
+
{
|
|
724
|
+
id: 'ios-deployment-target',
|
|
725
|
+
platform: 'ios',
|
|
726
|
+
title: `Deployment target is iOS ${MIN_IOS.join('.')} or later`,
|
|
727
|
+
severity: 'loud',
|
|
728
|
+
/**
|
|
729
|
+
* React Native's template targets `min_ios_version_supported` — 15.1 — and the SDK
|
|
730
|
+
* is built for 17.0, so a new project fails at `pod install` before anything else
|
|
731
|
+
* here can be tried. The Podfile and the Xcode target are read separately because
|
|
732
|
+
* they disagree independently: a raised Podfile with a 15.1 app target installs the
|
|
733
|
+
* pod and builds, then crashes at launch on any iOS 16 device it was allowed onto.
|
|
734
|
+
*/
|
|
735
|
+
inspect(project) {
|
|
736
|
+
const podfile = stripRubyComments(read(project.ios.podfile));
|
|
737
|
+
if (!podfile) {
|
|
738
|
+
return { state: 'unknown', detail: 'ios/Podfile not found' };
|
|
739
|
+
}
|
|
740
|
+
const line = podfile.match(/^\s*platform\s+:ios\s*,\s*(.+)$/m)?.[1]?.trim();
|
|
741
|
+
if (!line) {
|
|
742
|
+
return { state: 'unknown', detail: 'no `platform :ios` line in the Podfile' };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let target = line.match(/^['"]([\d.]+)['"]$/)?.[1];
|
|
746
|
+
let source = 'Podfile';
|
|
747
|
+
if (!target && /ios\.deploymentTarget/.test(line)) {
|
|
748
|
+
// Expo's generated Podfile reads Podfile.properties.json first, which is what
|
|
749
|
+
// expo-build-properties writes, and falls back to the template minimum.
|
|
750
|
+
const props = path.join(path.dirname(project.ios.podfile), 'Podfile.properties.json');
|
|
751
|
+
try {
|
|
752
|
+
target = JSON.parse(read(props) || '{}')['ios.deploymentTarget'];
|
|
753
|
+
source = 'Podfile.properties.json';
|
|
754
|
+
} catch {
|
|
755
|
+
return { state: 'unknown', detail: 'Podfile.properties.json is not valid JSON' };
|
|
756
|
+
}
|
|
757
|
+
if (!target) {
|
|
758
|
+
return {
|
|
759
|
+
state: 'wrong',
|
|
760
|
+
detail:
|
|
761
|
+
'ios.deploymentTarget is not set, so the Podfile falls back to ' +
|
|
762
|
+
'min_ios_version_supported',
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (!target) {
|
|
767
|
+
return /min_ios_version_supported/.test(line)
|
|
768
|
+
? {
|
|
769
|
+
state: 'wrong',
|
|
770
|
+
detail: 'the Podfile uses min_ios_version_supported, which is below 17.0',
|
|
771
|
+
}
|
|
772
|
+
: { state: 'unknown', detail: `cannot read a version from: ${line}` };
|
|
773
|
+
}
|
|
774
|
+
if (!versionAtLeast(target, MIN_IOS)) {
|
|
775
|
+
return { state: 'wrong', detail: `${source} targets ${target}` };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const effective = appDeploymentTargets(read(project.ios.pbxproj));
|
|
779
|
+
if (effective.length === 0) {
|
|
780
|
+
return {
|
|
781
|
+
state: 'unknown',
|
|
782
|
+
detail: `${source} targets ${target}; no application target found in the Xcode project`,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
// A configuration that sets it nowhere builds for the SDK's own default, which is
|
|
786
|
+
// the newest iOS — not a problem, so only explicit low values count.
|
|
787
|
+
const low = effective.filter((e) => e.value && !versionAtLeast(e.value, MIN_IOS));
|
|
788
|
+
if (low.length > 0) {
|
|
789
|
+
return {
|
|
790
|
+
state: 'wrong',
|
|
791
|
+
detail:
|
|
792
|
+
`${source} targets ${target}, but the app target builds for ` +
|
|
793
|
+
low.map((e) => `${e.value} (${e.name})`).join(', '),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
return { state: 'ok', detail: `${source}: ${target}` };
|
|
797
|
+
},
|
|
798
|
+
why:
|
|
799
|
+
'MAM SDK 21.x is built for iOS 17.0. Below it CocoaPods refuses the pod; with only ' +
|
|
800
|
+
'the Podfile raised, the app builds and then crashes at launch on older devices it ' +
|
|
801
|
+
'still claims to support.',
|
|
802
|
+
instruction: () =>
|
|
803
|
+
`In ios/Podfile:
|
|
804
|
+
|
|
805
|
+
platform :ios, '17.0'
|
|
806
|
+
|
|
807
|
+
and set Minimum Deployments to 17.0 on your app target in Xcode
|
|
808
|
+
(IPHONEOS_DEPLOYMENT_TARGET), then run pod install again.
|
|
809
|
+
|
|
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
|
+
},
|
|
834
|
+
},
|
|
835
|
+
|
|
466
836
|
{
|
|
467
837
|
id: 'ios-keychain-groups',
|
|
468
838
|
platform: 'ios',
|
|
@@ -524,6 +894,40 @@ ${KEYCHAIN_GROUPS.map((g) => ` <string>${g}</string>`).join('\n')}
|
|
|
524
894
|
The app's own group first, then the SDK's, then MSAL's cache. The first entry may be
|
|
525
895
|
written as the literal bundle id or as $(PRODUCT_BUNDLE_IDENTIFIER) — both work.`;
|
|
526
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
|
+
},
|
|
527
931
|
},
|
|
528
932
|
|
|
529
933
|
{
|
|
@@ -558,6 +962,12 @@ ${KEYCHAIN_GROUPS.map((g) => ` <string>${g}</string>`).join('\n')}
|
|
|
558
962
|
</array>
|
|
559
963
|
</dict>
|
|
560
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
|
+
},
|
|
561
971
|
},
|
|
562
972
|
|
|
563
973
|
{
|
|
@@ -587,6 +997,13 @@ ${KEYCHAIN_GROUPS.map((g) => ` <string>${g}</string>`).join('\n')}
|
|
|
587
997
|
${MSAL_QUERY_SCHEMES.map((s) => ` <string>${s}</string>`).join('\n')}
|
|
588
998
|
<string>companyportal</string>
|
|
589
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
|
+
},
|
|
590
1007
|
},
|
|
591
1008
|
|
|
592
1009
|
{
|
|
@@ -617,6 +1034,16 @@ ${MSAL_QUERY_SCHEMES.map((s) => ` <string>${s}</string>`).join('\n')}
|
|
|
617
1034
|
|
|
618
1035
|
Note: ADALCacheKeychainGroupOverride is a different key and is NOT removed — it has
|
|
619
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
|
+
},
|
|
620
1047
|
},
|
|
621
1048
|
|
|
622
1049
|
{
|
|
@@ -649,6 +1076,18 @@ ${MSAL_QUERY_SCHEMES.map((s) => ` <string>${s}</string>`).join('\n')}
|
|
|
649
1076
|
}
|
|
650
1077
|
|
|
651
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
|
+
},
|
|
652
1091
|
},
|
|
653
1092
|
|
|
654
1093
|
{
|
|
@@ -707,8 +1146,14 @@ ${MSAL_QUERY_SCHEMES.map((s) => ` <string>${s}</string>`).join('\n')}
|
|
|
707
1146
|
-i "$SRCROOT/$INFOPLIST_FILE" \\
|
|
708
1147
|
-e "$SRCROOT/${'$'}{CODE_SIGN_ENTITLEMENTS}"
|
|
709
1148
|
|
|
710
|
-
|
|
711
|
-
|
|
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
|
+
},
|
|
712
1157
|
},
|
|
713
1158
|
];
|
|
714
1159
|
|
|
@@ -785,16 +1230,27 @@ export function signatureHash(project) {
|
|
|
785
1230
|
* there is one copy. Duplicating it here is how the two drift and the check starts
|
|
786
1231
|
* reporting a row nobody targets.
|
|
787
1232
|
*/
|
|
788
|
-
const
|
|
1233
|
+
const TOOLCHAIN = (() => {
|
|
789
1234
|
try {
|
|
790
1235
|
const url = new URL('../../sdk-versions.json', import.meta.url);
|
|
791
|
-
|
|
792
|
-
const { requires } = toolchain;
|
|
793
|
-
return { gradle: requires.gradle, agp: requires.agp, kotlin: requires.kotlin };
|
|
1236
|
+
return JSON.parse(fs.readFileSync(url, 'utf8')).toolchain ?? {};
|
|
794
1237
|
} catch {
|
|
795
1238
|
return {};
|
|
796
1239
|
}
|
|
797
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 ?? [];
|
|
798
1254
|
|
|
799
1255
|
function firstMatch(src, re) {
|
|
800
1256
|
const m = src.match(re);
|