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,752 @@
|
|
|
1
|
+
package com.reactnativeintune
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReadableMap
|
|
7
|
+
import com.facebook.react.bridge.WritableMap
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.net.Uri
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import android.util.Log
|
|
13
|
+
import com.microsoft.intune.mam.client.app.MAMComponents
|
|
14
|
+
import com.microsoft.intune.mam.client.strict.MAMStrictMode
|
|
15
|
+
import com.microsoft.intune.mam.policy.MAMEnrollmentManager
|
|
16
|
+
import com.microsoft.intune.mam.policy.MAMUserInfo
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* TurboModule implementation. `NativeIntuneSpec` is generated by Codegen from
|
|
20
|
+
* `src/NativeIntune.ts` into this same package (`codegenConfig.android.javaPackageName`),
|
|
21
|
+
* so there is nothing to import.
|
|
22
|
+
*
|
|
23
|
+
* Every spec parameter arrives as a [ReadableMap] because Codegen maps inline object
|
|
24
|
+
* types that way on Android — unlike iOS, where they become typed C++ structs. Read keys
|
|
25
|
+
* through the constants in [Keys] rather than inline strings: a typo in a key is silent.
|
|
26
|
+
*/
|
|
27
|
+
class RNIntuneModule(private val reactContext: ReactApplicationContext) :
|
|
28
|
+
NativeIntuneSpec(reactContext) {
|
|
29
|
+
|
|
30
|
+
private val events = RNIntuneEventQueue(reactContext)
|
|
31
|
+
|
|
32
|
+
private var config: RNIntuneConfig? = null
|
|
33
|
+
|
|
34
|
+
/** MSAL. Built at configure time in `builtin` mode only (SPEC §3.2). */
|
|
35
|
+
private val auth = RNIntuneAuth(reactContext)
|
|
36
|
+
|
|
37
|
+
/** Held so they can be unregistered; also proves the plugin processed the app. */
|
|
38
|
+
private var registrations: List<RNIntuneNotifications.Registration> = emptyList()
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The account this process registered. Android cannot enumerate registered accounts,
|
|
42
|
+
* so there is nothing to read it back from after a restart.
|
|
43
|
+
*/
|
|
44
|
+
private var registeredAccountId: String? = null
|
|
45
|
+
|
|
46
|
+
/** The UPN the account was registered with. `unregisterAccountForMAM` needs it too. */
|
|
47
|
+
private var registeredUpn: String? = null
|
|
48
|
+
|
|
49
|
+
/** accountId -> completion, settled from the notification receiver. */
|
|
50
|
+
private val pendingEnrollments = mutableMapOf<String, (WritableMap) -> Unit>()
|
|
51
|
+
|
|
52
|
+
private val timeoutHandler = Handler(Looper.getMainLooper())
|
|
53
|
+
|
|
54
|
+
// ------------------------------------------------------------------ lifecycle
|
|
55
|
+
|
|
56
|
+
init {
|
|
57
|
+
// The callback object outlives any single React context — it is registered from
|
|
58
|
+
// Application.onMAMCreate — so it is pointed at whichever module instance is live.
|
|
59
|
+
RNIntuneAuthCallback.attach(this)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun invalidate() {
|
|
63
|
+
RNIntuneAuthCallback.detach(this)
|
|
64
|
+
timeoutHandler.removeCallbacksAndMessages(null)
|
|
65
|
+
RNIntuneNotifications.unregister(registrations)
|
|
66
|
+
registrations = emptyList()
|
|
67
|
+
super.invalidate()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override fun configure(config: ReadableMap, promise: Promise) {
|
|
71
|
+
if (!sdkAvailable) {
|
|
72
|
+
promise.reject(
|
|
73
|
+
ERR_SDK_UNAVAILABLE,
|
|
74
|
+
"The Intune MAM SDK is not on the classpath. Check that vendor/android was " +
|
|
75
|
+
"fetched and that the MAM Gradle plugin is applied in the app module.",
|
|
76
|
+
)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
val incoming = RNIntuneConfig.from(config)
|
|
81
|
+
|
|
82
|
+
// Switching tenants requires an explicit reset: reconfiguring in place would leave
|
|
83
|
+
// the previous tenant registered (SPEC §13.1).
|
|
84
|
+
val active = this.config
|
|
85
|
+
if (active != null && active.tenantId != incoming.tenantId) {
|
|
86
|
+
promise.reject(
|
|
87
|
+
ERR_RESET_REQUIRED,
|
|
88
|
+
"configure() was called with a different tenantId than the active one. " +
|
|
89
|
+
"Call reset() before switching tenants.",
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// No branding here. The three iOS runtime colour properties have no Android
|
|
95
|
+
// counterpart — the SDK's own screens follow the app's theme instead — so the
|
|
96
|
+
// branding fields are silently unused on this platform rather than faked.
|
|
97
|
+
|
|
98
|
+
// Strict Mode throws on integration bugs that would otherwise silently leave
|
|
99
|
+
// corporate data unprotected. It is not meant for production, which is why the
|
|
100
|
+
// public default is __DEV__ and not `true` (SPEC §6.1).
|
|
101
|
+
if (incoming.strictMode) {
|
|
102
|
+
runCatching { MAMStrictMode.enable() }
|
|
103
|
+
.onFailure { Log.w(TAG, "MAM Strict Mode could not be enabled: ${it.message}") }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Registered once and kept for the process lifetime: the SDK notifies unprompted,
|
|
107
|
+
// including on its own enrollment retry schedule and for service-initiated wipes.
|
|
108
|
+
if (registrations.isEmpty()) {
|
|
109
|
+
registrations = RNIntuneNotifications.register(::emit)
|
|
110
|
+
if (registrations.isEmpty()) {
|
|
111
|
+
// MAMComponents returns null when the app was not processed by the MAM Gradle
|
|
112
|
+
// plugin. Everything would then appear to work while nothing was protected, so
|
|
113
|
+
// this is the one integration mistake worth refusing to start on.
|
|
114
|
+
promise.reject(
|
|
115
|
+
ERR_SDK_UNAVAILABLE,
|
|
116
|
+
"The MAM notification registry is unavailable. This app was almost certainly " +
|
|
117
|
+
"built without the MAM Gradle plugin applied to its app module — see the " +
|
|
118
|
+
"README integration checklist. Without it the SDK is present but inert.",
|
|
119
|
+
)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.config = incoming
|
|
125
|
+
|
|
126
|
+
// Unlike iOS there is no runtime override to set: these values are held here and
|
|
127
|
+
// passed per-call to registerAccountForMAM (SPEC §13.1). The MAM service auth
|
|
128
|
+
// callback is not registered here either — it has to be in place from
|
|
129
|
+
// Application.onMAMCreate, long before this method can run, so it is the consumer's
|
|
130
|
+
// wiring and we only supply the class (SPEC §6.1.2).
|
|
131
|
+
|
|
132
|
+
if (!incoming.builtinAuth) {
|
|
133
|
+
// `external`: the host app owns MSAL. Building a second client here would put two
|
|
134
|
+
// caches in one binary, which is the thing that mode exists to avoid (SPEC §3.2).
|
|
135
|
+
promise.resolve(null)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Off the main thread: creating the MSAL client writes the config file and does
|
|
140
|
+
// network setup, and the blocking factory is the one that reports failure usefully.
|
|
141
|
+
Thread {
|
|
142
|
+
try {
|
|
143
|
+
auth.configure(incoming.clientId, incoming.authority, incoming.redirectUri)
|
|
144
|
+
promise.resolve(null)
|
|
145
|
+
} catch (e: Throwable) {
|
|
146
|
+
// Loud here rather than at the first sign-in, where the cause is much harder to
|
|
147
|
+
// see. A redirect URI whose signature hash does not match the registration is
|
|
148
|
+
// the common one.
|
|
149
|
+
this.config = null
|
|
150
|
+
promise.reject(
|
|
151
|
+
ERR_NATIVE,
|
|
152
|
+
"MSAL could not be initialised: ${e.message ?: "unknown reason"}",
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
}.start()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override fun isSupported(promise: Promise) {
|
|
159
|
+
promise.resolve(sdkAvailable)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
override fun getBrokerStatus(promise: Promise) {
|
|
163
|
+
val companyPortal = isCompanyPortalInstalled()
|
|
164
|
+
|
|
165
|
+
promise.resolve(
|
|
166
|
+
Arguments.createMap().apply {
|
|
167
|
+
putBoolean("brokerAvailable", companyPortal)
|
|
168
|
+
putBoolean("companyPortalInstalled", companyPortal)
|
|
169
|
+
// Authenticator is not a broker for MAM on Android.
|
|
170
|
+
putBoolean("authenticatorInstalled", false)
|
|
171
|
+
// Company Portal is mandatory here — enrollment is impossible without it.
|
|
172
|
+
putBoolean("required", true)
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
override fun openBrokerInstall(promise: Promise) {
|
|
178
|
+
if (isPackageInstalled(COMPANY_PORTAL_PACKAGE)) {
|
|
179
|
+
promise.reject(ERR_NOT_NEEDED, "Company Portal is already installed.")
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// market:// with the verified package name, falling back to the web Play Store for
|
|
184
|
+
// devices without the Play app.
|
|
185
|
+
val intents =
|
|
186
|
+
listOf(
|
|
187
|
+
Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$COMPANY_PORTAL_PACKAGE")),
|
|
188
|
+
Intent(
|
|
189
|
+
Intent.ACTION_VIEW,
|
|
190
|
+
Uri.parse("https://play.google.com/store/apps/details?id=$COMPANY_PORTAL_PACKAGE"),
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
for (intent in intents) {
|
|
195
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
196
|
+
if (runCatching { reactContext.startActivity(intent) }.isSuccess) {
|
|
197
|
+
promise.resolve(null)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
promise.reject(ERR_NATIVE, "Could not open the Play Store for Company Portal.")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ------------------------------------------------------------------ auth
|
|
205
|
+
|
|
206
|
+
/** Returns false and rejects when the caller may not use the built-in MSAL. */
|
|
207
|
+
private fun authReady(method: String, promise: Promise): Boolean {
|
|
208
|
+
if (config == null) {
|
|
209
|
+
notConfigured(method, promise)
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
if (config?.builtinAuth != true) {
|
|
213
|
+
promise.reject(
|
|
214
|
+
ERR_EXTERNAL_AUTH_MODE,
|
|
215
|
+
"$method is unavailable in authMode 'external'. The host app owns MSAL there; " +
|
|
216
|
+
"supply the account id to enroll() and answer tokenRequest via " +
|
|
217
|
+
"setTokenProvider.",
|
|
218
|
+
)
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
return true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private fun scopesOf(params: ReadableMap): List<String> {
|
|
225
|
+
val array = runCatching { params.getArray("scopes") }.getOrNull() ?: return emptyList()
|
|
226
|
+
return (0 until array.size()).mapNotNull { array.getString(it) }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
override fun signIn(params: ReadableMap, promise: Promise) {
|
|
230
|
+
if (!authReady("signIn", promise)) return
|
|
231
|
+
val scopes = scopesOf(params)
|
|
232
|
+
val loginHint = params.getString("loginHint")
|
|
233
|
+
val prompt = params.getString("prompt")
|
|
234
|
+
|
|
235
|
+
// Off the JS thread: with an account already signed in this tries the cache first,
|
|
236
|
+
// and that call blocks. MSAL puts its own UI on the main thread itself.
|
|
237
|
+
Thread {
|
|
238
|
+
auth.signIn(
|
|
239
|
+
scopes = scopes,
|
|
240
|
+
loginHint = loginHint,
|
|
241
|
+
prompt = prompt,
|
|
242
|
+
onResult = { promise.resolve(it) },
|
|
243
|
+
onError = { code, message -> promise.reject(code, message) },
|
|
244
|
+
)
|
|
245
|
+
}.start()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override fun signInSilent(params: ReadableMap, promise: Promise) {
|
|
249
|
+
if (!authReady("signInSilent", promise)) return
|
|
250
|
+
Thread {
|
|
251
|
+
auth.acquireTokenSilent(
|
|
252
|
+
scopes = scopesOf(params),
|
|
253
|
+
forceRefresh = false,
|
|
254
|
+
onResult = { promise.resolve(it) },
|
|
255
|
+
onError = { code, message -> promise.reject(code, message) },
|
|
256
|
+
)
|
|
257
|
+
}.start()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
override fun acquireToken(params: ReadableMap, promise: Promise) {
|
|
261
|
+
if (!authReady("acquireToken", promise)) return
|
|
262
|
+
val forceRefresh =
|
|
263
|
+
runCatching { params.getBoolean("forceRefresh") }.getOrDefault(false)
|
|
264
|
+
Thread {
|
|
265
|
+
auth.acquireTokenSilent(
|
|
266
|
+
scopes = scopesOf(params),
|
|
267
|
+
forceRefresh = forceRefresh,
|
|
268
|
+
onResult = { promise.resolve(it) },
|
|
269
|
+
onError = { code, message -> promise.reject(code, message) },
|
|
270
|
+
)
|
|
271
|
+
}.start()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
override fun getAccounts(promise: Promise) {
|
|
275
|
+
if (!authReady("getAccounts", promise)) return
|
|
276
|
+
Thread {
|
|
277
|
+
val out = Arguments.createArray()
|
|
278
|
+
auth.accounts().forEach { out.pushMap(it) }
|
|
279
|
+
promise.resolve(out)
|
|
280
|
+
}.start()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
override fun signOut(params: ReadableMap, promise: Promise) {
|
|
284
|
+
if (!authReady("signOut", promise)) return
|
|
285
|
+
val wipeIntune = runCatching { params.getBoolean("wipeIntune") }.getOrDefault(false)
|
|
286
|
+
|
|
287
|
+
// Off the main thread: with a wipe this runs the whole reset sequence, whose
|
|
288
|
+
// unregister blocks (SPEC §5.3, §12.5).
|
|
289
|
+
Thread {
|
|
290
|
+
if (wipeIntune) {
|
|
291
|
+
// Order matters and is the reason this lives in one place: unregister first,
|
|
292
|
+
// then clear MSAL. Reversed, the unregister loses the token it needs.
|
|
293
|
+
doReset(wipe = true, reason = "logout")
|
|
294
|
+
} else {
|
|
295
|
+
auth.signOutQuietly()
|
|
296
|
+
}
|
|
297
|
+
promise.resolve(null)
|
|
298
|
+
}.start()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ------------------------------------------------------------------ enrollment
|
|
302
|
+
|
|
303
|
+
override fun enroll(params: ReadableMap, promise: Promise) {
|
|
304
|
+
val config = this.config
|
|
305
|
+
if (config == null) {
|
|
306
|
+
notConfigured("enroll", promise)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
val accountId = params.getString("accountId").orEmpty()
|
|
311
|
+
if (accountId.isEmpty()) {
|
|
312
|
+
promise.reject(ERR_INVALID_ACCOUNT_ID, "enroll() requires an Entra object ID.")
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// registerAccountForMAM(upn, aadId, tenantId, authority) — parameter order verified
|
|
317
|
+
// from bytecode, not documentation: the offline implementation forwards them to
|
|
318
|
+
// MAMIdentityManager.insertOrUpdate(aadId, upn, tenantId, authority), which in turn
|
|
319
|
+
// builds MAMIdentity(upn, aadId, authority, tenantId).
|
|
320
|
+
//
|
|
321
|
+
// The UPN is required and NonNull, and this module's public API deliberately takes
|
|
322
|
+
// only the Entra object ID (SPEC §13.2). Nothing can supply the UPN yet: in
|
|
323
|
+
// authMode 'builtin' it comes from the module's own MSAL sign-in, which is spike
|
|
324
|
+
// S-3. Rejecting here with a named code beats calling the SDK with a fabricated
|
|
325
|
+
// identity, which would register the wrong account.
|
|
326
|
+
val upn = params.getString("upn").orEmpty().ifEmpty { knownUpnFor(accountId) }
|
|
327
|
+
if (upn.isNullOrEmpty()) {
|
|
328
|
+
promise.reject(
|
|
329
|
+
ERR_UPN_REQUIRED,
|
|
330
|
+
"Android enrollment needs the account's UPN as well as its object ID — " +
|
|
331
|
+
"registerAccountForMAM takes both and will not accept null for the UPN. In " +
|
|
332
|
+
"authMode 'builtin' the module reads it from its own MSAL account (spike " +
|
|
333
|
+
"S-3, not yet landed); in 'external' pass it as enroll({ accountId, upn }). " +
|
|
334
|
+
"iOS is unaffected: registerAndEnrollAccountId takes the object ID alone and " +
|
|
335
|
+
"the SDK resolves the UPN itself.",
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
val manager = MAMComponents.get(MAMEnrollmentManager::class.java)
|
|
341
|
+
if (manager == null) {
|
|
342
|
+
promise.reject(
|
|
343
|
+
ERR_SDK_UNAVAILABLE,
|
|
344
|
+
"MAMEnrollmentManager is unavailable — the MAM Gradle plugin is almost " +
|
|
345
|
+
"certainly not applied to this app module.",
|
|
346
|
+
)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Already registered? Answer now.
|
|
351
|
+
//
|
|
352
|
+
// `registerAccountForMAM` for a known account is a no-op that fires **no**
|
|
353
|
+
// notification — the SDK logs "skipping already registered account" and returns. The
|
|
354
|
+
// promise below would then wait for a result that is never coming and settle as
|
|
355
|
+
// `pending` at the timeout, which is both slow and untrue. Observed on device.
|
|
356
|
+
val known = runCatching { manager.getRegisteredAccountStatus(upn, accountId) }.getOrNull()
|
|
357
|
+
if (known != null) {
|
|
358
|
+
registeredAccountId = accountId
|
|
359
|
+
registeredUpn = upn
|
|
360
|
+
promise.resolve(RNIntuneNotifications.resultFrom(known, accountId))
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Held against the account ID and settled from the notification receiver. The call
|
|
365
|
+
// below returns immediately and says nothing about the outcome (SPEC §12.5).
|
|
366
|
+
synchronized(pendingEnrollments) {
|
|
367
|
+
val existing = pendingEnrollments[accountId]
|
|
368
|
+
pendingEnrollments[accountId] = { result ->
|
|
369
|
+
existing?.invoke(result)
|
|
370
|
+
promise.resolve(result)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
scheduleEnrollTimeout(accountId)
|
|
375
|
+
|
|
376
|
+
registeredAccountId = accountId
|
|
377
|
+
// Kept because unregisterAccountForMAM needs the UPN too, and by then the process
|
|
378
|
+
// may have restarted — so it also goes into the journal (SPEC §7).
|
|
379
|
+
registeredUpn = upn
|
|
380
|
+
manager.registerAccountForMAM(upn, accountId, config.tenantId, config.authority)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The UPN for an object ID, from the module's own MSAL account cache.
|
|
385
|
+
*
|
|
386
|
+
* Always null today — it becomes the MSAL lookup in the auth slice (S-3). Keeping the
|
|
387
|
+
* seam here means `enroll` does not change shape when that lands, and it is the reason
|
|
388
|
+
* the `upn` parameter is documented as `external`-mode only rather than as the normal
|
|
389
|
+
* way to call this.
|
|
390
|
+
*/
|
|
391
|
+
/**
|
|
392
|
+
* The UPN for an account id, from the module's own MSAL sign-in.
|
|
393
|
+
*
|
|
394
|
+
* `registerAccountForMAM` needs both and will not take a null UPN, while the public API
|
|
395
|
+
* deliberately carries only the object ID (SPEC §6.3). In `external` mode there is no
|
|
396
|
+
* MSAL here to ask, and the caller passes `upn` explicitly.
|
|
397
|
+
*/
|
|
398
|
+
private fun knownUpnFor(accountId: String): String? =
|
|
399
|
+
if (config?.builtinAuth == true) auth.upnFor(accountId) else null
|
|
400
|
+
|
|
401
|
+
private fun scheduleEnrollTimeout(accountId: String) {
|
|
402
|
+
timeoutHandler.postDelayed({
|
|
403
|
+
// Truthful rather than convenient: the SDK has not failed, it has not answered.
|
|
404
|
+
// It keeps retrying on its own schedule and the result still arrives as an event.
|
|
405
|
+
settleEnrollment(
|
|
406
|
+
accountId,
|
|
407
|
+
Arguments.createMap().apply {
|
|
408
|
+
putString("status", "pending")
|
|
409
|
+
putString("accountId", accountId)
|
|
410
|
+
putString("nativeCode", "RNIntuneTimeout")
|
|
411
|
+
putString(
|
|
412
|
+
"nativeMessage",
|
|
413
|
+
"The SDK did not report a result within the timeout. Enrollment continues " +
|
|
414
|
+
"in the background; watch onEnrollmentResult.",
|
|
415
|
+
)
|
|
416
|
+
putBoolean("restartRequired", false)
|
|
417
|
+
},
|
|
418
|
+
)
|
|
419
|
+
}, ENROLL_TIMEOUT_MS)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private fun settleEnrollment(accountId: String, result: WritableMap) {
|
|
423
|
+
val completion = synchronized(pendingEnrollments) { pendingEnrollments.remove(accountId) }
|
|
424
|
+
completion?.invoke(result)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* iOS-only. `loginAndEnrollAccount:` has no Android counterpart —
|
|
429
|
+
* `registerAccountForMAM` always needs a token supplied by the app, so there is
|
|
430
|
+
* nothing here that can run a sign-in on the SDK's behalf.
|
|
431
|
+
*/
|
|
432
|
+
override fun enrollInteractive(params: ReadableMap, promise: Promise) {
|
|
433
|
+
promise.reject(
|
|
434
|
+
ERR_SDK_UNAVAILABLE,
|
|
435
|
+
"enrollInteractive() is iOS-only. On Android the SDK cannot run the sign-in " +
|
|
436
|
+
"itself: registerAccountForMAM needs a token from the app, so use enroll() with " +
|
|
437
|
+
"a token provider.",
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
override fun getState(promise: Promise) {
|
|
442
|
+
// Deliberately never rejects: reporting `configured: false` is what makes this the
|
|
443
|
+
// reconciliation primitive (SPEC §13.2).
|
|
444
|
+
//
|
|
445
|
+
// Android has no "list registered accounts" API — getRegisteredAccountStatus takes
|
|
446
|
+
// an account ID and answers for that one. So the registered set is whatever this
|
|
447
|
+
// process registered, and after a restart it is empty until the app reconciles.
|
|
448
|
+
// That asymmetry with iOS (which has registeredAccountIds) is why the host app
|
|
449
|
+
// compares against its own backend rather than trusting this list alone.
|
|
450
|
+
val registered = registeredAccountId
|
|
451
|
+
val status = registered?.let { readStatus(it) }
|
|
452
|
+
|
|
453
|
+
promise.resolve(
|
|
454
|
+
Arguments.createMap().apply {
|
|
455
|
+
putBoolean("configured", config != null)
|
|
456
|
+
putString("configuredTenantId", config?.tenantId)
|
|
457
|
+
putArray(
|
|
458
|
+
"registeredAccountIds",
|
|
459
|
+
Arguments.createArray().apply { registered?.let { pushString(it) } },
|
|
460
|
+
)
|
|
461
|
+
putString(
|
|
462
|
+
"enrolledAccountId",
|
|
463
|
+
if (RNIntuneNotifications.unifiedStatus(status) == "succeeded") registered else null,
|
|
464
|
+
)
|
|
465
|
+
putString("status", status?.let { RNIntuneNotifications.unifiedStatus(it) })
|
|
466
|
+
putString("pendingReset", RNIntuneResetJournal.stage(reactContext))
|
|
467
|
+
putString("pendingResetReason", RNIntuneResetJournal.read(reactContext)?.reason)
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* **The process is expected to terminate during this call** (SPEC §7). Everything
|
|
474
|
+
* after `unregisterAccountForMAM` is best-effort; the journal is what carries the
|
|
475
|
+
* sequence across the death.
|
|
476
|
+
*/
|
|
477
|
+
override fun reset(params: ReadableMap, promise: Promise) {
|
|
478
|
+
doReset(params.getBoolean("wipe"), params.getString("reason").orEmpty())
|
|
479
|
+
promise.resolve(null)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Shared by `reset` and `signOut({ wipeIntune: true })`, so the order cannot diverge. */
|
|
483
|
+
private fun doReset(wipe: Boolean, reason: String) {
|
|
484
|
+
RNIntuneReset.run(
|
|
485
|
+
context = reactContext,
|
|
486
|
+
accountId = registeredAccountId,
|
|
487
|
+
upn = registeredUpn,
|
|
488
|
+
tenantId = config?.tenantId,
|
|
489
|
+
wipe = wipe,
|
|
490
|
+
reason = reason,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
// MSAL cache cleanup, after the unregister above and never before it: that call needs
|
|
494
|
+
// an Intune token which comes from this cache, so clearing it first would strand a
|
|
495
|
+
// registered account (SPEC §7 step 4). Skipped in `external`, where the cache is the
|
|
496
|
+
// host app's.
|
|
497
|
+
if (config?.builtinAuth == true) {
|
|
498
|
+
auth.signOutQuietly()
|
|
499
|
+
auth.invalidate()
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
config = null
|
|
503
|
+
registeredAccountId = null
|
|
504
|
+
registeredUpn = null
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
override fun completeReset(promise: Promise) {
|
|
508
|
+
// Verify rather than assume (SPEC §7 step 5). A reset that did not take stays open
|
|
509
|
+
// and is retried: the SDK resumes enrollment retries on its own schedule, and a
|
|
510
|
+
// half-finished reset is worse than none.
|
|
511
|
+
if (RNIntuneReset.complete(reactContext, ::readStatus)) {
|
|
512
|
+
promise.resolve(null)
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
promise.reject(
|
|
516
|
+
ERR_RESET_IN_PROGRESS,
|
|
517
|
+
"The account is still registered after reset, so the journal was left open. " +
|
|
518
|
+
"It will be retried on the next launch.",
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ------------------------------------------------------------------ policy
|
|
523
|
+
|
|
524
|
+
override fun getPolicy(promise: Promise) {
|
|
525
|
+
if (!sdkAvailable) {
|
|
526
|
+
promise.reject(ERR_SDK_UNAVAILABLE, "The Intune MAM SDK is not on the classpath.")
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
promise.resolve(RNIntunePolicy.snapshot(reactContext, registeredAccountId))
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
override fun getDiagnostics(promise: Promise) {
|
|
533
|
+
// Account IDs may appear here but must never be logged or persisted (CLAUDE.md
|
|
534
|
+
// rule 3). Nothing below is a token or a UPN.
|
|
535
|
+
promise.resolve(
|
|
536
|
+
Arguments.createMap().apply {
|
|
537
|
+
putString("platform", "android")
|
|
538
|
+
putString("sdkLinked", sdkAvailable.toString())
|
|
539
|
+
putString("configured", (config != null).toString())
|
|
540
|
+
putString("configuredTenantId", config?.tenantId.orEmpty())
|
|
541
|
+
putString("authMode", config?.authMode.orEmpty())
|
|
542
|
+
putString("companyPortalInstalled", isPackageInstalled(COMPANY_PORTAL_PACKAGE).toString())
|
|
543
|
+
putString("strictMode", (config?.strictMode ?: false).toString())
|
|
544
|
+
putString("notificationReceiversRegistered", registrations.size.toString())
|
|
545
|
+
if (sdkAvailable) {
|
|
546
|
+
// MAMComponents answers these without an enrolled account, and neither is a
|
|
547
|
+
// token, a UPN or an account ID (CLAUDE.md rule 3).
|
|
548
|
+
putString(
|
|
549
|
+
"managedApp",
|
|
550
|
+
runCatching { MAMComponents.isManagedApp(reactContext).toString() }
|
|
551
|
+
.getOrDefault("unknown"),
|
|
552
|
+
)
|
|
553
|
+
putString(
|
|
554
|
+
"wipeInProgress",
|
|
555
|
+
runCatching { MAMComponents.isWipeInProgress(reactContext).toString() }
|
|
556
|
+
.getOrDefault("unknown"),
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ------------------------------------------------------------------ token provider
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Not public API — `setTokenProvider` in `index.ts` wraps these. The MAM service token
|
|
567
|
+
* never crosses back out to JS and is never logged (CLAUDE.md rule 9).
|
|
568
|
+
*
|
|
569
|
+
* Wiring lands with the auth callback (SPEC §13.4): the SDK asks on a background
|
|
570
|
+
* thread, including on its own retry schedule with no JS call in flight, so the
|
|
571
|
+
* request has to be correlated by `requestId` rather than awaited inline.
|
|
572
|
+
*/
|
|
573
|
+
override fun resolveToken(params: ReadableMap) {
|
|
574
|
+
val requestId = params.getString("requestId") ?: return
|
|
575
|
+
RNIntuneAuthCallback.resolve(requestId, params.getString("token").orEmpty())
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
override fun rejectToken(params: ReadableMap) {
|
|
579
|
+
val requestId = params.getString("requestId") ?: return
|
|
580
|
+
RNIntuneAuthCallback.reject(requestId, params.getString("reason").orEmpty())
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ------------------------------------------------------------------ events
|
|
584
|
+
|
|
585
|
+
override fun addListener(eventName: String) = events.addListener()
|
|
586
|
+
|
|
587
|
+
override fun removeListeners(count: Double) = events.removeListeners(count.toInt())
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* A MAM service token from the module's own MSAL, or null when this is not our job.
|
|
591
|
+
*
|
|
592
|
+
* Null in `external` mode, where the host app owns MSAL and answers the JS
|
|
593
|
+
* `tokenRequest` instead. Called on the SDK's background thread and blocks it.
|
|
594
|
+
*/
|
|
595
|
+
internal fun mamServiceToken(resourceId: String): String? =
|
|
596
|
+
if (config?.builtinAuth == true) auth.mamServiceToken(resourceId) else null
|
|
597
|
+
|
|
598
|
+
/** Called by the auth callback from the SDK's own background thread. */
|
|
599
|
+
internal fun emitTokenRequest(
|
|
600
|
+
requestId: String,
|
|
601
|
+
resourceId: String,
|
|
602
|
+
accountId: String,
|
|
603
|
+
tenantId: String,
|
|
604
|
+
authority: String,
|
|
605
|
+
) {
|
|
606
|
+
emit(
|
|
607
|
+
"tokenRequest",
|
|
608
|
+
Arguments.createMap().apply {
|
|
609
|
+
putString("requestId", requestId)
|
|
610
|
+
putString("resourceId", resourceId)
|
|
611
|
+
putString("accountId", accountId)
|
|
612
|
+
putString("tenantId", tenantId)
|
|
613
|
+
putString("authority", authority)
|
|
614
|
+
},
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Settle first, publish second. The same notification serves a caller waiting on
|
|
620
|
+
* `enroll()` and every subscriber — including the SDK's own background retries, which
|
|
621
|
+
* have no caller at all (SPEC §13.5).
|
|
622
|
+
*/
|
|
623
|
+
internal fun emit(event: String, payload: WritableMap) {
|
|
624
|
+
if (event == "enrollmentResult" || event == "unenrollmentResult") {
|
|
625
|
+
payload.getString("accountId")?.let { settleEnrollment(it, payload.copy()) }
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// A service-initiated wipe opens the journal before anyone hears about it.
|
|
629
|
+
//
|
|
630
|
+
// Intercepted here because every notification passes through, and this is where the
|
|
631
|
+
// tenant id is in scope. The SDK terminates the process after a wipe, so a
|
|
632
|
+
// subscriber's cleanup may not finish — and with no journal entry there is no
|
|
633
|
+
// `pendingReset`, so the next launch has no idea anything happened and the app's own
|
|
634
|
+
// data and backend session are never dealt with. §7 already lists remote wipe as one
|
|
635
|
+
// of the callers of the single reset path; this makes that true (SPEC §7.4).
|
|
636
|
+
if (event == "wipeRequested") {
|
|
637
|
+
RNIntuneResetJournal.openForServiceWipe(
|
|
638
|
+
context = reactContext,
|
|
639
|
+
accountId = payload.getString("accountId"),
|
|
640
|
+
tenantId = config?.tenantId,
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
events.emit(event, payload)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ------------------------------------------------------------------ helpers
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The SDK's view of an account's enrollment.
|
|
651
|
+
*
|
|
652
|
+
* `getRegisteredAccountStatus(upn, oid)` — the order verified from bytecode, not
|
|
653
|
+
* documentation: the offline implementation checks its *second* argument and warns
|
|
654
|
+
* "called without valid OID", then calls `MAMIdentityManager.create(arg1, arg2)`.
|
|
655
|
+
* Passing the object ID alone lands it in the UPN slot and the registry answers "not
|
|
656
|
+
* registered" for an account that is — observed on device as
|
|
657
|
+
* `getAccountInfo() called for account that is not registered: <oid>;<null>`.
|
|
658
|
+
*
|
|
659
|
+
* The UPN comes from the SDK rather than from this object's fields, because those are
|
|
660
|
+
* in memory and a status read after a restart would otherwise silently miss.
|
|
661
|
+
*/
|
|
662
|
+
private fun readStatus(accountId: String): MAMEnrollmentManager.Result? =
|
|
663
|
+
runCatching {
|
|
664
|
+
val manager = MAMComponents.get(MAMEnrollmentManager::class.java) ?: return@runCatching null
|
|
665
|
+
val upn = registeredUpn
|
|
666
|
+
?: MAMComponents.get(MAMUserInfo::class.java)?.primaryUser
|
|
667
|
+
?: return@runCatching null
|
|
668
|
+
manager.getRegisteredAccountStatus(upn, accountId)
|
|
669
|
+
}
|
|
670
|
+
.getOrNull()
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* True when the MAM SDK is on the classpath. Resolved by name so this file needs no
|
|
674
|
+
* SDK import to answer it, and so a misbuilt app degrades instead of crashing.
|
|
675
|
+
*/
|
|
676
|
+
private val sdkAvailable: Boolean by lazy {
|
|
677
|
+
runCatching { Class.forName("com.microsoft.intune.mam.policy.MAMEnrollmentManager") }
|
|
678
|
+
.isSuccess
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Requires a `<queries>` entry for the package in the consumer's manifest on Android
|
|
683
|
+
* 11+. Without it this returns false even when Company Portal is installed — the same
|
|
684
|
+
* omission that makes MSAL fall back to a browser and lose the broker (SPEC §6.2).
|
|
685
|
+
*/
|
|
686
|
+
/**
|
|
687
|
+
* Whether Company Portal is present, preferring the SDK's own answer.
|
|
688
|
+
*
|
|
689
|
+
* `MAMComponents.isCompanyPortalInstalled` is first-party and, more importantly, does
|
|
690
|
+
* not depend on the consumer having declared the broker packages in `<queries>`. Our
|
|
691
|
+
* own `packageManager` lookup does: on Android 11+ an undeclared package is invisible,
|
|
692
|
+
* so a consumer who skips that manifest entry gets a confident `false` for a broker
|
|
693
|
+
* that is installed — and then tells the user to install what they already have.
|
|
694
|
+
*
|
|
695
|
+
* That exact failure happened on iOS for a different reason (the wrong URL form), and
|
|
696
|
+
* it is worth not leaving a second way to reach it.
|
|
697
|
+
*
|
|
698
|
+
* The package lookup stays as the fallback: it is the only thing that can answer before
|
|
699
|
+
* `configure`, and `getBrokerStatus` is deliberately answerable then, because the app
|
|
700
|
+
* needs it to decide whether to prompt for a broker install at all (SPEC §13.1).
|
|
701
|
+
*/
|
|
702
|
+
private fun isCompanyPortalInstalled(): Boolean =
|
|
703
|
+
runCatching { MAMComponents.isCompanyPortalInstalled(reactContext) }
|
|
704
|
+
.getOrNull()
|
|
705
|
+
?: isPackageInstalled(COMPANY_PORTAL_PACKAGE)
|
|
706
|
+
|
|
707
|
+
private fun isPackageInstalled(packageName: String): Boolean =
|
|
708
|
+
runCatching { reactContext.packageManager.getPackageInfo(packageName, 0) }.isSuccess
|
|
709
|
+
|
|
710
|
+
private fun notConfigured(method: String, promise: Promise) {
|
|
711
|
+
promise.reject(ERR_NOT_CONFIGURED, "$method was called before configure() resolved.")
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
companion object {
|
|
715
|
+
/** Resolves to "RNIntune" — the name in TurboModuleRegistry.getEnforcing (SPEC §2.2). */
|
|
716
|
+
const val NAME = NativeIntuneSpec.NAME
|
|
717
|
+
|
|
718
|
+
private const val TAG = "RNIntune"
|
|
719
|
+
|
|
720
|
+
private const val COMPANY_PORTAL_PACKAGE = "com.microsoft.windowsintune.companyportal"
|
|
721
|
+
|
|
722
|
+
// Stable rejection codes (SPEC §13.6). Never surface a raw platform error to JS.
|
|
723
|
+
// internal, not private: RNIntuneAuth rejects with the same vocabulary and there must
|
|
724
|
+
// be exactly one definition of each string (SPEC §13.6).
|
|
725
|
+
internal const val ERR_NOT_CONFIGURED = "E_NOT_CONFIGURED"
|
|
726
|
+
private const val ERR_RESET_REQUIRED = "E_RESET_REQUIRED"
|
|
727
|
+
private const val ERR_SDK_UNAVAILABLE = "E_SDK_UNAVAILABLE"
|
|
728
|
+
private const val ERR_INVALID_ACCOUNT_ID = "E_INVALID_ACCOUNT_ID"
|
|
729
|
+
private const val ERR_UPN_REQUIRED = "E_UPN_REQUIRED"
|
|
730
|
+
private const val ERR_RESET_IN_PROGRESS = "E_RESET_IN_PROGRESS"
|
|
731
|
+
private const val ERR_NOT_NEEDED = "E_NOT_NEEDED"
|
|
732
|
+
internal const val ERR_NATIVE = "E_NATIVE"
|
|
733
|
+
|
|
734
|
+
// MSAL outcomes that are ordinary control flow, not faults.
|
|
735
|
+
internal const val ERR_INTERACTION_REQUIRED = "E_INTERACTION_REQUIRED"
|
|
736
|
+
internal const val ERR_USER_CANCELLED = "E_USER_CANCELLED"
|
|
737
|
+
internal const val ERR_EXTERNAL_AUTH_MODE = "E_EXTERNAL_AUTH_MODE"
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Android-only. React Native outlives its Activity, so an interactive sign-in can be
|
|
741
|
+
* asked for when there is no window to present it in — a real state, not a
|
|
742
|
+
* formality, and one the app can recover from by retrying from a mounted screen.
|
|
743
|
+
*/
|
|
744
|
+
internal const val ERR_NO_ACTIVITY = "E_NO_ACTIVITY"
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Generous on purpose: enrollment involves a token acquisition and a service round
|
|
748
|
+
* trip, and returning early would report a failure the SDK has not reached.
|
|
749
|
+
*/
|
|
750
|
+
private const val ENROLL_TIMEOUT_MS = 90_000L
|
|
751
|
+
}
|
|
752
|
+
}
|