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.
- package/LICENSE +27 -0
- package/NOTICE +49 -0
- package/README.md +164 -0
- package/RNIntune.podspec +69 -0
- package/android/build.gradle +152 -0
- package/android/consumer-rules.pro +28 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneAuth.kt +330 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneAuthCallback.kt +195 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneConfig.kt +63 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneEventQueue.kt +50 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneModule.kt +752 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneMsalConfig.kt +78 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneNotifications.kt +187 -0
- package/android/src/main/java/com/reactnativeintune/RNIntunePackage.kt +33 -0
- package/android/src/main/java/com/reactnativeintune/RNIntunePolicy.kt +108 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneReset.kt +85 -0
- package/android/src/main/java/com/reactnativeintune/RNIntuneResetJournal.kt +140 -0
- package/app.plugin.js +10 -0
- package/ios/RNIntune.h +20 -0
- package/ios/RNIntune.mm +467 -0
- package/ios/RNIntuneAuth.h +121 -0
- package/ios/RNIntuneAuth.m +390 -0
- package/ios/RNIntuneCore.h +150 -0
- package/ios/RNIntuneCore.m +535 -0
- package/ios/RNIntuneDelegates.h +70 -0
- package/ios/RNIntuneDelegates.m +268 -0
- package/ios/RNIntunePendingRequests.h +57 -0
- package/ios/RNIntunePendingRequests.m +230 -0
- package/ios/RNIntunePlistGuard.h +37 -0
- package/ios/RNIntunePlistGuard.m +147 -0
- package/ios/RNIntunePolicy.h +25 -0
- package/ios/RNIntunePolicy.m +112 -0
- package/ios/RNIntuneReset.h +34 -0
- package/ios/RNIntuneReset.m +93 -0
- package/ios/RNIntuneResetJournal.h +69 -0
- package/ios/RNIntuneResetJournal.m +106 -0
- package/lib/module/NativeIntune.js +20 -0
- package/lib/module/NativeIntune.js.map +1 -0
- package/lib/module/api/auth.js +66 -0
- package/lib/module/api/auth.js.map +1 -0
- package/lib/module/api/enrollment.js +104 -0
- package/lib/module/api/enrollment.js.map +1 -0
- package/lib/module/api/events.js +50 -0
- package/lib/module/api/events.js.map +1 -0
- package/lib/module/api/lifecycle.js +73 -0
- package/lib/module/api/lifecycle.js.map +1 -0
- package/lib/module/api/policy.js +23 -0
- package/lib/module/api/policy.js.map +1 -0
- package/lib/module/api/tokenProvider.js +80 -0
- package/lib/module/api/tokenProvider.js.map +1 -0
- package/lib/module/index.js +82 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/internal/decode.js +153 -0
- package/lib/module/internal/decode.js.map +1 -0
- package/lib/module/internal/emitter.js +15 -0
- package/lib/module/internal/emitter.js.map +1 -0
- package/lib/module/internal/errors.js +32 -0
- package/lib/module/internal/errors.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +143 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeIntune.d.ts +120 -0
- package/lib/typescript/src/NativeIntune.d.ts.map +1 -0
- package/lib/typescript/src/api/auth.d.ts +37 -0
- package/lib/typescript/src/api/auth.d.ts.map +1 -0
- package/lib/typescript/src/api/enrollment.d.ts +54 -0
- package/lib/typescript/src/api/enrollment.d.ts.map +1 -0
- package/lib/typescript/src/api/events.d.ts +24 -0
- package/lib/typescript/src/api/events.d.ts.map +1 -0
- package/lib/typescript/src/api/lifecycle.d.ts +33 -0
- package/lib/typescript/src/api/lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/api/policy.d.ts +14 -0
- package/lib/typescript/src/api/policy.d.ts.map +1 -0
- package/lib/typescript/src/api/tokenProvider.d.ts +18 -0
- package/lib/typescript/src/api/tokenProvider.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +67 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/internal/decode.d.ts +48 -0
- package/lib/typescript/src/internal/decode.d.ts.map +1 -0
- package/lib/typescript/src/internal/emitter.d.ts +11 -0
- package/lib/typescript/src/internal/emitter.d.ts.map +1 -0
- package/lib/typescript/src/internal/errors.d.ts +22 -0
- package/lib/typescript/src/internal/errors.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +317 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +204 -0
- package/plugin/transforms.js +153 -0
- package/plugin/withIntune.js +320 -0
- package/scripts/cli.mjs +72 -0
- package/scripts/doctor.mjs +232 -0
- package/scripts/fetch-sdks.mjs +456 -0
- package/scripts/lib/checks.mjs +720 -0
- package/scripts/lib/project.mjs +196 -0
- package/scripts/setup.mjs +233 -0
- package/sdk-lock.json +27 -0
- package/sdk-versions.json +70 -0
- package/src/NativeIntune.ts +130 -0
- package/src/api/auth.ts +73 -0
- package/src/api/enrollment.ts +112 -0
- package/src/api/events.ts +87 -0
- package/src/api/lifecycle.ts +75 -0
- package/src/api/policy.ts +22 -0
- package/src/api/tokenProvider.ts +80 -0
- package/src/index.ts +127 -0
- package/src/internal/decode.ts +191 -0
- package/src/internal/emitter.ts +14 -0
- package/src/internal/errors.ts +43 -0
- 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
|