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,268 @@
1
+ //
2
+ // Copyright (c) 2026 react-native-intune contributors. MIT.
3
+ //
4
+
5
+ #import "RNIntuneDelegates.h"
6
+ #import "RNIntuneCore.h"
7
+
8
+ /// Unified status strings. Must match `EnrollmentStatus` in src/types.ts.
9
+ static NSString *const kSucceeded = @"succeeded";
10
+ static NSString *const kNotLicensed = @"notLicensed";
11
+ static NSString *const kNotTargeted = @"notTargeted";
12
+ static NSString *const kFailed = @"failed";
13
+ static NSString *const kAuthorizationNeeded = @"authorizationNeeded";
14
+ static NSString *const kWrongUser = @"wrongUser";
15
+ static NSString *const kUnenrolled = @"unenrolled";
16
+ static NSString *const kUnenrollmentFailed = @"unenrollmentFailed";
17
+ static NSString *const kUnknown = @"unknown";
18
+
19
+ @implementation RNIntuneDelegates {
20
+ RNIntuneEventSink _sink;
21
+ RNIntuneTokenRequestHandler _tokenHandler;
22
+ }
23
+
24
+ - (instancetype)initWithEventSink:(RNIntuneEventSink)sink
25
+ tokenRequestHandler:(RNIntuneTokenRequestHandler)tokenHandler
26
+ {
27
+ if ((self = [super init])) {
28
+ _sink = [sink copy];
29
+ _tokenHandler = [tokenHandler copy];
30
+ _restartHandledByApp = NO;
31
+ _suppliesTokens = NO;
32
+ }
33
+ return self;
34
+ }
35
+
36
+ /**
37
+ * The SDK asks before calling, and @optional means "not implemented" is a real answer.
38
+ * In `builtin` mode we want the SDK to use the app's MSAL itself, so we must not appear
39
+ * to offer tokens.
40
+ */
41
+ - (BOOL)respondsToSelector:(SEL)aSelector
42
+ {
43
+ if (aSelector == @selector(getAccessTokenForAccountId:resource:completion:)) {
44
+ return _suppliesTokens;
45
+ }
46
+ return [super respondsToSelector:aSelector];
47
+ }
48
+
49
+ #pragma mark - Status mapping
50
+
51
+ + (NSString *)unifiedStatusForCode:(IntuneMAMEnrollmentStatusCode)code
52
+ {
53
+ // The full table, with its reasoning, is SPEC §4.1. Read it before changing a row:
54
+ // several of these are decisions rather than transcription, and getting NotLicensed
55
+ // versus Failed backwards has opposite consequences for the user (SPEC §8).
56
+ switch (code) {
57
+ // ---- 1xx success ----
58
+ case IntuneMAMEnrollmentStatusNewPoliciesReceived:
59
+ case IntuneMAMEnrollmentStatusPoliciesHaveNotChanged:
60
+ return kSucceeded;
61
+
62
+ // Policy request succeeded, but no policy targets the user. Not a licence problem,
63
+ // and must not block.
64
+ case IntuneMAMEnrollmentStatusNoPolicyReceived:
65
+ case IntuneMAMEnrollmentStatusLicensedNotTargeted:
66
+ return kNotTargeted;
67
+
68
+ // A service-initiated wipe. The account's data is gone, so the state is unenrolled —
69
+ // reporting "succeeded" here is how a wipe ends up ignored.
70
+ case IntuneMAMEnrollmentStatusWipeReceived:
71
+ case IntuneMAMEnrollmentStatusUnenrollmentSuccess:
72
+ case IntuneMAMEnrollmentStatusAppNotEnrolled:
73
+ return kUnenrolled;
74
+
75
+ // ---- 2xx failure ----
76
+ // The call failed; the state is enrolled. enroll() has to be idempotent.
77
+ case IntuneMAMEnrollmentStatusAlreadyEnrolled:
78
+ return kSucceeded;
79
+
80
+ // Microsoft's own comment bundles two meanings into TenantMigration: "the tenant is
81
+ // either undergoing a migration, or the user is not licensed for MAM". Blocking
82
+ // every user of a tenant mid-migration would be a self-inflicted outage, so the
83
+ // non-blocking reading wins.
84
+ case IntuneMAMEnrollmentStatusAccountNotLicensed:
85
+ case IntuneMAMEnrollmentStatusTenantMigration:
86
+ return kNotLicensed;
87
+
88
+ // The user cancelled the prompt, so the true state is "credentials still needed",
89
+ // which is retryable. Mapping it to failed would block a user for pressing Cancel.
90
+ case IntuneMAMEnrollmentStatusAuthRequired:
91
+ case IntuneMAMEnrollmentStatusADALInternalError:
92
+ case IntuneMAMEnrollmentStatusLoginCanceled:
93
+ return kAuthorizationNeeded;
94
+
95
+ // Wrong account, in its several forms. Three of these are documented as requiring
96
+ // the app to remove the account.
97
+ case IntuneMAMEnrollmentStatusNotEmmAccount:
98
+ case IntuneMAMEnrollmentStatusMdmEnrolledDifferentUser:
99
+ case IntuneMAMEnrollmentStatusNotDeviceAccount:
100
+ case IntuneMAMEnrollmentStatusNotEnrolledAccount:
101
+ case IntuneMAMEnrollmentStatusSwitchExistingAccount:
102
+ case IntuneMAMEnrollmentStatusSingleManagedAccountMode:
103
+ return kWrongUser;
104
+
105
+ case IntuneMAMEnrollmentStatusFailedToClearMamData:
106
+ return kUnenrollmentFailed;
107
+
108
+ case IntuneMAMEnrollmentStatusInternalError:
109
+ case IntuneMAMEnrollmentStatusMamServiceDisabled:
110
+ case IntuneMAMEnrollmentStatusLocationServiceFailure:
111
+ case IntuneMAMEnrollmentStatusEnrollmentEndPointNetworkFailure:
112
+ case IntuneMAMEnrollmentStatusParsingFailure:
113
+ case IntuneMAMEnrollmentStatusNilAccount:
114
+ case IntuneMAMEnrollmentStatusPolicyEndPointNetworkFailure:
115
+ case IntuneMAMEnrollmentStatusTimeout:
116
+ case IntuneMAMEnrollmentStatusPolicyRecordGone:
117
+ case IntuneMAMEnrollmentStatusReEnrollForUnenrolledUser:
118
+ case IntuneMAMEnrollmentStatusUnsupportedAPI:
119
+ case IntuneMAMEnrollmentStatusADALMethodUnsupported:
120
+ case IntuneMAMEnrollmentStatusDeviceBlockedEnrollment:
121
+ return kFailed;
122
+ }
123
+
124
+ // No `default:` above, so adding an SDK constant produces a compiler warning here
125
+ // rather than silence. Anything unmapped still degrades to `unknown` instead of
126
+ // throwing, so an SDK update cannot crash a shipped app.
127
+ return kUnknown;
128
+ }
129
+
130
+ + (NSString *)nativeCodeNameForCode:(IntuneMAMEnrollmentStatusCode)code
131
+ {
132
+ #define RNIntuneCaseName(name) \
133
+ case IntuneMAMEnrollmentStatus##name: \
134
+ return @ #name;
135
+
136
+ switch (code) {
137
+ RNIntuneCaseName(NewPoliciesReceived)
138
+ RNIntuneCaseName(PoliciesHaveNotChanged)
139
+ RNIntuneCaseName(WipeReceived)
140
+ RNIntuneCaseName(NoPolicyReceived)
141
+ RNIntuneCaseName(UnenrollmentSuccess)
142
+ RNIntuneCaseName(AccountNotLicensed)
143
+ RNIntuneCaseName(InternalError)
144
+ RNIntuneCaseName(MamServiceDisabled)
145
+ RNIntuneCaseName(AuthRequired)
146
+ RNIntuneCaseName(LocationServiceFailure)
147
+ RNIntuneCaseName(EnrollmentEndPointNetworkFailure)
148
+ RNIntuneCaseName(ParsingFailure)
149
+ RNIntuneCaseName(NilAccount)
150
+ RNIntuneCaseName(AlreadyEnrolled)
151
+ RNIntuneCaseName(NotEmmAccount)
152
+ RNIntuneCaseName(MdmEnrolledDifferentUser)
153
+ RNIntuneCaseName(NotDeviceAccount)
154
+ RNIntuneCaseName(PolicyEndPointNetworkFailure)
155
+ RNIntuneCaseName(AppNotEnrolled)
156
+ RNIntuneCaseName(NotEnrolledAccount)
157
+ RNIntuneCaseName(FailedToClearMamData)
158
+ RNIntuneCaseName(Timeout)
159
+ RNIntuneCaseName(ADALInternalError)
160
+ RNIntuneCaseName(SwitchExistingAccount)
161
+ RNIntuneCaseName(LoginCanceled)
162
+ RNIntuneCaseName(PolicyRecordGone)
163
+ RNIntuneCaseName(ReEnrollForUnenrolledUser)
164
+ RNIntuneCaseName(TenantMigration)
165
+ RNIntuneCaseName(UnsupportedAPI)
166
+ RNIntuneCaseName(ADALMethodUnsupported)
167
+ RNIntuneCaseName(LicensedNotTargeted)
168
+ RNIntuneCaseName(DeviceBlockedEnrollment)
169
+ RNIntuneCaseName(SingleManagedAccountMode)
170
+ }
171
+
172
+ #undef RNIntuneCaseName
173
+
174
+ return [NSString stringWithFormat:@"Unmapped(%lu)", (unsigned long)code];
175
+ }
176
+
177
+ + (NSDictionary<NSString *, id> *)resultFromStatus:(IntuneMAMEnrollmentStatus *)status
178
+ {
179
+ // status.errorString is the SDK's own debug text. It carries no token — but it is not
180
+ // the place to add the UPN either, and status.identity is deliberately not read here
181
+ // (CLAUDE.md rule 3). accountId is the Entra object ID, which is what JS asked with.
182
+ return @{
183
+ @"status" : [self unifiedStatusForCode:status.statusCode],
184
+ @"accountId" : status.accountId ?: NSNull.null,
185
+ @"nativeCode" : [self nativeCodeNameForCode:status.statusCode],
186
+ @"nativeMessage" : status.errorString ?: @"",
187
+ // Set by the policy delegate's restartApplication callback, not by the status.
188
+ @"restartRequired" : @NO,
189
+ };
190
+ }
191
+
192
+ #pragma mark - IntuneMAMEnrollmentDelegate
193
+
194
+ - (void)enrollmentRequestWithStatus:(IntuneMAMEnrollmentStatus *)status
195
+ {
196
+ _sink(RNIntuneEventEnrollmentResult, [RNIntuneDelegates resultFromStatus:status]);
197
+ }
198
+
199
+ - (void)policyRequestWithStatus:(IntuneMAMEnrollmentStatus *)status
200
+ {
201
+ // A wipe arrives here, not through a dedicated callback, and it can arrive with the app
202
+ // having asked for nothing at all. Publish both: the reset machine listens for the
203
+ // wipe, and anything waiting on a policy refresh listens for the result (SPEC §4.4).
204
+ if (status.statusCode == IntuneMAMEnrollmentStatusWipeReceived) {
205
+ _sink(RNIntuneEventWipeRequested, @{@"accountId" : status.accountId ?: NSNull.null});
206
+ }
207
+ _sink(RNIntuneEventEnrollmentResult, [RNIntuneDelegates resultFromStatus:status]);
208
+ }
209
+
210
+ - (void)unenrollRequestWithStatus:(IntuneMAMEnrollmentStatus *)status
211
+ {
212
+ _sink(RNIntuneEventUnenrollmentResult, [RNIntuneDelegates resultFromStatus:status]);
213
+ }
214
+
215
+ /**
216
+ * The SDK asking for a MAM service token. This is the iOS counterpart of Android's
217
+ * MAMServiceAuthenticationCallback.
218
+ *
219
+ * Unlike Android's, this one is asynchronous — the SDK hands us a completion block — so
220
+ * nothing here blocks a thread while JS thinks. The token goes straight back to the SDK
221
+ * and is never returned to JS or logged (CLAUDE.md rule 9).
222
+ */
223
+ - (void)getAccessTokenForAccountId:(NSString *)oid
224
+ resource:(NSString *)resource
225
+ completion:(void (^)(IntuneMAMEnrollmentToken *))completion
226
+ {
227
+ _tokenHandler(oid, resource, ^(NSString *_Nullable token, NSString *_Nullable reason) {
228
+ IntuneMAMEnrollmentToken *result = [IntuneMAMEnrollmentToken new];
229
+ if (token.length > 0) {
230
+ result.accessToken = token;
231
+ result.oid = oid;
232
+ } else {
233
+ // The header is explicit that `error` is required when no token is returned.
234
+ // Without it the SDK cannot distinguish "no token" from a malformed reply.
235
+ result.error = [RNIntuneCore
236
+ errorWithCode:RNIntuneErrorTokenProviderFailed
237
+ message:reason.length > 0 ? reason : @"No MAM service token was provided"];
238
+ }
239
+ completion(result);
240
+ });
241
+ }
242
+
243
+ #pragma mark - IntuneMAMPolicyDelegate
244
+
245
+ - (BOOL)restartApplication
246
+ {
247
+ // Called on a background thread when policy is applied for the first time. Returning
248
+ // TRUE promises that the *host app* restarts itself; FALSE hands the restart to the
249
+ // SDK. We publish the event either way so the app can show something first.
250
+ _sink(RNIntuneEventRestartRequired, @{@"reason" : @"policyAppliedFirstTime"});
251
+ return _restartHandledByApp;
252
+ }
253
+
254
+ - (BOOL)wipeDataForAccountId:(NSString *)accountId
255
+ {
256
+ // Returning NO leaves the SDK's own wipe in place and keeps the app's data untouched
257
+ // here. The app's local data is cleared through the reset journal instead, because the
258
+ // process may not survive long enough to do it inline (SPEC §7).
259
+ _sink(RNIntuneEventWipeRequested, @{@"accountId" : accountId ?: NSNull.null});
260
+ return NO;
261
+ }
262
+
263
+ // identitySwitchRequiredForAccountId:, blockAccountId: and addAccountId: are deliberately
264
+ // not implemented. They exist for multi-identity apps, and single identity is a decided
265
+ // constraint (SPEC §9, CLAUDE.md rule 10): two accounts are never signed in at once, so
266
+ // there is no switch to arbitrate. The SDK's own defaults apply for all three.
267
+
268
+ @end
@@ -0,0 +1,57 @@
1
+ //
2
+ // Everything waiting for the SDK to call back.
3
+ //
4
+ // Two kinds of wait, one set of rules:
5
+ //
6
+ // - **enroll** — `registerAndEnrollAccountId:` returns immediately and says nothing;
7
+ // the outcome arrives later on the delegate. The caller's completion waits here,
8
+ // keyed by account ID.
9
+ // - **token** — the SDK asks for a MAM service token and waits on a completion block
10
+ // while JS answers. Keyed by a generated request ID.
11
+ //
12
+ // Both are settled from SDK-chosen threads, so both are behind one lock. Both time out
13
+ // rather than waiting forever, and both are *taken* rather than read when settled — a
14
+ // late answer after a timeout must not be applied twice.
15
+ //
16
+
17
+ #import <Foundation/Foundation.h>
18
+
19
+ #import "RNIntuneDelegates.h"
20
+
21
+ NS_ASSUME_NONNULL_BEGIN
22
+
23
+ @interface RNIntunePendingRequests : NSObject
24
+
25
+ - (instancetype)initWithEventSink:(RNIntuneEventSink)sink NS_DESIGNATED_INITIALIZER;
26
+ - (instancetype)init NS_UNAVAILABLE;
27
+
28
+ /// Registers `completion`, starts the timeout, then makes the SDK call — in that order,
29
+ /// because `registerAndEnrollAccountId:` can report through the delegate before it
30
+ /// returns.
31
+ - (void)awaitEnrollment:(NSString *)accountId
32
+ completion:(void (^)(NSDictionary<NSString *, id> *result))completion;
33
+
34
+ /**
35
+ * Interactive enrollment, which cannot be keyed by account ID — the caller only has a
36
+ * UPN, and the SDK reports the object ID. Only one can be in flight because the SDK is
37
+ * showing a modal sign-in screen, so a single slot is enough.
38
+ */
39
+ - (void)awaitInteractiveEnrollmentWithUpn:(nullable NSString *)upn
40
+ completion:(void (^)(NSDictionary<NSString *, id> *result))completion;
41
+
42
+ /// Called for every delegate event; settles a waiting caller when one matches.
43
+ - (void)settleForEvent:(NSString *)event body:(NSDictionary *)body;
44
+
45
+ /// Publishes a `tokenRequest` and holds the SDK's completion until JS answers.
46
+ - (void)handleTokenRequestForAccountId:(NSString *)accountId
47
+ resource:(NSString *)resource
48
+ tenantId:(nullable NSString *)tenantId
49
+ authority:(nullable NSString *)authority
50
+ completion:(RNIntuneTokenCompletion)completion;
51
+
52
+ - (void)resolveTokenRequest:(NSString *)requestId token:(NSString *)token;
53
+ - (void)rejectTokenRequest:(NSString *)requestId reason:(NSString *)reason;
54
+
55
+ @end
56
+
57
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,230 @@
1
+ //
2
+ // Copyright (c) 2026 react-native-intune contributors. MIT.
3
+ //
4
+
5
+ #import "RNIntunePendingRequests.h"
6
+ #import "RNIntuneCore.h"
7
+
8
+ #import <IntuneMAMSwift/IntuneMAMSwift.h>
9
+
10
+ /**
11
+ * How long to wait for the SDK to say something before completing an enroll as `pending`.
12
+ * Generous on purpose: enrollment involves a token acquisition and a service round trip,
13
+ * and returning early would report a failure the SDK has not actually reached.
14
+ */
15
+ static const NSTimeInterval RNIntuneEnrollTimeout = 90.0;
16
+
17
+ /// How long JS gets to answer a token request before the SDK is told we cannot supply one.
18
+ static const NSTimeInterval RNIntuneTokenTimeout = 45.0;
19
+
20
+ @implementation RNIntunePendingRequests {
21
+ RNIntuneEventSink _emit;
22
+ /// accountId -> completion, waiting on the enrollment delegate.
23
+ NSMutableDictionary<NSString *, void (^)(NSDictionary *)> *_pendingEnrollments;
24
+ /// requestId -> the SDK's own completion, waiting on JS.
25
+ NSMutableDictionary<NSString *, RNIntuneTokenCompletion> *_pendingTokenRequests;
26
+ /// Guards both: callbacks arrive on threads the SDK chooses.
27
+ NSLock *_lock;
28
+ NSUInteger _requestCounter;
29
+ /// Single slot: the SDK's sign-in screen is modal, so only one can be waiting.
30
+ void (^_Nullable _pendingInteractive)(NSDictionary *);
31
+ }
32
+
33
+ - (instancetype)initWithEventSink:(RNIntuneEventSink)sink
34
+ {
35
+ if ((self = [super init])) {
36
+ _emit = [sink copy];
37
+ _pendingEnrollments = [NSMutableDictionary new];
38
+ _pendingTokenRequests = [NSMutableDictionary new];
39
+ _lock = [NSLock new];
40
+ _requestCounter = 0;
41
+ }
42
+ return self;
43
+ }
44
+
45
+ #pragma mark - Enrollment
46
+
47
+ - (void)awaitEnrollment:(NSString *)accountId
48
+ completion:(void (^)(NSDictionary<NSString *, id> *))completion
49
+ {
50
+ [_lock lock];
51
+ // A second enroll() for the same account while one is in flight is not an error — the
52
+ // SDK is already working on it. Both callers get the one result the delegate reports.
53
+ void (^existing)(NSDictionary *) = _pendingEnrollments[accountId];
54
+ _pendingEnrollments[accountId] = ^(NSDictionary *result) {
55
+ if (existing) {
56
+ existing(result);
57
+ }
58
+ completion(result);
59
+ };
60
+ [_lock unlock];
61
+
62
+ __weak __typeof(self) weakSelf = self;
63
+ dispatch_after(
64
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RNIntuneEnrollTimeout * NSEC_PER_SEC)),
65
+ dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
66
+ // Truthful rather than convenient: the SDK has not failed, it has not answered.
67
+ // It keeps retrying on its own schedule and the result still arrives as an event.
68
+ [weakSelf completeEnrollment:accountId
69
+ withResult:@{
70
+ @"status" : @"pending",
71
+ @"accountId" : accountId,
72
+ @"nativeCode" : @"RNIntuneTimeout",
73
+ @"nativeMessage" :
74
+ @"The SDK did not report a result within the "
75
+ @"timeout. Enrollment continues in the background; "
76
+ @"watch onEnrollmentResult.",
77
+ @"restartRequired" : @NO,
78
+ }];
79
+ });
80
+
81
+ // Returns immediately and reports nothing. Everything meaningful happens above.
82
+ [IntuneMAMEnrollmentManager.instance registerAndEnrollAccountId:accountId];
83
+ }
84
+
85
+ - (void)awaitInteractiveEnrollmentWithUpn:(NSString *)upn
86
+ completion:(void (^)(NSDictionary<NSString *, id> *))completion
87
+ {
88
+ [_lock lock];
89
+ void (^existing)(NSDictionary *) = _pendingInteractive;
90
+ _pendingInteractive = completion;
91
+ [_lock unlock];
92
+
93
+ // A previous interactive attempt that never reported is abandoned rather than left
94
+ // hanging: the user has started a new one, so the old screen is gone.
95
+ if (existing) {
96
+ existing(@{
97
+ @"status" : @"unknown",
98
+ @"accountId" : NSNull.null,
99
+ @"nativeCode" : @"RNIntuneSuperseded",
100
+ @"nativeMessage" : @"A newer interactive enrollment replaced this one.",
101
+ @"restartRequired" : @NO,
102
+ });
103
+ }
104
+
105
+ __weak __typeof(self) weakSelf = self;
106
+ dispatch_after(
107
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RNIntuneEnrollTimeout * NSEC_PER_SEC)),
108
+ dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
109
+ [weakSelf completeInteractiveWithResult:@{
110
+ @"status" : @"pending",
111
+ @"accountId" : NSNull.null,
112
+ @"nativeCode" : @"RNIntuneTimeout",
113
+ @"nativeMessage" : @"The SDK did not report a result within the timeout. The "
114
+ @"sign-in may still be on screen; watch onEnrollmentResult.",
115
+ @"restartRequired" : @NO,
116
+ }];
117
+ });
118
+
119
+ // nil is legal and documented: the SDK then asks for the address itself.
120
+ dispatch_async(dispatch_get_main_queue(), ^{
121
+ [IntuneMAMEnrollmentManager.instance
122
+ loginAndEnrollAccount:upn.length > 0 ? upn : nil];
123
+ });
124
+ }
125
+
126
+ - (void)completeInteractiveWithResult:(NSDictionary *)result
127
+ {
128
+ [_lock lock];
129
+ void (^completion)(NSDictionary *) = _pendingInteractive;
130
+ _pendingInteractive = nil;
131
+ [_lock unlock];
132
+
133
+ if (completion) {
134
+ completion(result);
135
+ }
136
+ }
137
+
138
+ - (void)settleForEvent:(NSString *)event body:(NSDictionary *)body
139
+ {
140
+ if (![event isEqualToString:RNIntuneEventEnrollmentResult]) {
141
+ return;
142
+ }
143
+ // An interactive caller is waiting on a UPN and has no account ID to match, so any
144
+ // enrollment result settles it. The SDK's sign-in screen is modal, so the only thing
145
+ // that could race here is one of its own background retries — an early result, not a
146
+ // wrong one.
147
+ [self completeInteractiveWithResult:body];
148
+
149
+ id accountId = body[@"accountId"];
150
+ if (![accountId isKindOfClass:NSString.class]) {
151
+ return;
152
+ }
153
+ [self completeEnrollment:accountId withResult:body];
154
+ }
155
+
156
+ - (void)completeEnrollment:(NSString *)accountId withResult:(NSDictionary *)result
157
+ {
158
+ [_lock lock];
159
+ void (^completion)(NSDictionary *) = _pendingEnrollments[accountId];
160
+ [_pendingEnrollments removeObjectForKey:accountId];
161
+ [_lock unlock];
162
+
163
+ if (completion) {
164
+ completion(result);
165
+ }
166
+ }
167
+
168
+ #pragma mark - Token bridge
169
+
170
+ - (void)handleTokenRequestForAccountId:(NSString *)accountId
171
+ resource:(NSString *)resource
172
+ tenantId:(NSString *)tenantId
173
+ authority:(NSString *)authority
174
+ completion:(RNIntuneTokenCompletion)completion
175
+ {
176
+ [_lock lock];
177
+ NSString *requestId =
178
+ [NSString stringWithFormat:@"tok-%lu", (unsigned long)(++_requestCounter)];
179
+ _pendingTokenRequests[requestId] = completion;
180
+ [_lock unlock];
181
+
182
+ // `resourceId` is what the provider turns into a scope; the token never comes back
183
+ // through JS to the SDK by any other route (SPEC §13.4).
184
+ _emit(RNIntuneEventTokenRequest,
185
+ @{
186
+ @"requestId" : requestId,
187
+ @"resourceId" : resource ?: @"",
188
+ @"accountId" : accountId ?: @"",
189
+ @"tenantId" : tenantId ?: @"",
190
+ @"authority" : authority ?: @"",
191
+ });
192
+
193
+ __weak __typeof(self) weakSelf = self;
194
+ dispatch_after(
195
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RNIntuneTokenTimeout * NSEC_PER_SEC)),
196
+ dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
197
+ [weakSelf rejectTokenRequest:requestId
198
+ reason:@"The token provider did not answer within the "
199
+ @"timeout."];
200
+ });
201
+ }
202
+
203
+ - (nullable RNIntuneTokenCompletion)takeTokenRequest:(NSString *)requestId
204
+ {
205
+ [_lock lock];
206
+ RNIntuneTokenCompletion completion = _pendingTokenRequests[requestId];
207
+ [_pendingTokenRequests removeObjectForKey:requestId];
208
+ [_lock unlock];
209
+ return completion;
210
+ }
211
+
212
+ - (void)resolveTokenRequest:(NSString *)requestId token:(NSString *)token
213
+ {
214
+ // Taken, not read: a late answer after the timeout already fired must not reach the
215
+ // SDK twice.
216
+ RNIntuneTokenCompletion completion = [self takeTokenRequest:requestId];
217
+ if (completion) {
218
+ completion(token, nil);
219
+ }
220
+ }
221
+
222
+ - (void)rejectTokenRequest:(NSString *)requestId reason:(NSString *)reason
223
+ {
224
+ RNIntuneTokenCompletion completion = [self takeTokenRequest:requestId];
225
+ if (completion) {
226
+ completion(nil, reason);
227
+ }
228
+ }
229
+
230
+ @end
@@ -0,0 +1,37 @@
1
+ //
2
+ // The configure-time assertions (SPEC §12.7 S1).
3
+ //
4
+ // Each one is a configuration mistake that otherwise surfaces days later as
5
+ // `MAMEnrollmentStatusAuthRequired`, in a tenant you cannot reach, with nothing in the
6
+ // logs pointing at the cause. Refusing to start is the kinder failure.
7
+ //
8
+
9
+ #import <Foundation/Foundation.h>
10
+
11
+ @class RNIntuneConfig;
12
+
13
+ NS_ASSUME_NONNULL_BEGIN
14
+
15
+ @interface RNIntunePlistGuard : NSObject
16
+
17
+ /// The host app's `IntuneMAMSettings` dictionary, or an empty one.
18
+ + (NSDictionary *)settings;
19
+
20
+ /// YES when `Info.plist` and `configure()` agree. Otherwise fills `error` with
21
+ /// `E_PLIST_CONFLICT` and a message naming the offending key.
22
+ + (BOOL)check:(RNIntuneConfig *)config error:(NSError *_Nullable *_Nullable)error;
23
+
24
+ /// For `getDiagnostics` — whether any forbidden identity key is present.
25
+ + (BOOL)hasIdentityKeys;
26
+
27
+ /// For `getDiagnostics` — the plist's MaxFileProtectionLevel, or an empty string.
28
+ + (NSString *)maxFileProtectionLevel;
29
+
30
+ /// The plist's `ADALCacheKeychainGroupOverride`, or an empty string. This key has no
31
+ /// runtime setter, so the plist is the only source and MSAL must be pointed at the same
32
+ /// value (§5.1.4).
33
+ + (NSString *)keychainGroupOverride;
34
+
35
+ @end
36
+
37
+ NS_ASSUME_NONNULL_END