react-native-intune 0.1.0-alpha.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.
Files changed (110) hide show
  1. package/LICENSE +27 -0
  2. package/NOTICE +49 -0
  3. package/README.md +164 -0
  4. package/RNIntune.podspec +69 -0
  5. package/android/build.gradle +152 -0
  6. package/android/consumer-rules.pro +28 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/reactnativeintune/RNIntuneAuth.kt +330 -0
  9. package/android/src/main/java/com/reactnativeintune/RNIntuneAuthCallback.kt +195 -0
  10. package/android/src/main/java/com/reactnativeintune/RNIntuneConfig.kt +63 -0
  11. package/android/src/main/java/com/reactnativeintune/RNIntuneEventQueue.kt +50 -0
  12. package/android/src/main/java/com/reactnativeintune/RNIntuneModule.kt +752 -0
  13. package/android/src/main/java/com/reactnativeintune/RNIntuneMsalConfig.kt +78 -0
  14. package/android/src/main/java/com/reactnativeintune/RNIntuneNotifications.kt +187 -0
  15. package/android/src/main/java/com/reactnativeintune/RNIntunePackage.kt +33 -0
  16. package/android/src/main/java/com/reactnativeintune/RNIntunePolicy.kt +108 -0
  17. package/android/src/main/java/com/reactnativeintune/RNIntuneReset.kt +85 -0
  18. package/android/src/main/java/com/reactnativeintune/RNIntuneResetJournal.kt +140 -0
  19. package/app.plugin.js +10 -0
  20. package/ios/RNIntune.h +20 -0
  21. package/ios/RNIntune.mm +467 -0
  22. package/ios/RNIntuneAuth.h +121 -0
  23. package/ios/RNIntuneAuth.m +390 -0
  24. package/ios/RNIntuneCore.h +150 -0
  25. package/ios/RNIntuneCore.m +535 -0
  26. package/ios/RNIntuneDelegates.h +70 -0
  27. package/ios/RNIntuneDelegates.m +268 -0
  28. package/ios/RNIntunePendingRequests.h +57 -0
  29. package/ios/RNIntunePendingRequests.m +230 -0
  30. package/ios/RNIntunePlistGuard.h +37 -0
  31. package/ios/RNIntunePlistGuard.m +147 -0
  32. package/ios/RNIntunePolicy.h +25 -0
  33. package/ios/RNIntunePolicy.m +112 -0
  34. package/ios/RNIntuneReset.h +34 -0
  35. package/ios/RNIntuneReset.m +93 -0
  36. package/ios/RNIntuneResetJournal.h +69 -0
  37. package/ios/RNIntuneResetJournal.m +106 -0
  38. package/lib/module/NativeIntune.js +20 -0
  39. package/lib/module/NativeIntune.js.map +1 -0
  40. package/lib/module/api/auth.js +66 -0
  41. package/lib/module/api/auth.js.map +1 -0
  42. package/lib/module/api/enrollment.js +104 -0
  43. package/lib/module/api/enrollment.js.map +1 -0
  44. package/lib/module/api/events.js +50 -0
  45. package/lib/module/api/events.js.map +1 -0
  46. package/lib/module/api/lifecycle.js +73 -0
  47. package/lib/module/api/lifecycle.js.map +1 -0
  48. package/lib/module/api/policy.js +23 -0
  49. package/lib/module/api/policy.js.map +1 -0
  50. package/lib/module/api/tokenProvider.js +80 -0
  51. package/lib/module/api/tokenProvider.js.map +1 -0
  52. package/lib/module/index.js +82 -0
  53. package/lib/module/index.js.map +1 -0
  54. package/lib/module/internal/decode.js +153 -0
  55. package/lib/module/internal/decode.js.map +1 -0
  56. package/lib/module/internal/emitter.js +15 -0
  57. package/lib/module/internal/emitter.js.map +1 -0
  58. package/lib/module/internal/errors.js +32 -0
  59. package/lib/module/internal/errors.js.map +1 -0
  60. package/lib/module/package.json +1 -0
  61. package/lib/module/types.js +143 -0
  62. package/lib/module/types.js.map +1 -0
  63. package/lib/typescript/package.json +1 -0
  64. package/lib/typescript/src/NativeIntune.d.ts +120 -0
  65. package/lib/typescript/src/NativeIntune.d.ts.map +1 -0
  66. package/lib/typescript/src/api/auth.d.ts +37 -0
  67. package/lib/typescript/src/api/auth.d.ts.map +1 -0
  68. package/lib/typescript/src/api/enrollment.d.ts +54 -0
  69. package/lib/typescript/src/api/enrollment.d.ts.map +1 -0
  70. package/lib/typescript/src/api/events.d.ts +24 -0
  71. package/lib/typescript/src/api/events.d.ts.map +1 -0
  72. package/lib/typescript/src/api/lifecycle.d.ts +33 -0
  73. package/lib/typescript/src/api/lifecycle.d.ts.map +1 -0
  74. package/lib/typescript/src/api/policy.d.ts +14 -0
  75. package/lib/typescript/src/api/policy.d.ts.map +1 -0
  76. package/lib/typescript/src/api/tokenProvider.d.ts +18 -0
  77. package/lib/typescript/src/api/tokenProvider.d.ts.map +1 -0
  78. package/lib/typescript/src/index.d.ts +67 -0
  79. package/lib/typescript/src/index.d.ts.map +1 -0
  80. package/lib/typescript/src/internal/decode.d.ts +48 -0
  81. package/lib/typescript/src/internal/decode.d.ts.map +1 -0
  82. package/lib/typescript/src/internal/emitter.d.ts +11 -0
  83. package/lib/typescript/src/internal/emitter.d.ts.map +1 -0
  84. package/lib/typescript/src/internal/errors.d.ts +22 -0
  85. package/lib/typescript/src/internal/errors.d.ts.map +1 -0
  86. package/lib/typescript/src/types.d.ts +317 -0
  87. package/lib/typescript/src/types.d.ts.map +1 -0
  88. package/package.json +204 -0
  89. package/plugin/transforms.js +153 -0
  90. package/plugin/withIntune.js +320 -0
  91. package/scripts/cli.mjs +72 -0
  92. package/scripts/doctor.mjs +232 -0
  93. package/scripts/fetch-sdks.mjs +456 -0
  94. package/scripts/lib/checks.mjs +720 -0
  95. package/scripts/lib/project.mjs +196 -0
  96. package/scripts/setup.mjs +233 -0
  97. package/sdk-lock.json +27 -0
  98. package/sdk-versions.json +70 -0
  99. package/src/NativeIntune.ts +130 -0
  100. package/src/api/auth.ts +73 -0
  101. package/src/api/enrollment.ts +112 -0
  102. package/src/api/events.ts +87 -0
  103. package/src/api/lifecycle.ts +75 -0
  104. package/src/api/policy.ts +22 -0
  105. package/src/api/tokenProvider.ts +80 -0
  106. package/src/index.ts +127 -0
  107. package/src/internal/decode.ts +191 -0
  108. package/src/internal/emitter.ts +14 -0
  109. package/src/internal/errors.ts +43 -0
  110. package/src/types.ts +381 -0
