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.
package/android/build.gradle
CHANGED
|
@@ -93,33 +93,54 @@ gradle.taskGraph.whenReady { graph ->
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
*
|
|
96
|
+
* The MAM SDK is resolved through a repository rather than as a file.
|
|
97
97
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* local .aar file dependencies are not supported when building an AAR".
|
|
101
|
-
*
|
|
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
|
-
*
|
|
104
|
-
*
|
|
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
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
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
|
|
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
|
|
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",
|
package/plugin/transforms.js
CHANGED
|
@@ -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
|
};
|
package/plugin/withIntune.js
CHANGED
|
@@ -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);
|
package/scripts/lib/checks.mjs
CHANGED
|
@@ -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',
|