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
package/ios/RNIntune.mm
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright (c) 2026 react-native-intune contributors. MIT.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#import "RNIntune.h"
|
|
6
|
+
#import "RNIntuneCore.h"
|
|
7
|
+
|
|
8
|
+
#import <UIKit/UIKit.h>
|
|
9
|
+
|
|
10
|
+
// The only file that may import React (CLAUDE.md rule 7). RCTPresentedViewController()
|
|
11
|
+
// finds the controller MSAL must present its sign-in from, including when the app already
|
|
12
|
+
// has a modal up — which is why the auth layer takes it as a parameter rather than
|
|
13
|
+
// guessing at the key window's root.
|
|
14
|
+
#import <React/RCTUtils.h>
|
|
15
|
+
|
|
16
|
+
/// Rejects with a stable code (SPEC §13.6). RCTPromiseRejectBlock reads `code` off the
|
|
17
|
+
/// NSError's userInfo, so every rejection built by RNIntuneCore arrives in JS with the
|
|
18
|
+
/// documented string rather than a platform error number.
|
|
19
|
+
static void RNIntuneReject(RCTPromiseRejectBlock reject, NSError *error)
|
|
20
|
+
{
|
|
21
|
+
NSString *code = error.userInfo[@"code"] ?: RNIntuneErrorNative;
|
|
22
|
+
reject(code, error.localizedDescription, error);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static void RNIntuneRejectNotConfigured(RCTPromiseRejectBlock reject, NSString *method)
|
|
26
|
+
{
|
|
27
|
+
NSError *error = [RNIntuneCore
|
|
28
|
+
errorWithCode:RNIntuneErrorNotConfigured
|
|
29
|
+
message:[NSString stringWithFormat:
|
|
30
|
+
@"%@ was called before configure() resolved.", method]];
|
|
31
|
+
RNIntuneReject(reject, error);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// `authMode: 'external'` means the host app owns MSAL; signing in here would put a
|
|
35
|
+
/// second instance and a second cache in one binary (SPEC §3.2).
|
|
36
|
+
static void RNIntuneRejectExternalAuth(RCTPromiseRejectBlock reject, NSString *method)
|
|
37
|
+
{
|
|
38
|
+
NSError *error = [RNIntuneCore
|
|
39
|
+
errorWithCode:RNIntuneErrorExternalAuthMode
|
|
40
|
+
message:[NSString stringWithFormat:
|
|
41
|
+
@"%@ is unavailable in authMode 'external'. The host "
|
|
42
|
+
@"app owns MSAL there; supply the account id to "
|
|
43
|
+
@"enroll() and answer tokenRequest via "
|
|
44
|
+
@"setTokenProvider.",
|
|
45
|
+
method]];
|
|
46
|
+
RNIntuneReject(reject, error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Every auth method needs the same three checks in the same order. Returns nil when the
|
|
50
|
+
/// caller may proceed, or the rejection has already been sent.
|
|
51
|
+
static BOOL RNIntuneAuthReady(RCTPromiseRejectBlock reject, NSString *method)
|
|
52
|
+
{
|
|
53
|
+
if (!RNIntuneCore.shared.isConfigured) {
|
|
54
|
+
RNIntuneRejectNotConfigured(reject, method);
|
|
55
|
+
return NO;
|
|
56
|
+
}
|
|
57
|
+
if (!RNIntuneCore.shared.builtinAuth) {
|
|
58
|
+
RNIntuneRejectExternalAuth(reject, method);
|
|
59
|
+
return NO;
|
|
60
|
+
}
|
|
61
|
+
return YES;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Answers one auth call: a result resolves, an error rejects with its stable code.
|
|
65
|
+
static RNIntuneAuthCompletion RNIntuneAuthHandler(RCTPromiseResolveBlock resolve,
|
|
66
|
+
RCTPromiseRejectBlock reject)
|
|
67
|
+
{
|
|
68
|
+
return ^(NSDictionary<NSString *, id> *result, NSError *error) {
|
|
69
|
+
if (result != nil) {
|
|
70
|
+
resolve(result);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
RNIntuneReject(reject, error);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@implementation RNIntune {
|
|
78
|
+
/// Events emitted before JS subscribed. Flushed in startObserving.
|
|
79
|
+
NSMutableArray<NSDictionary *> *_pendingEvents;
|
|
80
|
+
BOOL _observing;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Generates +moduleName. The argument must match TurboModuleRegistry.getEnforcing in
|
|
84
|
+
// src/NativeIntune.ts (SPEC §2.2) — do not also declare +moduleName by hand, the macro
|
|
85
|
+
// already does and the duplicate does not compile.
|
|
86
|
+
RCT_EXPORT_MODULE(RNIntune)
|
|
87
|
+
|
|
88
|
+
- (instancetype)init
|
|
89
|
+
{
|
|
90
|
+
if ((self = [super init])) {
|
|
91
|
+
_pendingEvents = [NSMutableArray new];
|
|
92
|
+
_observing = NO;
|
|
93
|
+
|
|
94
|
+
// Wired here, not in configure(): the SDK can call a delegate before the app has
|
|
95
|
+
// called anything, and an event lost during startup means the reset machine never
|
|
96
|
+
// runs (SPEC §4.4). sendEventWithName: queues while nothing is listening.
|
|
97
|
+
__weak __typeof(self) weakSelf = self;
|
|
98
|
+
[RNIntuneCore.shared setEventSink:^(NSString *event, NSDictionary *body) {
|
|
99
|
+
// The SDK picks its own thread; RCTEventEmitter must be driven from the JS queue.
|
|
100
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
101
|
+
[weakSelf sendEventWithName:event body:body];
|
|
102
|
+
});
|
|
103
|
+
}];
|
|
104
|
+
}
|
|
105
|
+
return self;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
+ (BOOL)requiresMainQueueSetup
|
|
109
|
+
{
|
|
110
|
+
// Construction only allocates the queue. The SDK's own initialisation is triggered by
|
|
111
|
+
// configure(), not by module creation, so there is no reason to block app startup.
|
|
112
|
+
return NO;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#pragma mark - Events
|
|
116
|
+
|
|
117
|
+
- (NSArray<NSString *> *)supportedEvents
|
|
118
|
+
{
|
|
119
|
+
return @[
|
|
120
|
+
RNIntuneEventEnrollmentResult,
|
|
121
|
+
RNIntuneEventPolicyChanged,
|
|
122
|
+
RNIntuneEventUnenrollmentResult,
|
|
123
|
+
RNIntuneEventWipeRequested,
|
|
124
|
+
RNIntuneEventRestartRequired,
|
|
125
|
+
RNIntuneEventTokenRequest,
|
|
126
|
+
RNIntuneEventBrokerStatusChanged,
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
- (void)startObserving
|
|
131
|
+
{
|
|
132
|
+
_observing = YES;
|
|
133
|
+
|
|
134
|
+
// A wipe notification lost during startup means the reset machine never runs, so
|
|
135
|
+
// anything emitted before this point is replayed rather than discarded (SPEC §4.4).
|
|
136
|
+
NSArray<NSDictionary *> *queued = [_pendingEvents copy];
|
|
137
|
+
[_pendingEvents removeAllObjects];
|
|
138
|
+
for (NSDictionary *event in queued) {
|
|
139
|
+
[super sendEventWithName:event[@"name"] body:event[@"body"]];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
- (void)stopObserving
|
|
144
|
+
{
|
|
145
|
+
_observing = NO;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
- (void)sendEventWithName:(NSString *)name body:(id)body
|
|
149
|
+
{
|
|
150
|
+
if (_observing) {
|
|
151
|
+
[super sendEventWithName:name body:body];
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
[_pendingEvents addObject:@{@"name" : name, @"body" : body ?: NSNull.null}];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#pragma mark - Lifecycle
|
|
158
|
+
|
|
159
|
+
- (void)configure:(JS::NativeIntune::SpecConfigureConfig &)config
|
|
160
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
161
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
162
|
+
{
|
|
163
|
+
// The generated struct is a typed view over the NSDictionary, so a renamed spec field
|
|
164
|
+
// fails to compile here instead of silently reading nil. Everything crossing into the
|
|
165
|
+
// .m core below is a plain ObjC type.
|
|
166
|
+
RNIntuneConfig *resolved = [RNIntuneConfig new];
|
|
167
|
+
resolved.clientId = config.clientId();
|
|
168
|
+
resolved.tenantId = config.tenantId();
|
|
169
|
+
resolved.authority = config.authority();
|
|
170
|
+
resolved.redirectUri = config.redirectUri();
|
|
171
|
+
resolved.authMode = config.authMode();
|
|
172
|
+
resolved.maxFileProtectionLevel = config.maxFileProtectionLevel();
|
|
173
|
+
resolved.keychainGroupOverride = config.keychainGroupOverride();
|
|
174
|
+
resolved.verboseLogging = config.verboseLogging();
|
|
175
|
+
resolved.restartHandledByApp = config.restartHandledByApp();
|
|
176
|
+
resolved.telemetryEnabled = config.telemetryEnabled();
|
|
177
|
+
resolved.brandingBackground = config.brandingBackground();
|
|
178
|
+
resolved.brandingForeground = config.brandingForeground();
|
|
179
|
+
resolved.brandingAccent = config.brandingAccent();
|
|
180
|
+
resolved.brandingSecondaryBackground = config.brandingSecondaryBackground();
|
|
181
|
+
resolved.brandingSecondaryForeground = config.brandingSecondaryForeground();
|
|
182
|
+
|
|
183
|
+
NSError *error = nil;
|
|
184
|
+
if (![RNIntuneCore.shared applyConfig:resolved error:&error]) {
|
|
185
|
+
RNIntuneReject(reject, error);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
resolve(nil);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
- (void)isSupported:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
192
|
+
{
|
|
193
|
+
resolve(@(RNIntuneCore.shared.sdkAvailable));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
- (void)getBrokerStatus:(RCTPromiseResolveBlock)resolve
|
|
197
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
198
|
+
{
|
|
199
|
+
resolve([RNIntuneCore.shared brokerStatus]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
- (void)openBrokerInstall:(RCTPromiseResolveBlock)resolve
|
|
203
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
204
|
+
{
|
|
205
|
+
NSDictionary *status = [RNIntuneCore.shared brokerStatus];
|
|
206
|
+
if ([status[@"brokerAvailable"] boolValue]) {
|
|
207
|
+
RNIntuneReject(reject,
|
|
208
|
+
[RNIntuneCore errorWithCode:RNIntuneErrorNotNeeded
|
|
209
|
+
message:@"A broker is already installed."]);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// A search URL rather than a hardcoded App Store id. The numeric id for Authenticator
|
|
214
|
+
// is well known but is not something this repository has verified, and a wrong one
|
|
215
|
+
// sends users to the wrong app — CLAUDE.md rule 1 applies to store identifiers too.
|
|
216
|
+
NSURL *url = [NSURL
|
|
217
|
+
URLWithString:@"itms-apps://apps.apple.com/search?term=Microsoft%20Authenticator"];
|
|
218
|
+
|
|
219
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
220
|
+
[UIApplication.sharedApplication openURL:url
|
|
221
|
+
options:@{}
|
|
222
|
+
completionHandler:^(BOOL success) {
|
|
223
|
+
if (success) {
|
|
224
|
+
resolve(nil);
|
|
225
|
+
} else {
|
|
226
|
+
RNIntuneReject(
|
|
227
|
+
reject,
|
|
228
|
+
[RNIntuneCore
|
|
229
|
+
errorWithCode:RNIntuneErrorNative
|
|
230
|
+
message:@"Could not open the App Store."]);
|
|
231
|
+
}
|
|
232
|
+
}];
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#pragma mark - Auth
|
|
237
|
+
|
|
238
|
+
/// These five take `Object` in the Codegen spec, so the values arrive untyped and are
|
|
239
|
+
/// read defensively here. `index.ts` has already narrowed and defaulted them
|
|
240
|
+
/// (CLAUDE.md rule 11) — this is the second line of defence, not the first.
|
|
241
|
+
static NSArray<NSString *> *RNIntuneScopes(NSDictionary *params)
|
|
242
|
+
{
|
|
243
|
+
id scopes = params[@"scopes"];
|
|
244
|
+
return [scopes isKindOfClass:NSArray.class] ? scopes : @[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static NSString *RNIntuneOptionalString(NSDictionary *params, NSString *key)
|
|
248
|
+
{
|
|
249
|
+
id value = params[key];
|
|
250
|
+
return [value isKindOfClass:NSString.class] && [value length] > 0 ? value : nil;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
- (void)signIn:(NSDictionary *)params
|
|
254
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
255
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
256
|
+
{
|
|
257
|
+
if (!RNIntuneAuthReady(reject, @"signIn")) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
[RNIntuneCore.shared.auth
|
|
261
|
+
signInWithScopes:RNIntuneScopes(params)
|
|
262
|
+
loginHint:RNIntuneOptionalString(params, @"loginHint")
|
|
263
|
+
prompt:RNIntuneOptionalString(params, @"prompt")
|
|
264
|
+
presentingController:RCTPresentedViewController()
|
|
265
|
+
completion:RNIntuneAuthHandler(resolve, reject)];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
- (void)signInSilent:(NSDictionary *)params
|
|
269
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
270
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
271
|
+
{
|
|
272
|
+
if (!RNIntuneAuthReady(reject, @"signInSilent")) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
[RNIntuneCore.shared.auth
|
|
276
|
+
signInSilentWithScopes:RNIntuneScopes(params)
|
|
277
|
+
accountId:RNIntuneOptionalString(params, @"accountId")
|
|
278
|
+
completion:RNIntuneAuthHandler(resolve, reject)];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
- (void)acquireToken:(NSDictionary *)params
|
|
282
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
283
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
284
|
+
{
|
|
285
|
+
if (!RNIntuneAuthReady(reject, @"acquireToken")) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
id forceRefresh = params[@"forceRefresh"];
|
|
289
|
+
[RNIntuneCore.shared.auth
|
|
290
|
+
acquireTokenWithScopes:RNIntuneScopes(params)
|
|
291
|
+
accountId:RNIntuneOptionalString(params, @"accountId")
|
|
292
|
+
forceRefresh:[forceRefresh isKindOfClass:NSNumber.class]
|
|
293
|
+
? [forceRefresh boolValue]
|
|
294
|
+
: NO
|
|
295
|
+
completion:RNIntuneAuthHandler(resolve, reject)];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
- (void)getAccounts:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
299
|
+
{
|
|
300
|
+
if (!RNIntuneAuthReady(reject, @"getAccounts")) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
NSError *error = nil;
|
|
304
|
+
NSArray *accounts = [RNIntuneCore.shared.auth accountsWithError:&error];
|
|
305
|
+
if (accounts == nil) {
|
|
306
|
+
RNIntuneReject(reject, error);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
resolve(accounts);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
- (void)signOut:(JS::NativeIntune::SpecSignOutParams &)params
|
|
313
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
314
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
315
|
+
{
|
|
316
|
+
if (!RNIntuneAuthReady(reject, @"signOut")) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
NSString *accountId = params.accountId();
|
|
321
|
+
BOOL wipeIntune = params.wipeIntune();
|
|
322
|
+
|
|
323
|
+
// Off the main thread whenever a wipe is involved: the unregister inside the reset
|
|
324
|
+
// sequence blocks while it acquires the Intune AAD token (SPEC §5.3, §12.5).
|
|
325
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
326
|
+
if (wipeIntune) {
|
|
327
|
+
// The order is the whole point of doing this in one place: unregister first, then
|
|
328
|
+
// clear MSAL. Reversed, the unregister loses the token it needs and leaves the
|
|
329
|
+
// account registered — which is what the host app could not reliably sequence back
|
|
330
|
+
// when MSAL lived on its side (SPEC §13.1.1).
|
|
331
|
+
[RNIntuneCore.shared resetWithWipe:YES reason:@"logout"];
|
|
332
|
+
resolve(nil);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
NSError *error = nil;
|
|
337
|
+
if (![RNIntuneCore.shared.auth removeAccountId:accountId error:&error]) {
|
|
338
|
+
RNIntuneReject(reject, error);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
resolve(nil);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#pragma mark - Enrollment
|
|
346
|
+
|
|
347
|
+
- (void)enroll:(JS::NativeIntune::SpecEnrollParams &)params
|
|
348
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
349
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
350
|
+
{
|
|
351
|
+
if (!RNIntuneCore.shared.isConfigured) {
|
|
352
|
+
RNIntuneRejectNotConfigured(reject, @"enroll");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
NSString *accountId = params.accountId();
|
|
357
|
+
if (accountId.length == 0) {
|
|
358
|
+
RNIntuneReject(reject,
|
|
359
|
+
[RNIntuneCore errorWithCode:RNIntuneErrorInvalidAccountId
|
|
360
|
+
message:@"enroll() requires an Entra object ID."]);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Resolves for every outcome including failures — a non-success status is data, not an
|
|
365
|
+
// exception (SPEC §13.2). It only rejects for the programming errors above.
|
|
366
|
+
[RNIntuneCore.shared enrollAccountId:accountId
|
|
367
|
+
completion:^(NSDictionary<NSString *, id> *result) {
|
|
368
|
+
resolve(result);
|
|
369
|
+
}];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
- (void)enrollInteractive:(JS::NativeIntune::SpecEnrollInteractiveParams &)params
|
|
373
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
374
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
375
|
+
{
|
|
376
|
+
if (!RNIntuneCore.shared.isConfigured) {
|
|
377
|
+
RNIntuneRejectNotConfigured(reject, @"enrollInteractive");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
[RNIntuneCore.shared
|
|
381
|
+
enrollInteractiveWithUpn:params.upn()
|
|
382
|
+
completion:^(NSDictionary<NSString *, id> *result) {
|
|
383
|
+
resolve(result);
|
|
384
|
+
}];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
- (void)getState:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
388
|
+
{
|
|
389
|
+
// Deliberately never rejects: reporting `configured: false` is what makes this the
|
|
390
|
+
// reconciliation primitive (SPEC §13.2).
|
|
391
|
+
resolve([RNIntuneCore.shared state]);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
- (void)reset:(JS::NativeIntune::SpecResetParams &)params
|
|
395
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
396
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
397
|
+
{
|
|
398
|
+
BOOL wipe = params.wipe();
|
|
399
|
+
NSString *reason = params.reason();
|
|
400
|
+
|
|
401
|
+
// deRegisterAndUnenrollAccountId: blocks while it acquires the Intune AAD token, so it
|
|
402
|
+
// can never run on the main thread (SPEC §5.3, §12.5).
|
|
403
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
404
|
+
[RNIntuneCore.shared resetWithWipe:wipe reason:reason];
|
|
405
|
+
resolve(nil);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
- (void)completeReset:(RCTPromiseResolveBlock)resolve
|
|
410
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
411
|
+
{
|
|
412
|
+
if ([RNIntuneCore.shared completeReset]) {
|
|
413
|
+
resolve(nil);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// Left open on purpose: the next launch sees pendingReset and retries. Reporting
|
|
417
|
+
// success here would strand an account the SDK will keep trying to re-enroll.
|
|
418
|
+
RNIntuneReject(reject,
|
|
419
|
+
[RNIntuneCore errorWithCode:RNIntuneErrorResetInProgress
|
|
420
|
+
message:@"The account is still registered after "
|
|
421
|
+
@"reset, so the journal was left open. It "
|
|
422
|
+
@"will be retried on the next launch."]);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
#pragma mark - Policy
|
|
426
|
+
|
|
427
|
+
- (void)getPolicy:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
428
|
+
{
|
|
429
|
+
NSDictionary *snapshot = [RNIntuneCore.shared policySnapshot];
|
|
430
|
+
if (snapshot == nil) {
|
|
431
|
+
RNIntuneReject(reject,
|
|
432
|
+
[RNIntuneCore errorWithCode:RNIntuneErrorSDKUnavailable
|
|
433
|
+
message:@"The Intune MAM SDK is not linked."]);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
resolve(snapshot);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
- (void)getDiagnostics:(RCTPromiseResolveBlock)resolve
|
|
440
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
441
|
+
{
|
|
442
|
+
resolve([RNIntuneCore.shared diagnostics]);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#pragma mark - Token provider
|
|
446
|
+
|
|
447
|
+
// Not public API — setTokenProvider in index.ts wraps these. The token goes straight to
|
|
448
|
+
// the SDK and is never logged or returned to JS (CLAUDE.md rule 9).
|
|
449
|
+
- (void)resolveToken:(JS::NativeIntune::SpecResolveTokenParams &)params
|
|
450
|
+
{
|
|
451
|
+
[RNIntuneCore.shared resolveTokenRequest:params.requestId() token:params.token()];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
- (void)rejectToken:(JS::NativeIntune::SpecRejectTokenParams &)params
|
|
455
|
+
{
|
|
456
|
+
[RNIntuneCore.shared rejectTokenRequest:params.requestId() reason:params.reason()];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#pragma mark - TurboModule
|
|
460
|
+
|
|
461
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
462
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
463
|
+
{
|
|
464
|
+
return std::make_shared<facebook::react::NativeIntuneSpecJSI>(params);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//
|
|
2
|
+
// MSAL: the client, sign-in, and tokens for the app's own scopes (SPEC §3, spike S-3).
|
|
3
|
+
//
|
|
4
|
+
// Pure Objective-C and free of React imports, like every other file on this side of the
|
|
5
|
+
// boundary — `RNIntune.mm` is the only place that knows about React (CLAUDE.md rule 7).
|
|
6
|
+
// That is why the presenting view controller arrives as a parameter instead of being
|
|
7
|
+
// looked up here: finding it is the bridge's job.
|
|
8
|
+
//
|
|
9
|
+
// Two tokens exist and only one of them leaves this file. The MAM service token is never
|
|
10
|
+
// returned, never logged, and is not even acquired here — the SDK acquires it itself,
|
|
11
|
+
// from the cache this object populates (§5.1.3). Everything returned by these methods is
|
|
12
|
+
// for the *app's* scopes (§3.5, CLAUDE.md rule 9).
|
|
13
|
+
//
|
|
14
|
+
|
|
15
|
+
#import <Foundation/Foundation.h>
|
|
16
|
+
#import <UIKit/UIKit.h>
|
|
17
|
+
|
|
18
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
19
|
+
|
|
20
|
+
/// Completes with an `AuthResult`-shaped dictionary, or an error carrying one of the
|
|
21
|
+
/// stable codes in SPEC §13.6. Never both.
|
|
22
|
+
typedef void (^RNIntuneAuthCompletion)(NSDictionary<NSString *, id> *_Nullable result,
|
|
23
|
+
NSError *_Nullable error);
|
|
24
|
+
|
|
25
|
+
@interface RNIntuneAuth : NSObject
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the MSAL client for one tenant.
|
|
29
|
+
*
|
|
30
|
+
* `keychainGroup` is the value resolved from the host app's plist, not from `configure`
|
|
31
|
+
* (§5.1.4): the Intune SDK's half of this setting has no runtime setter, so the group is
|
|
32
|
+
* a build-time value and `configure` only asserts agreement. Pass nil to let MSAL keep
|
|
33
|
+
* its own default, which is already what the Intune SDK expects.
|
|
34
|
+
*
|
|
35
|
+
* Returns NO and fills `error` when MSAL rejects the configuration — a malformed
|
|
36
|
+
* authority URL is the common case, and it is worth failing loudly at `configure` rather
|
|
37
|
+
* than at the first sign-in.
|
|
38
|
+
*/
|
|
39
|
+
- (BOOL)configureWithClientId:(NSString *)clientId
|
|
40
|
+
authority:(NSString *)authority
|
|
41
|
+
redirectUri:(NSString *)redirectUri
|
|
42
|
+
keychainGroup:(nullable NSString *)keychainGroup
|
|
43
|
+
error:(NSError *_Nullable *_Nullable)error;
|
|
44
|
+
|
|
45
|
+
/// Drops the client and forgets the tenant. Called from `reset`, which must leave nothing
|
|
46
|
+
/// pinned to the old tenant (SPEC §7).
|
|
47
|
+
- (void)invalidate;
|
|
48
|
+
|
|
49
|
+
@property (nonatomic, readonly, getter=isConfigured) BOOL configured;
|
|
50
|
+
|
|
51
|
+
#pragma mark - Sign-in
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Interactive sign-in. Uses the broker when one is present, which also gives device-wide
|
|
55
|
+
* SSO: a user already signed into another Microsoft app usually gets a token with no
|
|
56
|
+
* prompt at all.
|
|
57
|
+
*
|
|
58
|
+
* `scopes` may be empty — see `defaultScopes`. `prompt` takes the public API's strings
|
|
59
|
+
* (`selectAccount`, `login`, `consent`, `whenRequired`); anything else falls back to
|
|
60
|
+
* MSAL's default rather than rejecting, so a future value added to the JS enum cannot
|
|
61
|
+
* break a shipped native binary.
|
|
62
|
+
*/
|
|
63
|
+
- (void)signInWithScopes:(NSArray<NSString *> *)scopes
|
|
64
|
+
loginHint:(nullable NSString *)loginHint
|
|
65
|
+
prompt:(nullable NSString *)prompt
|
|
66
|
+
presentingController:(UIViewController *)controller
|
|
67
|
+
completion:(RNIntuneAuthCompletion)completion;
|
|
68
|
+
|
|
69
|
+
/// Cache-first. Fails with `E_INTERACTION_REQUIRED` when a prompt is needed — the callers'
|
|
70
|
+
/// pattern is silent first, interactive on that failure.
|
|
71
|
+
- (void)signInSilentWithScopes:(NSArray<NSString *> *)scopes
|
|
72
|
+
accountId:(nullable NSString *)accountId
|
|
73
|
+
completion:(RNIntuneAuthCompletion)completion;
|
|
74
|
+
|
|
75
|
+
/// A token for the app's own scopes, for an account that is already signed in.
|
|
76
|
+
- (void)acquireTokenWithScopes:(NSArray<NSString *> *)scopes
|
|
77
|
+
accountId:(nullable NSString *)accountId
|
|
78
|
+
forceRefresh:(BOOL)forceRefresh
|
|
79
|
+
completion:(RNIntuneAuthCompletion)completion;
|
|
80
|
+
|
|
81
|
+
#pragma mark - Accounts
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Accounts with cached refresh tokens, as `AuthAccount` dictionaries.
|
|
85
|
+
*
|
|
86
|
+
* Under single identity (SPEC §9) this holds at most one; more than one means state a
|
|
87
|
+
* reset should have cleared, which is worth surfacing rather than hiding.
|
|
88
|
+
*/
|
|
89
|
+
- (nullable NSArray<NSDictionary<NSString *, id> *> *)accountsWithError:
|
|
90
|
+
(NSError *_Nullable *_Nullable)error;
|
|
91
|
+
|
|
92
|
+
/// Removes the account and its tokens from the MSAL cache. This is the half of `reset`
|
|
93
|
+
/// the module could not do while MSAL lived in the host app (SPEC §7 step 4).
|
|
94
|
+
- (BOOL)removeAccountId:(NSString *)accountId error:(NSError *_Nullable *_Nullable)error;
|
|
95
|
+
|
|
96
|
+
/// Empties the cache of every account. Used by `reset`, which runs without a specific
|
|
97
|
+
/// account in hand once the SDK has already unregistered it.
|
|
98
|
+
- (void)removeAllAccounts;
|
|
99
|
+
|
|
100
|
+
#pragma mark - Defaults
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* What `signIn` asks for when the caller names no scopes.
|
|
104
|
+
*
|
|
105
|
+
* `https://graph.microsoft.com/User.Read`, because MSAL leaves no cheaper option: it
|
|
106
|
+
* adds `openid`, `profile` and `offline_access` itself and *rejects* a call that names
|
|
107
|
+
* them ("reserved scopes and may not be specified"), so a sign-in must ask for some real
|
|
108
|
+
* resource. `User.Read` is the one Entra grants every new app registration by default,
|
|
109
|
+
* and the one Microsoft's own samples use for exactly this.
|
|
110
|
+
*
|
|
111
|
+
* It is a default, not a requirement — an app with its own API passes its own scopes.
|
|
112
|
+
* What matters for this module is the side effect either way: the refresh token lands in
|
|
113
|
+
* the shared cache, and the SDK then acquires the MAM token from it silently (§5.1.3).
|
|
114
|
+
* The refresh token is per account, not per resource, so which scope got it there does
|
|
115
|
+
* not affect enrollment.
|
|
116
|
+
*/
|
|
117
|
+
@property (class, nonatomic, readonly) NSArray<NSString *> *defaultScopes;
|
|
118
|
+
|
|
119
|
+
@end
|
|
120
|
+
|
|
121
|
+
NS_ASSUME_NONNULL_END
|