@@ -0,0 +1,535 @@
1
+ //
2
+ // Copyright (c) 2026 react-native-intune contributors. MIT.
3
+ //
4
+
5
+ #import "RNIntuneCore.h"
6
+ #import "RNIntuneDelegates.h"
7
+ #import "RNIntunePendingRequests.h"
8
+ #import "RNIntunePlistGuard.h"
9
+ #import "RNIntunePolicy.h"
10
+ #import "RNIntuneReset.h"
11
+ #import "RNIntuneResetJournal.h"
12
+
13
+ #import <UIKit/UIKit.h>
14
+
15
+ #import <IntuneMAMSwift/IntuneMAMSwift.h>
16
+
17
+ NSString *const RNIntuneErrorNotConfigured = @"E_NOT_CONFIGURED";
18
+ NSString *const RNIntuneErrorResetRequired = @"E_RESET_REQUIRED";
19
+ NSString *const RNIntuneErrorSDKUnavailable = @"E_SDK_UNAVAILABLE";
20
+ NSString *const RNIntuneErrorInvalidAccountId = @"E_INVALID_ACCOUNT_ID";
21
+ NSString *const RNIntuneErrorNotNeeded = @"E_NOT_NEEDED";
22
+ NSString *const RNIntuneErrorExternalAuthMode = @"E_EXTERNAL_AUTH_MODE";
23
+ NSString *const RNIntuneErrorNative = @"E_NATIVE";
24
+ NSString *const RNIntuneErrorTokenProviderFailed = @"E_TOKEN_PROVIDER_FAILED";
25
+ NSString *const RNIntuneErrorTokenProviderMissing = @"E_TOKEN_PROVIDER_MISSING";
26
+ NSString *const RNIntuneErrorResetInProgress = @"E_RESET_IN_PROGRESS";
27
+ NSString *const RNIntuneErrorInteractionRequired = @"E_INTERACTION_REQUIRED";
28
+ NSString *const RNIntuneErrorUserCancelled = @"E_USER_CANCELLED";
29
+ NSString *const RNIntuneErrorPlistConflict = @"E_PLIST_CONFLICT";
30
+
31
+ NSString *const RNIntuneEventEnrollmentResult = @"enrollmentResult";
32
+ NSString *const RNIntuneEventPolicyChanged = @"policyChanged";
33
+ NSString *const RNIntuneEventUnenrollmentResult = @"unenrollmentResult";
34
+ NSString *const RNIntuneEventWipeRequested = @"wipeRequested";
35
+ NSString *const RNIntuneEventRestartRequired = @"restartRequired";
36
+ NSString *const RNIntuneEventTokenRequest = @"tokenRequest";
37
+ NSString *const RNIntuneEventBrokerStatusChanged = @"brokerStatusChanged";
38
+
39
+ static NSString *const RNIntuneErrorDomain = @"com.reactnativeintune";
40
+
41
+ /// URL schemes the brokers register. Checking these requires matching entries in the
42
+ /// host app's LSApplicationQueriesSchemes — without them iOS returns NO regardless of
43
+ /// what is installed, so a false negative here means a missing plist key, not a missing
44
+ /// app. This is the same trap that silently costs MSAL its broker (SPEC §5.1.2).
45
+ static NSString *const RNIntuneAuthenticatorScheme = @"msauthv2";
46
+ static NSString *const RNIntuneCompanyPortalScheme = @"companyportal";
47
+
48
+
49
+ #pragma mark - Verbose logger
50
+
51
+ /// Bridges the SDK's logger protocol to NSLog. Only installed when `verboseLogging` is
52
+ /// on, which is why there is no level filter: the caller asked for everything.
53
+ @interface RNIntuneVerboseLogger : NSObject <IntuneMAMLogger>
54
+ @end
55
+
56
+ @implementation RNIntuneVerboseLogger
57
+ - (void)log:(NSString *)message level:(IntuneMAMLogLevel)level
58
+ {
59
+ static NSString *const names[] = {@"verbose", @"info", @"warning", @"error"};
60
+ NSString *name = (level <= IntuneMAMLogLevelError) ? names[level] : @"?";
61
+ NSLog(@"[react-native-intune][%@] %@", name, message);
62
+ }
63
+ @end
64
+
65
+ @implementation RNIntuneConfig
66
+ @end
67
+
68
+ @implementation RNIntuneCore {
69
+ RNIntuneConfig *_Nullable _config;
70
+ RNIntuneDelegates *_Nullable _delegates;
71
+ RNIntuneVerboseLogger *_Nullable _logger;
72
+ void (^_Nullable _sink)(NSString *, NSDictionary *);
73
+ RNIntunePendingRequests *_pending;
74
+ RNIntuneAuth *_auth;
75
+ }
76
+
77
+ - (instancetype)init
78
+ {
79
+ if ((self = [super init])) {
80
+ __weak __typeof(self) weakSelf = self;
81
+ _pending = [[RNIntunePendingRequests alloc]
82
+ initWithEventSink:^(NSString *event, NSDictionary *body) {
83
+ [weakSelf emit:event body:body];
84
+ }];
85
+ // Non-nil from the start, holding no tenant state until configured, so no caller
86
+ // needs a nil check — `isConfigured` on it is the only question worth asking.
87
+ _auth = [RNIntuneAuth new];
88
+ }
89
+ return self;
90
+ }
91
+
92
+ + (instancetype)shared
93
+ {
94
+ static RNIntuneCore *shared = nil;
95
+ static dispatch_once_t onceToken;
96
+ dispatch_once(&onceToken, ^{
97
+ shared = [[self alloc] init];
98
+ });
99
+ return shared;
100
+ }
101
+
102
+ - (void)setEventSink:(void (^)(NSString *, NSDictionary *))sink
103
+ {
104
+ _sink = [sink copy];
105
+ }
106
+
107
+ - (void)emit:(NSString *)event body:(NSDictionary *)body
108
+ {
109
+ if (_sink) {
110
+ _sink(event, body);
111
+ }
112
+ }
113
+
114
+ #pragma mark - Availability
115
+
116
+ - (BOOL)sdkAvailable
117
+ {
118
+ // Looked up by name rather than by import so the answer is about *this* binary: is the
119
+ // xcframework actually linked, not merely available at compile time.
120
+ return NSClassFromString(@"IntuneMAMEnrollmentManager") != nil;
121
+ }
122
+
123
+ #pragma mark - Configuration
124
+
125
+ - (BOOL)isConfigured
126
+ {
127
+ return _config != nil;
128
+ }
129
+
130
+ - (nullable NSString *)configuredTenantId
131
+ {
132
+ return _config.tenantId;
133
+ }
134
+
135
+ - (BOOL)applyConfig:(RNIntuneConfig *)config error:(NSError *_Nullable *_Nullable)error
136
+ {
137
+ if (!self.sdkAvailable) {
138
+ if (error) {
139
+ *error = [RNIntuneCore
140
+ errorWithCode:RNIntuneErrorSDKUnavailable
141
+ message:@"The Intune MAM SDK is not linked into this binary. Check that "
142
+ @"vendor/ios was fetched and that pod install ran."];
143
+ }
144
+ return NO;
145
+ }
146
+
147
+ // Switching tenants requires an explicit reset. Reconfiguring in place would leave the
148
+ // previous tenant enrolled, and the runtime overrides below persist across restarts,
149
+ // so the mismatch would survive a relaunch (SPEC §13.1).
150
+ if (_config != nil && ![_config.tenantId isEqualToString:config.tenantId]) {
151
+ if (error) {
152
+ *error = [RNIntuneCore
153
+ errorWithCode:RNIntuneErrorResetRequired
154
+ message:@"configure() was called with a different tenantId than the "
155
+ @"active one. Call reset() before switching tenants."];
156
+ }
157
+ return NO;
158
+ }
159
+
160
+ if (![RNIntunePlistGuard check:config error:error]) {
161
+ return NO;
162
+ }
163
+
164
+ // Runtime overrides. These are class properties, they take effect for subsequent SDK
165
+ // operations, and they persist across restarts until cleared by reset() — a feature
166
+ // for per-tenant configuration and a hazard for tenant switching (SPEC §5.2).
167
+ IntuneMAMSettings.aadClientIdOverride = config.clientId;
168
+ IntuneMAMSettings.aadAuthorityUriOverride = config.authority;
169
+ IntuneMAMSettings.aadRedirectUriOverride = config.redirectUri;
170
+ IntuneMAMSettings.telemetryEnabled = config.telemetryEnabled;
171
+
172
+ // Belt and braces for CLAUDE.md rule 3: even with verbose logging on, these must not
173
+ // reach the SDK's log output.
174
+ IntuneMAMSettings.valuesToScrubFromLogging =
175
+ @[ config.clientId, config.tenantId, config.redirectUri ];
176
+
177
+ // Only assign what was actually asked for: writing nil would clear a colour the app
178
+ // may have set in its plist, and '' is not a colour.
179
+ #define RNIntuneApplyColour(prop, value) \
180
+ if ((value).length > 0) IntuneMAMSettings.prop = (value);
181
+ RNIntuneApplyColour(backgroundColor, config.brandingBackground)
182
+ RNIntuneApplyColour(foregroundColor, config.brandingForeground)
183
+ RNIntuneApplyColour(accentColor, config.brandingAccent)
184
+ RNIntuneApplyColour(secondaryBackgroundColor, config.brandingSecondaryBackground)
185
+ RNIntuneApplyColour(secondaryForegroundColor, config.brandingSecondaryForeground)
186
+ #undef RNIntuneApplyColour
187
+
188
+ if (config.verboseLogging) {
189
+ _logger = [RNIntuneVerboseLogger new];
190
+ IntuneMAMPolicyManager.instance.logger = _logger;
191
+ } else {
192
+ _logger = nil;
193
+ IntuneMAMPolicyManager.instance.logger = nil;
194
+ }
195
+
196
+ // Delegates are installed once and kept for the process lifetime: the SDK calls them
197
+ // unprompted, including before any JS call and during a background policy refresh.
198
+ if (_delegates == nil) {
199
+ __weak __typeof(self) weakSelf = self;
200
+ _delegates = [[RNIntuneDelegates alloc]
201
+ initWithEventSink:^(NSString *event, NSDictionary *body) {
202
+ __strong __typeof(weakSelf) self = weakSelf;
203
+ if (self == nil) {
204
+ return;
205
+ }
206
+ // A service-initiated wipe opens the journal before anyone hears about it.
207
+ //
208
+ // This is the one place worth intercepting: every delegate event passes through
209
+ // here, and the SDK reports a wipe from two different callbacks. Doing it here
210
+ // also means the tenant id is in scope, which the delegates do not have.
211
+ //
212
+ // Why it matters: the SDK terminates the process after the wipe, so a
213
+ // subscriber's cleanup may not finish — and without a journal entry there is no
214
+ // `pendingReset`, so the next launch has no idea anything happened and the app's
215
+ // own data and backend session are never dealt with. §7 already lists remote
216
+ // wipe as one of the callers of the single reset path; this makes that true.
217
+ if ([event isEqualToString:RNIntuneEventWipeRequested]) {
218
+ id accountId = body[@"accountId"];
219
+ [RNIntuneResetJournal.shared
220
+ openForServiceWipeWithAccountId:[accountId isKindOfClass:NSString.class]
221
+ ? accountId
222
+ : nil
223
+ tenantId:self.configuredTenantId];
224
+ }
225
+
226
+ // Settle first, publish second. The same delegate callback serves a caller
227
+ // waiting on enroll() and every subscriber — including for the SDK's own
228
+ // background retries, which have no caller at all (SPEC §13.5).
229
+ [self->_pending settleForEvent:event body:body];
230
+ [self emit:event body:body];
231
+ }
232
+ tokenRequestHandler:^(NSString *accountId, NSString *resource,
233
+ RNIntuneTokenCompletion completion) {
234
+ __strong __typeof(weakSelf) self = weakSelf;
235
+ if (self == nil) {
236
+ // Nobody can answer, so tell the SDK now rather than holding its callback.
237
+ completion(nil, @"The module was deallocated.");
238
+ return;
239
+ }
240
+ [self->_pending handleTokenRequestForAccountId:accountId
241
+ resource:resource
242
+ tenantId:self.configuredTenantId
243
+ authority:self->_config.authority
244
+ completion:completion];
245
+ }];
246
+ IntuneMAMEnrollmentManager.instance.delegate = _delegates;
247
+ IntuneMAMPolicyManager.instance.delegate = _delegates;
248
+
249
+ [NSNotificationCenter.defaultCenter
250
+ addObserver:self
251
+ selector:@selector(policyDidChange:)
252
+ name:IntuneMAMPolicyDidChangeNotification
253
+ object:nil];
254
+ }
255
+ _delegates.restartHandledByApp = config.restartHandledByApp;
256
+ // Only 'external' mode has a token provider behind it. In 'builtin' the module owns
257
+ // MSAL, so the SDK should reach for it directly rather than asking us and being told no.
258
+ _delegates.suppliesTokens = [config.authMode isEqualToString:@"external"];
259
+
260
+ // MSAL, in `builtin` mode only. In `external` the host app owns it and signing in here
261
+ // would put a second MSAL instance and a second cache in one binary (SPEC §3.1).
262
+ //
263
+ // The keychain group comes from the plist, never from `config`: the SDK's half of that
264
+ // setting has no runtime setter, so the plist is the single source and the guard above
265
+ // has already refused a `configure` that disagrees with it (§5.1.4). Pointing MSAL at
266
+ // anything else here is what rule 8 forbids.
267
+ if ([config.authMode isEqualToString:@"builtin"]) {
268
+ NSString *keychainGroup = RNIntunePlistGuard.keychainGroupOverride;
269
+ if (![_auth configureWithClientId:config.clientId
270
+ authority:config.authority
271
+ redirectUri:config.redirectUri
272
+ keychainGroup:keychainGroup.length > 0 ? keychainGroup : nil
273
+ error:error]) {
274
+ return NO;
275
+ }
276
+ }
277
+
278
+ _config = config;
279
+ return YES;
280
+ }
281
+
282
+ - (void)policyDidChange:(NSNotification *)notification
283
+ {
284
+ // Carries `isManaged` only, on purpose. A policy has more fields than belong in an
285
+ // event payload, and the SDK gives no diff — so this says "something changed, re-read"
286
+ // and the subscriber calls getPolicy() (SPEC §4.3).
287
+ [self emit:RNIntuneEventPolicyChanged
288
+ body:@{
289
+ @"isManaged" : @(IntuneMAMPolicyManager.instance.isManagementEnabled),
290
+ }];
291
+ }
292
+
293
+ #pragma mark - Reads
294
+
295
+ - (NSDictionary<NSString *, id> *)brokerStatus
296
+ {
297
+ UIApplication *app = UIApplication.sharedApplication;
298
+
299
+ // The host must be `broker`, not an empty one.
300
+ //
301
+ // `canOpenURL:` against `msauthv2://` answers NO even with Authenticator installed and
302
+ // `msauthv2` declared in LSApplicationQueriesSchemes — found on device, where
303
+ // Authenticator 6.8.54 was present and this method still reported no broker at all.
304
+ // MSAL itself queries `<scheme>://broker`
305
+ // (IdentityCore/src/parameters/MSIDBrokerInvocationOptions.m,
306
+ // `isRequiredBrokerPresent`), and our answer has to agree with the library that
307
+ // actually performs the brokered sign-in — otherwise the app is told to install
308
+ // something it already has, or worse, told it is fine when it is not.
309
+ BOOL (^canOpen)(NSString *) = ^BOOL(NSString *url) {
310
+ NSURL *parsed = [NSURL URLWithString:url];
311
+ return parsed != nil && [app canOpenURL:parsed];
312
+ };
313
+
314
+ NSString *brokerScheme =
315
+ [RNIntuneAuthenticatorScheme stringByAppendingString:@"://broker"];
316
+ BOOL hasAuthenticator = canOpen(brokerScheme);
317
+
318
+ // Company Portal is a broker too, and it is the one Android needs, so probe its own
319
+ // scheme as well. Both forms, because `://broker` is MSAL's convention and
320
+ // `companyportal://` is the scheme Intune's own documentation uses — whichever the
321
+ // installed version answers, the answer is "present".
322
+ BOOL hasCompanyPortal =
323
+ canOpen([RNIntuneCompanyPortalScheme stringByAppendingString:@"://broker"]) ||
324
+ canOpen([RNIntuneCompanyPortalScheme stringByAppendingString:@"://"]);
325
+
326
+ // Through a BOOL variable, not boxed inline. `a || b` has type `int` in C, so
327
+ // `@(hasAuthenticator || hasCompanyPortal)` boxes an NSNumber holding the integer 1
328
+ // rather than a boolean YES — and that crosses the bridge as the JS number `1`, not
329
+ // `true`. Found on device: `brokerAvailable` read false while `authenticatorInstalled`
330
+ // read true, which is arithmetically impossible and was the only field in this
331
+ // dictionary built from an expression instead of a variable.
332
+ BOOL brokerAvailable = hasAuthenticator || hasCompanyPortal;
333
+
334
+ return @{
335
+ // Either broker will do on iOS — Authenticator is the common one.
336
+ @"brokerAvailable" : @(brokerAvailable),
337
+ @"companyPortalInstalled" : @(hasCompanyPortal),
338
+ @"authenticatorInstalled" : @(hasAuthenticator),
339
+ // YES, and this used to say NO with the comment "unlike Android, iOS can enroll
340
+ // without a broker present". That was wrong, and §3.1 was right.
341
+ //
342
+ // Verified by removing Authenticator from an iPad and signing in: MSAL fell back to
343
+ // Safari and the *Entra* sign-in completed, but Microsoft's own page then refused to
344
+ // go further without the Authenticator app installed. The browser is a middleman for
345
+ // the sign-in, not a substitute for the broker, and enrollment needs a broker-issued
346
+ // token.
347
+ //
348
+ // Strictly the requirement comes from the tenant's policy and Conditional Access
349
+ // configuration, so a module cannot know in advance whether a given tenant enforces
350
+ // it. The costs are not symmetric: prompting for a broker that turns out unnecessary
351
+ // is harmless, while not prompting for one that is needed leaves the user stuck on a
352
+ // Microsoft page mid-sign-in with no explanation from the app. So this errs toward
353
+ // prompting.
354
+ @"required" : @YES,
355
+ };
356
+ }
357
+
358
+ - (NSDictionary<NSString *, id> *)state
359
+ {
360
+ if (!self.sdkAvailable) {
361
+ return @{
362
+ @"configured" : @NO,
363
+ @"configuredTenantId" : NSNull.null,
364
+ @"registeredAccountIds" : @[],
365
+ @"enrolledAccountId" : NSNull.null,
366
+ @"status" : NSNull.null,
367
+ @"pendingReset" : NSNull.null,
368
+ @"pendingResetReason" : NSNull.null,
369
+ };
370
+ }
371
+
372
+ IntuneMAMEnrollmentManager *manager = IntuneMAMEnrollmentManager.instance;
373
+ NSString *enrolled = manager.enrolledAccountId;
374
+
375
+ return @{
376
+ @"configured" : @(self.isConfigured),
377
+ @"configuredTenantId" : self.configuredTenantId ?: NSNull.null,
378
+ @"registeredAccountIds" : manager.registeredAccountIds ?: @[],
379
+ @"enrolledAccountId" : enrolled ?: NSNull.null,
380
+ // iOS has no "read the current enrollment status" API — the SDK reports status
381
+ // through the delegate and nowhere else. So the truthful answer here is derived:
382
+ // enrolled or not. The last delegate status reaches JS as an event instead
383
+ // (SPEC §4.1).
384
+ @"status" : enrolled != nil ? @"succeeded" : NSNull.null,
385
+ @"pendingReset" : RNIntuneResetJournal.shared.stage ?: NSNull.null,
386
+ @"pendingResetReason" : RNIntuneResetJournal.shared.reason ?: NSNull.null,
387
+ };
388
+ }
389
+
390
+ - (NSDictionary<NSString *, NSString *> *)diagnostics
391
+ {
392
+ NSMutableDictionary<NSString *, NSString *> *out = [NSMutableDictionary new];
393
+ out[@"platform"] = @"ios";
394
+ out[@"sdkLinked"] = self.sdkAvailable ? @"true" : @"false";
395
+ out[@"configured"] = self.isConfigured ? @"true" : @"false";
396
+ // The tenant ID is tenant-identifying but not user-identifying, so it stays. No token,
397
+ // no UPN, and no account ID is added here (CLAUDE.md rule 3).
398
+ out[@"configuredTenantId"] = self.configuredTenantId ?: @"";
399
+ out[@"authMode"] = _config.authMode ?: @"";
400
+ out[@"verboseLogging"] = _config.verboseLogging ? @"true" : @"false";
401
+
402
+ if (self.sdkAvailable) {
403
+ out[@"sdkVersion"] = IntuneMAMVersionInfo.sdkVersion ?: @"";
404
+ out[@"managementEnabled"] =
405
+ IntuneMAMPolicyManager.instance.isManagementEnabled ? @"true" : @"false";
406
+ out[@"enrolled"] =
407
+ IntuneMAMEnrollmentManager.instance.enrolledAccountId != nil ? @"true" : @"false";
408
+
409
+ // Three counts, because the SDK keeps three separate lists and they disagree.
410
+ //
411
+ // Found on device: after a reset that `completeReset` verified as clean, the SDK's
412
+ // own "Remove Account" sheet still listed the previous account — because that sheet
413
+ // is driven by `managedAccountIds`, which the reset verification does not look at.
414
+ // Counts rather than the ids themselves: the question a support ticket asks is
415
+ // "is anything left", not "which".
416
+ out[@"registeredAccountCount"] = [NSString
417
+ stringWithFormat:@"%lu",
418
+ (unsigned long)IntuneMAMEnrollmentManager.instance
419
+ .registeredAccountIds.count];
420
+ out[@"enrolledAccountCount"] = [NSString
421
+ stringWithFormat:@"%lu",
422
+ (unsigned long)IntuneMAMEnrollmentManager.instance
423
+ .enrolledAccountIds.count];
424
+ out[@"managedAccountCount"] = [NSString
425
+ stringWithFormat:@"%lu",
426
+ (unsigned long)IntuneMAMPolicyManager.instance.managedAccountIds
427
+ .count];
428
+ }
429
+
430
+ out[@"plistHasIdentityKeys"] = RNIntunePlistGuard.hasIdentityKeys ? @"true" : @"false";
431
+ out[@"plistMaxFileProtectionLevel"] = RNIntunePlistGuard.maxFileProtectionLevel;
432
+
433
+ return out;
434
+ }
435
+
436
+ #pragma mark - Forwarding
437
+
438
+ - (void)enrollAccountId:(NSString *)accountId
439
+ completion:(void (^)(NSDictionary<NSString *, id> *))completion
440
+ {
441
+ // Already enrolled with *this* account? Answer now.
442
+ //
443
+ // `registerAndEnrollAccountId:` for an account the SDK already holds is a no-op that
444
+ // reports nothing at all — no delegate callback, no notification. The correlation below
445
+ // would then wait for a result that is never coming and settle as `pending` at the
446
+ // 90-second timeout, which is slow and untrue. Observed on an iPad.
447
+ //
448
+ // The same defect was found and fixed on Android, where the SDK is at least explicit
449
+ // about it in logcat ("skipping already registered account"). iOS is silent.
450
+ //
451
+ // Only for the same account, deliberately. A *different* account already being enrolled
452
+ // is the `wrongUser` case, and there the SDK does react — it shows its own account
453
+ // removal UI — so short-circuiting it would hide behaviour the caller needs to see.
454
+ NSString *enrolled = IntuneMAMEnrollmentManager.instance.enrolledAccountId;
455
+ if (enrolled.length > 0 && [enrolled isEqualToString:accountId]) {
456
+ completion(@{
457
+ // Through the mapping table rather than a literal, so there is one definition of
458
+ // what a success is called (SPEC §4.1).
459
+ @"status" : [RNIntuneDelegates
460
+ unifiedStatusForCode:IntuneMAMEnrollmentStatusPoliciesHaveNotChanged],
461
+ @"accountId" : accountId,
462
+ // Module-supplied, like RNIntuneTimeout: the SDK produced no status object here
463
+ // because it was never asked to do any work.
464
+ @"nativeCode" : @"RNIntuneAlreadyEnrolled",
465
+ @"nativeMessage" : @"The account was already enrolled, so the SDK was not asked "
466
+ @"to enroll it again.",
467
+ @"restartRequired" : @NO,
468
+ });
469
+ return;
470
+ }
471
+
472
+ [_pending awaitEnrollment:accountId completion:completion];
473
+ }
474
+
475
+ - (void)enrollInteractiveWithUpn:(NSString *)upn
476
+ completion:(void (^)(NSDictionary<NSString *, id> *))completion
477
+ {
478
+ [_pending awaitInteractiveEnrollmentWithUpn:upn completion:completion];
479
+ }
480
+
481
+ - (void)resolveTokenRequest:(NSString *)requestId token:(NSString *)token
482
+ {
483
+ [_pending resolveTokenRequest:requestId token:token];
484
+ }
485
+
486
+ - (void)rejectTokenRequest:(NSString *)requestId reason:(NSString *)reason
487
+ {
488
+ [_pending rejectTokenRequest:requestId reason:reason];
489
+ }
490
+
491
+ - (nullable NSDictionary<NSString *, id> *)policySnapshot
492
+ {
493
+ return [RNIntunePolicy snapshot];
494
+ }
495
+
496
+ - (BOOL)builtinAuth
497
+ {
498
+ // Absent a config there is nothing to be external about, and `builtin` is the default.
499
+ return _config == nil || [_config.authMode isEqualToString:@"builtin"];
500
+ }
501
+
502
+ - (RNIntuneAuth *)auth
503
+ {
504
+ return _auth;
505
+ }
506
+
507
+ - (void)resetWithWipe:(BOOL)wipe reason:(NSString *)reason
508
+ {
509
+ [RNIntuneReset runWithWipe:wipe
510
+ reason:reason
511
+ tenantId:self.configuredTenantId
512
+ auth:self.builtinAuth ? _auth : nil];
513
+ // Cleared here rather than inside RNIntuneReset: the config is this object's state,
514
+ // and the reset sequence should not be reaching into it.
515
+ _config = nil;
516
+ }
517
+
518
+ - (BOOL)completeReset
519
+ {
520
+ return [RNIntuneReset complete];
521
+ }
522
+
523
+ #pragma mark - Errors
524
+
525
+ + (NSError *)errorWithCode:(NSString *)code message:(NSString *)message
526
+ {
527
+ return [NSError errorWithDomain:RNIntuneErrorDomain
528
+ code:0
529
+ userInfo:@{
530
+ NSLocalizedDescriptionKey : message,
531
+ @"code" : code,
532
+ }];
533
+ }
534
+
535
+ @end
@@ -0,0 +1,70 @@
1
+ //
2
+ // IntuneMAMEnrollmentDelegate and IntuneMAMPolicyDelegate conformance. Pure ObjC, like
3
+ // everything else that faces the SDK (SPEC §5.1.1).
4
+ //
5
+ // The SDK calls these on threads of its own choosing, and it calls them without the app
6
+ // having asked for anything — a wipe or a background policy refresh arrives unprompted.
7
+ // So this object owns no promise; it publishes events, and whoever is waiting correlates
8
+ // (SPEC §12.5).
9
+ //
10
+
11
+ #import <Foundation/Foundation.h>
12
+
13
+ #import <IntuneMAMSwift/IntuneMAMSwift.h>
14
+
15
+ NS_ASSUME_NONNULL_BEGIN
16
+
17
+ /// Emits one event to JS. Implementations must marshal to the JS thread themselves.
18
+ typedef void (^RNIntuneEventSink)(NSString *event, NSDictionary *body);
19
+
20
+ /// Answers a token request with an access token, or with a reason it could not be got.
21
+ typedef void (^RNIntuneTokenCompletion)(NSString *_Nullable token,
22
+ NSString *_Nullable failureReason);
23
+
24
+ /**
25
+ * Asks whoever owns MSAL for a MAM service token.
26
+ *
27
+ * The SDK requests this while enrolling, and again on its own retry schedule with no JS
28
+ * call in flight, which is why it is a registered hook rather than a parameter on
29
+ * `enroll` (SPEC §13.4).
30
+ */
31
+ typedef void (^RNIntuneTokenRequestHandler)(NSString *accountId,
32
+ NSString *resource,
33
+ RNIntuneTokenCompletion completion);
34
+
35
+ @interface RNIntuneDelegates : NSObject <IntuneMAMEnrollmentDelegate, IntuneMAMPolicyDelegate>
36
+
37
+ - (instancetype)initWithEventSink:(RNIntuneEventSink)sink
38
+ tokenRequestHandler:(RNIntuneTokenRequestHandler)tokenHandler
39
+ NS_DESIGNATED_INITIALIZER;
40
+ - (instancetype)init NS_UNAVAILABLE;
41
+
42
+ /**
43
+ * Whether this object should answer `getAccessTokenForAccountId:resource:completion:`.
44
+ *
45
+ * Only in `authMode: 'external'`. `getAccessTokenForAccountId:` is an @optional protocol
46
+ * method, and the SDK checks whether the delegate implements it: if it does not, the SDK
47
+ * acquires the MAM token itself through the app's MSAL. Claiming the method and then
48
+ * answering "no token" is worse than staying silent — it stops the SDK using a path that
49
+ * would have worked, and enrollment dies at the location service with no token attached.
50
+ */
51
+ @property (nonatomic) BOOL suppliesTokens;
52
+
53
+ /// Mirrors `restartHandledByApp` from `configure`. Decides the return value of
54
+ /// `restartApplication`, which is how the SDK asks who performs the restart.
55
+ @property (nonatomic) BOOL restartHandledByApp;
56
+
57
+ /// Maps a raw SDK status onto the unified vocabulary in SPEC §4.1 and shapes it as an
58
+ /// `EnrollmentResult`. Exposed because `getState` needs the same mapping as the delegate.
59
+ + (NSDictionary<NSString *, id> *)resultFromStatus:(IntuneMAMEnrollmentStatus *)status;
60
+
61
+ /// The unified status string alone, for callers that already have a status code.
62
+ + (NSString *)unifiedStatusForCode:(IntuneMAMEnrollmentStatusCode)code;
63
+
64
+ /// The SDK constant's *name*, e.g. `LicensedNotTargeted`. Goes into support logs; the
65
+ /// number is meaningless to anyone reading a ticket.
66
+ + (NSString *)nativeCodeNameForCode:(IntuneMAMEnrollmentStatusCode)code;
67
+
68
+ @end
69
+
70
+ NS_ASSUME_NONNULL_END