react-native-intune 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +27 -0
  2. package/NOTICE +49 -0
  3. package/README.md +164 -0
  4. package/RNIntune.podspec +69 -0
  5. package/android/build.gradle +152 -0
  6. package/android/consumer-rules.pro +28 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/reactnativeintune/RNIntuneAuth.kt +330 -0
  9. package/android/src/main/java/com/reactnativeintune/RNIntuneAuthCallback.kt +195 -0
  10. package/android/src/main/java/com/reactnativeintune/RNIntuneConfig.kt +63 -0
  11. package/android/src/main/java/com/reactnativeintune/RNIntuneEventQueue.kt +50 -0
  12. package/android/src/main/java/com/reactnativeintune/RNIntuneModule.kt +752 -0
  13. package/android/src/main/java/com/reactnativeintune/RNIntuneMsalConfig.kt +78 -0
  14. package/android/src/main/java/com/reactnativeintune/RNIntuneNotifications.kt +187 -0
  15. package/android/src/main/java/com/reactnativeintune/RNIntunePackage.kt +33 -0
  16. package/android/src/main/java/com/reactnativeintune/RNIntunePolicy.kt +108 -0
  17. package/android/src/main/java/com/reactnativeintune/RNIntuneReset.kt +85 -0
  18. package/android/src/main/java/com/reactnativeintune/RNIntuneResetJournal.kt +140 -0
  19. package/app.plugin.js +10 -0
  20. package/ios/RNIntune.h +20 -0
  21. package/ios/RNIntune.mm +467 -0
  22. package/ios/RNIntuneAuth.h +121 -0
  23. package/ios/RNIntuneAuth.m +390 -0
  24. package/ios/RNIntuneCore.h +150 -0
  25. package/ios/RNIntuneCore.m +535 -0
  26. package/ios/RNIntuneDelegates.h +70 -0
  27. package/ios/RNIntuneDelegates.m +268 -0
  28. package/ios/RNIntunePendingRequests.h +57 -0
  29. package/ios/RNIntunePendingRequests.m +230 -0
  30. package/ios/RNIntunePlistGuard.h +37 -0
  31. package/ios/RNIntunePlistGuard.m +147 -0
  32. package/ios/RNIntunePolicy.h +25 -0
  33. package/ios/RNIntunePolicy.m +112 -0
  34. package/ios/RNIntuneReset.h +34 -0
  35. package/ios/RNIntuneReset.m +93 -0
  36. package/ios/RNIntuneResetJournal.h +69 -0
  37. package/ios/RNIntuneResetJournal.m +106 -0
  38. package/lib/module/NativeIntune.js +20 -0
  39. package/lib/module/NativeIntune.js.map +1 -0
  40. package/lib/module/api/auth.js +66 -0
  41. package/lib/module/api/auth.js.map +1 -0
  42. package/lib/module/api/enrollment.js +104 -0
  43. package/lib/module/api/enrollment.js.map +1 -0
  44. package/lib/module/api/events.js +50 -0
  45. package/lib/module/api/events.js.map +1 -0
  46. package/lib/module/api/lifecycle.js +73 -0
  47. package/lib/module/api/lifecycle.js.map +1 -0
  48. package/lib/module/api/policy.js +23 -0
  49. package/lib/module/api/policy.js.map +1 -0
  50. package/lib/module/api/tokenProvider.js +80 -0
  51. package/lib/module/api/tokenProvider.js.map +1 -0
  52. package/lib/module/index.js +82 -0
  53. package/lib/module/index.js.map +1 -0
  54. package/lib/module/internal/decode.js +153 -0
  55. package/lib/module/internal/decode.js.map +1 -0
  56. package/lib/module/internal/emitter.js +15 -0
  57. package/lib/module/internal/emitter.js.map +1 -0
  58. package/lib/module/internal/errors.js +32 -0
  59. package/lib/module/internal/errors.js.map +1 -0
  60. package/lib/module/package.json +1 -0
  61. package/lib/module/types.js +143 -0
  62. package/lib/module/types.js.map +1 -0
  63. package/lib/typescript/package.json +1 -0
  64. package/lib/typescript/src/NativeIntune.d.ts +120 -0
  65. package/lib/typescript/src/NativeIntune.d.ts.map +1 -0
  66. package/lib/typescript/src/api/auth.d.ts +37 -0
  67. package/lib/typescript/src/api/auth.d.ts.map +1 -0
  68. package/lib/typescript/src/api/enrollment.d.ts +54 -0
  69. package/lib/typescript/src/api/enrollment.d.ts.map +1 -0
  70. package/lib/typescript/src/api/events.d.ts +24 -0
  71. package/lib/typescript/src/api/events.d.ts.map +1 -0
  72. package/lib/typescript/src/api/lifecycle.d.ts +33 -0
  73. package/lib/typescript/src/api/lifecycle.d.ts.map +1 -0
  74. package/lib/typescript/src/api/policy.d.ts +14 -0
  75. package/lib/typescript/src/api/policy.d.ts.map +1 -0
  76. package/lib/typescript/src/api/tokenProvider.d.ts +18 -0
  77. package/lib/typescript/src/api/tokenProvider.d.ts.map +1 -0
  78. package/lib/typescript/src/index.d.ts +67 -0
  79. package/lib/typescript/src/index.d.ts.map +1 -0
  80. package/lib/typescript/src/internal/decode.d.ts +48 -0
  81. package/lib/typescript/src/internal/decode.d.ts.map +1 -0
  82. package/lib/typescript/src/internal/emitter.d.ts +11 -0
  83. package/lib/typescript/src/internal/emitter.d.ts.map +1 -0
  84. package/lib/typescript/src/internal/errors.d.ts +22 -0
  85. package/lib/typescript/src/internal/errors.d.ts.map +1 -0
  86. package/lib/typescript/src/types.d.ts +317 -0
  87. package/lib/typescript/src/types.d.ts.map +1 -0
  88. package/package.json +204 -0
  89. package/plugin/transforms.js +153 -0
  90. package/plugin/withIntune.js +320 -0
  91. package/scripts/cli.mjs +72 -0
  92. package/scripts/doctor.mjs +232 -0
  93. package/scripts/fetch-sdks.mjs +456 -0
  94. package/scripts/lib/checks.mjs +720 -0
  95. package/scripts/lib/project.mjs +196 -0
  96. package/scripts/setup.mjs +233 -0
  97. package/sdk-lock.json +27 -0
  98. package/sdk-versions.json +70 -0
  99. package/src/NativeIntune.ts +130 -0
  100. package/src/api/auth.ts +73 -0
  101. package/src/api/enrollment.ts +112 -0
  102. package/src/api/events.ts +87 -0
  103. package/src/api/lifecycle.ts +75 -0
  104. package/src/api/policy.ts +22 -0
  105. package/src/api/tokenProvider.ts +80 -0
  106. package/src/index.ts +127 -0
  107. package/src/internal/decode.ts +191 -0
  108. package/src/internal/emitter.ts +14 -0
  109. package/src/internal/errors.ts +43 -0
  110. package/src/types.ts +381 -0
