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,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
|