@@ -0,0 +1,147 @@
1
+ //
2
+ // Copyright (c) 2026 react-native-intune contributors. MIT.
3
+ //
4
+
5
+ #import "RNIntunePlistGuard.h"
6
+ #import "RNIntuneCore.h"
7
+
8
+ /// The `IntuneMAMSettings` dictionary in the host app's Info.plist, and the keys inside
9
+ /// it that the SDK reads at launch. Verified against the strings in the 21.8.0 binary.
10
+ static NSString *const kSettingsKey = @"IntuneMAMSettings";
11
+ static NSString *const kClientId = @"ADALClientId";
12
+ static NSString *const kAuthority = @"ADALAuthority";
13
+ static NSString *const kRedirectUri = @"ADALRedirectUri";
14
+ static NSString *const kKeychainGroup = @"ADALCacheKeychainGroupOverride";
15
+ static NSString *const kMaxFileProtection = @"MaxFileProtectionLevel";
16
+
17
+ @implementation RNIntunePlistGuard
18
+
19
+ + (NSDictionary *)settings
20
+ {
21
+ id settings = NSBundle.mainBundle.infoDictionary[kSettingsKey];
22
+ return [settings isKindOfClass:NSDictionary.class] ? settings : @{};
23
+ }
24
+
25
+ + (BOOL)hasIdentityKeys
26
+ {
27
+ NSDictionary *plist = [self settings];
28
+ return plist[kClientId] != nil || plist[kAuthority] != nil ||
29
+ plist[kRedirectUri] != nil;
30
+ }
31
+
32
+ + (NSString *)maxFileProtectionLevel
33
+ {
34
+ return [self settings][kMaxFileProtection] ?: @"";
35
+ }
36
+
37
+ + (NSString *)keychainGroupOverride
38
+ {
39
+ NSString *value = [self settings][kKeychainGroup];
40
+ return [value isKindOfClass:NSString.class] ? value : @"";
41
+ }
42
+
43
+ + (BOOL)check:(RNIntuneConfig *)config error:(NSError *_Nullable *_Nullable)error
44
+ {
45
+ return [self checkIdentityKeys:error] && [self checkKeychainGroup:config error:error] &&
46
+ [self checkFileProtection:config error:error];
47
+ }
48
+
49
+ #pragma mark - 1. Hardcoded identity next to a runtime override
50
+
51
+ /**
52
+ * GitHub issue #405: the plist value and the runtime override disagree, and enrollment
53
+ * fails with no indication which one won. CLAUDE.md rule 4 forbids these keys outright
54
+ * for exactly this reason.
55
+ */
56
+ + (BOOL)checkIdentityKeys:(NSError *_Nullable *_Nullable)error
57
+ {
58
+ NSDictionary *plist = [self settings];
59
+ NSMutableArray<NSString *> *present = [NSMutableArray new];
60
+ for (NSString *key in @[ kClientId, kAuthority, kRedirectUri ]) {
61
+ if (plist[key] != nil) {
62
+ [present addObject:key];
63
+ }
64
+ }
65
+ if (present.count == 0) {
66
+ return YES;
67
+ }
68
+
69
+ if (error) {
70
+ *error = [RNIntuneCore
71
+ errorWithCode:RNIntuneErrorPlistConflict
72
+ message:[NSString
73
+ stringWithFormat:
74
+ @"Info.plist -> IntuneMAMSettings contains %@, which "
75
+ @"conflicts with the values passed to configure(). This "
76
+ @"module configures identity at runtime only. Remove these "
77
+ @"keys — a conflicting plist key alongside a runtime "
78
+ @"override is a known cause of enrollment failing with "
79
+ @"AuthRequired.",
80
+ [present componentsJoinedByString:@", "]]];
81
+ }
82
+ return NO;
83
+ }
84
+
85
+ #pragma mark - 2. Keychain group, which has no runtime setter
86
+
87
+ /**
88
+ * `ADALCacheKeychainGroupOverride` is read from the plist at launch, so the module cannot
89
+ * "set it to match" as SPEC §5.1.2 originally described. The best it can do is refuse to
90
+ * let the two disagree — MSAL's cache and the SDK's must never diverge.
91
+ */
92
+ + (BOOL)checkKeychainGroup:(RNIntuneConfig *)config
93
+ error:(NSError *_Nullable *_Nullable)error
94
+ {
95
+ NSString *wanted =
96
+ config.keychainGroupOverride.length > 0 ? config.keychainGroupOverride : nil;
97
+ if (wanted == nil) {
98
+ return YES;
99
+ }
100
+
101
+ NSString *inPlist = [self settings][kKeychainGroup];
102
+ if ([inPlist isEqualToString:wanted]) {
103
+ return YES;
104
+ }
105
+
106
+ if (error) {
107
+ *error = [RNIntuneCore
108
+ errorWithCode:RNIntuneErrorPlistConflict
109
+ message:[NSString stringWithFormat:
110
+ @"configure() asked for keychain group \"%@\" but "
111
+ @"Info.plist -> IntuneMAMSettings -> %@ is \"%@\". "
112
+ @"This key has no runtime equivalent, so it must be "
113
+ @"set in the plist.",
114
+ wanted, kKeychainGroup, inPlist ?: @"(absent)"]];
115
+ }
116
+ return NO;
117
+ }
118
+
119
+ #pragma mark - 3. MaxFileProtectionLevel, likewise plist-only
120
+
121
+ /**
122
+ * Silently ignoring a requested value is how an app ends up with a local database that
123
+ * becomes unreadable ten seconds after the device locks (SPEC §5.2, open question O-D).
124
+ */
125
+ + (BOOL)checkFileProtection:(RNIntuneConfig *)config
126
+ error:(NSError *_Nullable *_Nullable)error
127
+ {
128
+ BOOL isDefault = [config.maxFileProtectionLevel isEqualToString:@"complete"];
129
+ if (isDefault || [self settings][kMaxFileProtection] != nil) {
130
+ return YES;
131
+ }
132
+
133
+ if (error) {
134
+ *error = [RNIntuneCore
135
+ errorWithCode:RNIntuneErrorPlistConflict
136
+ message:[NSString
137
+ stringWithFormat:
138
+ @"configure() asked for maxFileProtectionLevel \"%@\", but "
139
+ @"%@ has no runtime equivalent and is absent from "
140
+ @"Info.plist -> IntuneMAMSettings. Set it there (the SDK "
141
+ @"reads it at launch) or drop the option.",
142
+ config.maxFileProtectionLevel, kMaxFileProtection]];
143
+ }
144
+ return NO;
145
+ }
146
+
147
+ @end
@@ -0,0 +1,25 @@
1
+ //
2
+ // PolicySnapshot (SPEC §4.3).
3
+ //
4
+ // For adapting the host app's own UI — nothing here is enforcement. PIN prompts,
5
+ // screenshot blocking and encryption all happen inside the SDK and are not driven by
6
+ // these values.
7
+ //
8
+ // There is no clipboard field and there cannot be one: neither platform exposes a
9
+ // clipboard policy getter. `canSaveToPersonal` and `canOpenFromUnmanaged` serve the same
10
+ // purpose using values the SDK will actually answer.
11
+ //
12
+
13
+ #import <Foundation/Foundation.h>
14
+
15
+ NS_ASSUME_NONNULL_BEGIN
16
+
17
+ @interface RNIntunePolicy : NSObject
18
+
19
+ /// Nil only when the SDK is not linked. An unmanaged app reports permissive values
20
+ /// rather than `false` — reporting `false` would hide controls nothing is restricting.
21
+ + (nullable NSDictionary<NSString *, id> *)snapshot;
22
+
23
+ @end
24
+
25
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright (c) 2026 react-native-intune contributors. MIT.
3
+ //
4
+
5
+ #import "RNIntunePolicy.h"
6
+ #import "RNIntuneCore.h"
7
+
8
+ #import <IntuneMAMSwift/IntuneMAMSwift.h>
9
+
10
+ /// Names the policy channel. An SDK update that adds a source must not produce a crash
11
+ /// or an empty string, so anything unrecognised keeps its number.
12
+ static NSString *RNIntunePolicySourceName(IntuneMAMPolicySource source)
13
+ {
14
+ switch (source) {
15
+ case IntuneMAMPolicySource_MDM:
16
+ return @"MDM";
17
+ case IntuneMAMPolicySource_MAM:
18
+ return @"MAM";
19
+ case IntuneMAMPolicySource_Other:
20
+ return @"Other";
21
+ }
22
+ return [NSString stringWithFormat:@"Unknown(%ld)", (long)source];
23
+ }
24
+
25
+ @implementation RNIntunePolicy
26
+
27
+ + (nullable NSDictionary<NSString *, id> *)snapshot
28
+ {
29
+ if (!RNIntuneCore.shared.sdkAvailable) {
30
+ return nil;
31
+ }
32
+
33
+ IntuneMAMPolicyManager *manager = IntuneMAMPolicyManager.instance;
34
+
35
+ // Ask for the *primary account's* policy, not `manager.policy`.
36
+ //
37
+ // `manager.policy` returns the policy for the current thread's identity, and no
38
+ // identity is set on the React Native JS thread — so the SDK answers with a default
39
+ // permissive policy object. It is not nil, which is what makes this so quiet: every
40
+ // field reads as "allowed" and the snapshot looks like a tenant with no restrictions.
41
+ // Found on device: an App Protection Policy with `Screen capture: Block` and Edge
42
+ // required still reported screenshots allowed and no managed browser.
43
+ //
44
+ // `primaryAccountId` is the single-identity accessor — the header says it is for
45
+ // applications that do not support multiple managed accounts, which is us by decision
46
+ // (SPEC §9). This is a read; it is not `setCurrentThreadAccountId`, and CLAUDE.md
47
+ // rule 10 still holds.
48
+ NSString *primaryAccountId = manager.primaryAccountId;
49
+ BOOL haveIdentity = primaryAccountId.length > 0;
50
+
51
+ id<IntuneMAMPolicy> policy = haveIdentity
52
+ ? [manager policyForAccountId:primaryAccountId]
53
+ : manager.policy;
54
+ BOOL managed = haveIdentity ? [manager isAccountIdManaged:primaryAccountId]
55
+ : manager.isManagementEnabled;
56
+
57
+ if (policy == nil) {
58
+ // Unmanaged is not an error: an app with no policy is fully permissive, and
59
+ // reporting `false` everywhere would hide functionality nothing is restricting.
60
+ return @{
61
+ @"isManaged" : @NO,
62
+ @"canSaveToLocal" : @YES,
63
+ @"canSaveToPersonal" : @YES,
64
+ @"canOpenFromUnmanaged" : @YES,
65
+ @"screenshotAllowed" : @YES,
66
+ @"raw" : @{
67
+ @"hasPrimaryAccount" : haveIdentity ? @"true" : @"false",
68
+ @"policySource" : RNIntunePolicySourceName(manager.mamPolicySource),
69
+ },
70
+ };
71
+ }
72
+
73
+ // `withAccountId:nil` means the current identity. Single identity is decided, so there
74
+ // is never another one to ask about (SPEC §9, CLAUDE.md rule 10).
75
+ BOOL saveLocal = [policy isSaveToAllowedForLocation:IntuneMAMSaveLocationLocalDrive
76
+ withAccountId:nil];
77
+ BOOL savePersonal = [policy isSaveToAllowedForLocation:IntuneMAMSaveLocationOther
78
+ withAccountId:nil];
79
+ BOOL openUnmanaged = [policy isOpenFromAllowedForLocation:IntuneMAMOpenLocationOther
80
+ withAccountId:nil];
81
+
82
+ // Everything else the SDK reports, as strings. Explicitly outside semver: anything
83
+ // depended on here has to be promoted to a typed field first (SPEC §4.3).
84
+ NSDictionary<NSString *, NSString *> *raw = @{
85
+ @"isPINRequired" : policy.isPINRequired ? @"true" : @"false",
86
+ @"isManagedBrowserRequired" : policy.isManagedBrowserRequired ? @"true" : @"false",
87
+ @"isContactSyncAllowed" : policy.isContactSyncAllowed ? @"true" : @"false",
88
+ @"isSpotlightIndexingAllowed" : policy.isSpotlightIndexingAllowed ? @"true" : @"false",
89
+ @"areSiriIntentsAllowed" : policy.areSiriIntentsAllowed ? @"true" : @"false",
90
+ @"isAppSharingAllowed" : policy.isAppSharingAllowed ? @"true" : @"false",
91
+ @"isFileEncryptionRequired" : policy.isFileEncryptionRequired ? @"true" : @"false",
92
+ @"notificationPolicy" :
93
+ [NSString stringWithFormat:@"%ld", (long)policy.notificationPolicy],
94
+ // Diagnostic, not policy. An all-permissive snapshot means one of two things, and
95
+ // these two values say which: no identity resolved, or a policy that came from
96
+ // somewhere other than the MAM channel. Boolean only — the account ID itself must
97
+ // not travel here (CLAUDE.md rule 3).
98
+ @"hasPrimaryAccount" : haveIdentity ? @"true" : @"false",
99
+ @"policySource" : RNIntunePolicySourceName(manager.mamPolicySource),
100
+ };
101
+
102
+ return @{
103
+ @"isManaged" : @(managed),
104
+ @"canSaveToLocal" : @(saveLocal),
105
+ @"canSaveToPersonal" : @(savePersonal),
106
+ @"canOpenFromUnmanaged" : @(openUnmanaged),
107
+ @"screenshotAllowed" : @(policy.isScreenCaptureAllowed),
108
+ @"raw" : raw,
109
+ };
110
+ }
111
+
112
+ @end
@@ -0,0 +1,34 @@
1
+ //
2
+ // The reset sequence (SPEC §7).
3
+ //
4
+ // Separate from RNIntuneResetJournal on purpose: the journal is storage, this is the
5
+ // order of operations. The order is the part that is easy to get wrong — the unregister
6
+ // has to precede any token purge, and the process is not guaranteed to survive it.
7
+ //
8
+
9
+ #import <Foundation/Foundation.h>
10
+
11
+ @class RNIntuneAuth;
12
+
13
+ NS_ASSUME_NONNULL_BEGIN
14
+
15
+ @interface RNIntuneReset : NSObject
16
+
17
+ /**
18
+ * Opens the journal, then unregisters and unenrolls every registered account.
19
+ *
20
+ * **Blocks** while the SDK acquires the Intune AAD token, so it must not run on the main
21
+ * thread. The caller is responsible for that.
22
+ */
23
+ + (void)runWithWipe:(BOOL)wipe
24
+ reason:(NSString *)reason
25
+ tenantId:(nullable NSString *)tenantId
26
+ auth:(nullable RNIntuneAuth *)auth;
27
+
28
+ /// Verifies the account is really gone and closes the journal. NO leaves it open, so the
29
+ /// next launch retries rather than declaring a reset that did not happen.
30
+ + (BOOL)complete;
31
+
32
+ @end
33
+
34
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,93 @@
1
+ //
2
+ // Copyright (c) 2026 react-native-intune contributors. MIT.
3
+ //
4
+
5
+ #import "RNIntuneReset.h"
6
+ #import "RNIntuneCore.h"
7
+ #import "RNIntuneResetJournal.h"
8
+
9
+ #import <IntuneMAMSwift/IntuneMAMSwift.h>
10
+
11
+ @implementation RNIntuneReset
12
+
13
+ + (void)runWithWipe:(BOOL)wipe
14
+ reason:(NSString *)reason
15
+ tenantId:(NSString *)tenantId
16
+ auth:(RNIntuneAuth *)auth
17
+ {
18
+ IntuneMAMEnrollmentManager *manager =
19
+ RNIntuneCore.shared.sdkAvailable ? IntuneMAMEnrollmentManager.instance : nil;
20
+
21
+ // Every *registered* account, not just the enrolled one. An account whose enrollment
22
+ // failed is still registered, and the SDK keeps retrying it on a 24-hour schedule
23
+ // until it is unregistered — so leaving those behind is precisely the "incomplete
24
+ // reset is actively harmful" case in SPEC §7. Found by a runtime test that left a
25
+ // failed registration behind and then could not close the journal.
26
+ NSArray<NSString *> *accountIds = [manager.registeredAccountIds copy] ?: @[];
27
+ NSString *enrolled = manager.enrolledAccountId;
28
+
29
+ // Written first, and forced to disk, because the next call may end the process.
30
+ [RNIntuneResetJournal.shared openWithAccountId:enrolled ?: accountIds.firstObject
31
+ tenantId:tenantId
32
+ wipe:wipe
33
+ reason:reason];
34
+
35
+ for (NSString *accountId in accountIds) {
36
+ if (accountId.length == 0) {
37
+ continue;
38
+ }
39
+ // Blocks while acquiring the Intune AAD token (SPEC §5.3). The caller runs this off
40
+ // the main thread.
41
+ [manager deRegisterAndUnenrollAccountId:accountId withWipe:wipe];
42
+ }
43
+
44
+ // Reached only if the process survived. Clearing the overrides is what makes a tenant
45
+ // switch possible afterwards — they persist across restarts otherwise (SPEC §5.2).
46
+ [RNIntuneResetJournal.shared advanceToStage:RNIntuneResetStageCleaningAuth];
47
+ IntuneMAMSettings.aadClientIdOverride = nil;
48
+ IntuneMAMSettings.aadAuthorityUriOverride = nil;
49
+ IntuneMAMSettings.aadRedirectUriOverride = nil;
50
+
51
+ // MSAL cache cleanup, and it is not cosmetic: issue #539 reports
52
+ // `deRegisterAndUnenrollAccountId` leaving keychain state behind, and #464 a fresh
53
+ // install where the SDK reports no enrolled account yet the sign-in webview autofills a
54
+ // previously enrolled address and then fails on account mismatch. Both are residual
55
+ // MSAL state rather than MAM state.
56
+ //
57
+ // After the unregister, deliberately: that call blocks while it acquires the Intune AAD
58
+ // token, and the token comes from this very cache. Clearing it first would make the
59
+ // unregister fail and strand a registered account (SPEC §5.3).
60
+ //
61
+ // Every account rather than one, for the same reason the loop above unregisters them
62
+ // all — by this point there is no single account id left to be selective with.
63
+ //
64
+ // nil in `external` mode, where the cache is the host app's (SPEC §3.2).
65
+ [auth removeAllAccounts];
66
+ [auth invalidate];
67
+
68
+ [RNIntuneResetJournal.shared advanceToStage:RNIntuneResetStageCleaningLocal];
69
+ }
70
+
71
+ + (BOOL)complete
72
+ {
73
+ if (RNIntuneResetJournal.shared.entry == nil) {
74
+ return YES;
75
+ }
76
+
77
+ // Verify rather than assume (SPEC §7 step 5). A reset that did not take must stay open
78
+ // and be retried, because the SDK resumes enrollment retries on its own schedule and a
79
+ // half-finished reset is worse than none.
80
+ if (RNIntuneCore.shared.sdkAvailable) {
81
+ IntuneMAMEnrollmentManager *manager = IntuneMAMEnrollmentManager.instance;
82
+ NSString *stillEnrolled = manager.enrolledAccountId;
83
+ NSArray *stillRegistered = manager.registeredAccountIds;
84
+ if (stillEnrolled.length > 0 || stillRegistered.count > 0) {
85
+ return NO;
86
+ }
87
+ }
88
+
89
+ [RNIntuneResetJournal.shared close];
90
+ return YES;
91
+ }
92
+
93
+ @end
@@ -0,0 +1,69 @@
1
+ //
2
+ // The reset journal (SPEC §7).
3
+ //
4
+ // Unregistering an account that had policy enforced makes the SDK wipe that account's
5
+ // data, and **the process terminating is expected behaviour, not a crash**. Code after
6
+ // the unregister call does not run. So the sequence is not held in memory: each stage is
7
+ // written down before it is attempted, and the next launch reads what was in flight.
8
+ //
9
+ // IDLE -> UNREGISTERING -> CLEANING_AUTH -> CLEANING_LOCAL -> IDLE
10
+ //
11
+ // Pure Objective-C, no SDK dependency: the journal has to be readable at launch before
12
+ // anything else is configured.
13
+ //
14
+
15
+ #import <Foundation/Foundation.h>
16
+
17
+ NS_ASSUME_NONNULL_BEGIN
18
+
19
+ /// Stage strings. Must match `ResetStage` in src/types.ts.
20
+ extern NSString *const RNIntuneResetStageUnregistering;
21
+ extern NSString *const RNIntuneResetStageCleaningAuth;
22
+ extern NSString *const RNIntuneResetStageCleaningLocal;
23
+
24
+ @interface RNIntuneResetJournal : NSObject
25
+
26
+ + (instancetype)shared;
27
+
28
+ /// The open entry, or nil when no reset is in flight. Keys: stage, accountId, tenantId,
29
+ /// wipe, reason, startedAt.
30
+ @property (nonatomic, readonly, nullable) NSDictionary<NSString *, id> *entry;
31
+
32
+ /// Current stage, or nil. This is what `getState().pendingReset` reports.
33
+ @property (nonatomic, readonly, nullable) NSString *stage;
34
+
35
+ /// Why the open reset started, or nil. Reported as `getState().pendingResetReason` so a
36
+ /// resume can pass the original reason on rather than flattening it to `'resume'`.
37
+ @property (nonatomic, readonly, nullable) NSString *reason;
38
+
39
+ /// Writes the entry at `UNREGISTERING`. Must return before the unregister call is made —
40
+ /// after it, there may be no process left to write anything.
41
+ - (void)openWithAccountId:(nullable NSString *)accountId
42
+ tenantId:(nullable NSString *)tenantId
43
+ wipe:(BOOL)wipe
44
+ reason:(NSString *)reason;
45
+
46
+ /**
47
+ * Opens the journal for a wipe the *service* started, unless one is already open.
48
+ *
49
+ * A service-initiated wipe is one of the callers of the reset path (SPEC §7, "one code
50
+ * path, many callers"), and it needs the journal for the same reason `reset()` does: the
51
+ * SDK terminates the process, so the consumer's cleanup may not finish and nothing else
52
+ * will come back for it. Without an entry there is no `pendingReset`, so the next launch
53
+ * has no idea anything happened.
54
+ *
55
+ * Returns NO when an entry already exists. A reset already in flight must not be
56
+ * overwritten — its `reason` and `accountId` are what the resumed sequence acts on.
57
+ */
58
+ - (BOOL)openForServiceWipeWithAccountId:(nullable NSString *)accountId
59
+ tenantId:(nullable NSString *)tenantId;
60
+
61
+ - (void)advanceToStage:(NSString *)stage;
62
+
63
+ /// Only after the account is verified gone. An unverified reset stays open and is retried
64
+ /// on the next launch rather than being marked done (SPEC §7 step 5).
65
+ - (void)close;
66
+
67
+ @end
68
+
69
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,106 @@
1
+ //
2
+ // Copyright (c) 2026 react-native-intune contributors. MIT.
3
+ //
4
+
5
+ #import "RNIntuneResetJournal.h"
6
+
7
+ NSString *const RNIntuneResetStageUnregistering = @"unregistering";
8
+ NSString *const RNIntuneResetStageCleaningAuth = @"cleaningAuth";
9
+ NSString *const RNIntuneResetStageCleaningLocal = @"cleaningLocal";
10
+
11
+ /**
12
+ * NSUserDefaults rather than a file in the app container.
13
+ *
14
+ * SPEC §7 requires the journal to survive the wipe, and on iOS the SDK encrypts files in
15
+ * the container under policy. Defaults are written by the app's own process and are not
16
+ * part of the SDK's managed-file surface, so they survive both the wipe and the process
17
+ * death that follows it — which is the whole point.
18
+ *
19
+ * [verify] against a real wipe once a tenant exists. Process-death survival is exercised
20
+ * and works; wipe survival cannot be tested without a policy-targeted account, and it is
21
+ * the half that matters most.
22
+ */
23
+ static NSString *const RNIntuneJournalKey = @"com.reactnativeintune.resetJournal";
24
+
25
+ @implementation RNIntuneResetJournal
26
+
27
+ + (instancetype)shared
28
+ {
29
+ static RNIntuneResetJournal *shared = nil;
30
+ static dispatch_once_t onceToken;
31
+ dispatch_once(&onceToken, ^{
32
+ shared = [[self alloc] init];
33
+ });
34
+ return shared;
35
+ }
36
+
37
+ - (nullable NSDictionary<NSString *, id> *)entry
38
+ {
39
+ id stored = [NSUserDefaults.standardUserDefaults objectForKey:RNIntuneJournalKey];
40
+ return [stored isKindOfClass:NSDictionary.class] ? stored : nil;
41
+ }
42
+
43
+ - (nullable NSString *)stage
44
+ {
45
+ id stage = self.entry[@"stage"];
46
+ return [stage isKindOfClass:NSString.class] ? stage : nil;
47
+ }
48
+
49
+ - (nullable NSString *)reason
50
+ {
51
+ id reason = self.entry[@"reason"];
52
+ // An empty string is not a reason; treat it as absent so callers get one nil to check.
53
+ return [reason isKindOfClass:NSString.class] && [reason length] > 0 ? reason : nil;
54
+ }
55
+
56
+ - (void)openWithAccountId:(nullable NSString *)accountId
57
+ tenantId:(nullable NSString *)tenantId
58
+ wipe:(BOOL)wipe
59
+ reason:(NSString *)reason
60
+ {
61
+ NSDictionary *entry = @{
62
+ @"stage" : RNIntuneResetStageUnregistering,
63
+ @"accountId" : accountId ?: @"",
64
+ @"tenantId" : tenantId ?: @"",
65
+ @"wipe" : @(wipe),
66
+ @"reason" : reason ?: @"",
67
+ @"startedAt" : @([NSDate.date timeIntervalSince1970]),
68
+ };
69
+ [NSUserDefaults.standardUserDefaults setObject:entry forKey:RNIntuneJournalKey];
70
+ // Forced to disk before returning: the caller is about to make a call that may end the
71
+ // process, and an entry still sitting in memory is an entry that never existed.
72
+ [NSUserDefaults.standardUserDefaults synchronize];
73
+ }
74
+
75
+ - (BOOL)openForServiceWipeWithAccountId:(NSString *)accountId
76
+ tenantId:(NSString *)tenantId
77
+ {
78
+ if (self.entry != nil) {
79
+ return NO;
80
+ }
81
+ // `wipe: YES` because the SDK is already wiping; the flag records what happened rather
82
+ // than requesting it. `reason` is what lets the consumer's handler tell an
83
+ // administrator revoking access from a user logging out (SPEC §7.4).
84
+ [self openWithAccountId:accountId tenantId:tenantId wipe:YES reason:@"remote_wipe"];
85
+ return YES;
86
+ }
87
+
88
+ - (void)advanceToStage:(NSString *)stage
89
+ {
90
+ NSDictionary *current = self.entry;
91
+ if (current == nil) {
92
+ return;
93
+ }
94
+ NSMutableDictionary *next = [current mutableCopy];
95
+ next[@"stage"] = stage;
96
+ [NSUserDefaults.standardUserDefaults setObject:next forKey:RNIntuneJournalKey];
97
+ [NSUserDefaults.standardUserDefaults synchronize];
98
+ }
99
+
100
+ - (void)close
101
+ {
102
+ [NSUserDefaults.standardUserDefaults removeObjectForKey:RNIntuneJournalKey];
103
+ [NSUserDefaults.standardUserDefaults synchronize];
104
+ }
105
+
106
+ @end
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Codegen spec — the ONLY file Codegen reads, and the only `Native*.ts` allowed in
5
+ * `jsSrcsDir`. An extra one is picked up and fails confusingly.
6
+ *
7
+ * This file is deliberately loose (CLAUDE.md rule 11). Codegen accepts only
8
+ * `boolean | number | string | Object | Array | Promise | void`: no literal unions, no
9
+ * optional parameters, no enums. So every union and every optional is normalized in
10
+ * `index.ts` before it crosses the bridge, and every aggregate return is `Object` here
11
+ * and a real type there. Nothing in the public API is `Object`.
12
+ *
13
+ * Editing this file means Codegen must re-run — `pod install` on iOS,
14
+ * `generateCodegenArtifactsFromSchema` on Android. A native build that breaks right
15
+ * after a change here is a stale Codegen artifact until proven otherwise.
16
+ */
17
+
18
+ import { TurboModuleRegistry } from 'react-native';
19
+ export default TurboModuleRegistry.getEnforcing('RNIntune');
20
+ //# sourceMappingURL=NativeIntune.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeIntune.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,mBAAmB,QAA0B,cAAc;AAkHpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,UAAU,CAAC","ignoreList":[]}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Sign-in, for `authMode: 'builtin'` — the module owns MSAL (SPEC §3).
5
+ *
6
+ * Two tokens exist and only one of them appears here: the MAM service token is acquired
7
+ * internally and never crosses the bridge. Everything returned by these calls is for
8
+ * *your* API's scopes (SPEC §3.5).
9
+ */
10
+
11
+ import NativeIntune from "../NativeIntune.js";
12
+ import { assertAccountId } from "../internal/errors.js";
13
+ import { toAuthAccount, toAuthResult } from "../internal/decode.js";
14
+ import { enroll } from "./enrollment.js";
15
+ /**
16
+ * Uses the broker when one is present, which also gives device-wide SSO: a user already
17
+ * signed into Outlook usually gets a token with no prompt at all.
18
+ */
19
+ export async function signIn(params = {}) {
20
+ return toAuthResult(await NativeIntune.signIn(params));
21
+ }
22
+
23
+ /**
24
+ * Cache-first. Rejects with `E_INTERACTION_REQUIRED` when a prompt is needed — the normal
25
+ * pattern is `signInSilent` first, `signIn` on that rejection.
26
+ */
27
+ export async function signInSilent(params = {}) {
28
+ return toAuthResult(await NativeIntune.signInSilent(params));
29
+ }
30
+
31
+ /** For your own API's scopes, after sign-in. */
32
+ export async function acquireToken(params) {
33
+ return toAuthResult(await NativeIntune.acquireToken(params));
34
+ }
35
+
36
+ /**
37
+ * Accounts with cached refresh tokens. Under single identity (SPEC §9) this holds at
38
+ * most one; more than one means state a reset should have cleared.
39
+ */
40
+ export async function getAccounts() {
41
+ const accounts = await NativeIntune.getAccounts();
42
+ return accounts.map(toAuthAccount);
43
+ }
44
+
45
+ /**
46
+ * Removes the account from the MSAL cache. With `wipeIntune: true` it also runs the full
47
+ * reset sequence, which is what you want on logout — and which means **the process may
48
+ * terminate during this call on Android.**
49
+ */
50
+ export function signOut(params) {
51
+ assertAccountId(params.accountId);
52
+ return NativeIntune.signOut(params);
53
+ }
54
+
55
+ /** The common path in one call. Use the primitives when you need a step in between. */
56
+ export async function signInAndEnroll(params = {}) {
57
+ const auth = await signIn(params);
58
+ const enrollment = await enroll({
59
+ accountId: auth.accountId
60
+ });
61
+ return {
62
+ auth,
63
+ enrollment
64
+ };
65
+ }
66
+ //# sourceMappingURL=auth.js.map