react-native-nitro-auth 0.5.4 → 0.5.5

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 (43) hide show
  1. package/README.md +60 -30
  2. package/android/src/main/cpp/JniOnLoad.cpp +3 -1
  3. package/android/src/main/cpp/PlatformAuth+Android.cpp +11 -11
  4. package/android/src/main/java/com/auth/AuthAdapter.kt +100 -116
  5. package/android/src/main/java/com/auth/NitroAuthModule.kt +8 -1
  6. package/ios/AuthAdapter.swift +62 -28
  7. package/lib/commonjs/index.js +23 -1
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/service.js +31 -6
  10. package/lib/commonjs/service.js.map +1 -1
  11. package/lib/commonjs/use-auth.js +11 -22
  12. package/lib/commonjs/use-auth.js.map +1 -1
  13. package/lib/commonjs/utils/auth-error.js +37 -0
  14. package/lib/commonjs/utils/auth-error.js.map +1 -0
  15. package/lib/module/index.js +1 -0
  16. package/lib/module/index.js.map +1 -1
  17. package/lib/module/service.js +31 -6
  18. package/lib/module/service.js.map +1 -1
  19. package/lib/module/use-auth.js +11 -22
  20. package/lib/module/use-auth.js.map +1 -1
  21. package/lib/module/utils/auth-error.js +30 -0
  22. package/lib/module/utils/auth-error.js.map +1 -0
  23. package/lib/typescript/commonjs/index.d.ts +1 -0
  24. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/use-auth.d.ts +2 -1
  27. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/utils/auth-error.d.ts +16 -0
  29. package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -0
  30. package/lib/typescript/module/index.d.ts +1 -0
  31. package/lib/typescript/module/index.d.ts.map +1 -1
  32. package/lib/typescript/module/service.d.ts.map +1 -1
  33. package/lib/typescript/module/use-auth.d.ts +2 -1
  34. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  35. package/lib/typescript/module/utils/auth-error.d.ts +16 -0
  36. package/lib/typescript/module/utils/auth-error.d.ts.map +1 -0
  37. package/nitrogen/generated/android/NitroAuthOnLoad.cpp +22 -17
  38. package/nitrogen/generated/android/NitroAuthOnLoad.hpp +13 -4
  39. package/package.json +7 -7
  40. package/src/index.ts +1 -0
  41. package/src/service.ts +32 -6
  42. package/src/use-auth.ts +21 -86
  43. package/src/utils/auth-error.ts +49 -0
@@ -1,37 +1,38 @@
1
- @file:Suppress("DEPRECATION")
2
-
3
1
  package com.auth
4
2
 
3
+ import android.app.Activity
4
+ import android.app.Application
5
5
  import android.content.Context
6
+ import android.content.Intent
7
+ import android.net.Uri
6
8
  import android.os.Bundle
9
+ import android.util.Base64
7
10
  import android.util.Log
11
+ import androidx.browser.customtabs.CustomTabsIntent
12
+ import androidx.credentials.CredentialManager
13
+ import androidx.credentials.GetCredentialRequest
14
+ import androidx.credentials.GetCredentialResponse
8
15
  import com.google.android.gms.auth.api.signin.GoogleSignIn
9
16
  import com.google.android.gms.auth.api.signin.GoogleSignInAccount
10
17
  import com.google.android.gms.auth.api.signin.GoogleSignInClient
11
18
  import com.google.android.gms.auth.api.signin.GoogleSignInOptions
12
- import com.google.android.gms.common.GoogleApiAvailability
13
19
  import com.google.android.gms.common.ConnectionResult
20
+ import com.google.android.gms.common.GoogleApiAvailability
14
21
  import com.google.android.gms.common.api.Scope
15
- import androidx.credentials.CredentialManager
16
- import androidx.credentials.GetCredentialRequest
17
- import androidx.credentials.GetCredentialResponse
18
22
  import com.google.android.libraries.identity.googleid.GetGoogleIdOption
19
23
  import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
20
24
  import kotlinx.coroutines.CoroutineScope
21
25
  import kotlinx.coroutines.Dispatchers
26
+ import kotlinx.coroutines.SupervisorJob
27
+ import kotlinx.coroutines.cancel
22
28
  import kotlinx.coroutines.launch
23
- import android.app.Activity
24
- import android.app.Application
25
- import android.content.Intent
26
- import android.net.Uri
27
- import androidx.browser.customtabs.CustomTabsIntent
28
- import java.util.UUID
29
+ import kotlinx.coroutines.withContext
29
30
  import org.json.JSONObject
30
- import android.util.Base64
31
+ import java.util.UUID
31
32
 
32
33
  object AuthAdapter {
33
34
  private const val TAG = "AuthAdapter"
34
-
35
+
35
36
  @Volatile
36
37
  private var isInitialized = false
37
38
 
@@ -41,34 +42,37 @@ object AuthAdapter {
41
42
  private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
42
43
  private var pendingScopes: List<String> = emptyList()
43
44
  private var pendingMicrosoftScopes: List<String> = emptyList()
44
-
45
+
45
46
  private var pendingPkceVerifier: String? = null
46
47
  private var pendingState: String? = null
47
48
  private var pendingNonce: String? = null
48
49
  private var pendingMicrosoftTenant: String? = null
49
50
  private var pendingMicrosoftClientId: String? = null
50
51
  private var pendingMicrosoftB2cDomain: String? = null
51
-
52
+
52
53
  private var inMemoryMicrosoftRefreshToken: String? = null
53
54
  private var inMemoryMicrosoftScopes: List<String> =
54
55
  listOf("openid", "email", "profile", "offline_access", "User.Read")
55
56
 
57
+ // Module-scoped coroutine scope — cancelled on module invalidation via dispose().
58
+ private var moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
59
+
56
60
  @JvmStatic
57
61
  private external fun nativeInitialize(context: Context)
58
-
62
+
59
63
  @JvmStatic
60
64
  private external fun nativeOnLoginSuccess(
61
- provider: String,
62
- email: String?,
63
- name: String?,
64
- photo: String?,
65
+ provider: String,
66
+ email: String?,
67
+ name: String?,
68
+ photo: String?,
65
69
  idToken: String?,
66
70
  accessToken: String?,
67
71
  serverAuthCode: String?,
68
72
  scopes: Array<String>?,
69
73
  expirationTime: Long?
70
74
  )
71
-
75
+
72
76
  @JvmStatic
73
77
  private external fun nativeOnLoginError(error: String, underlyingError: String?)
74
78
 
@@ -80,9 +84,7 @@ object AuthAdapter {
80
84
 
81
85
  @Synchronized
82
86
  fun initialize(context: Context) {
83
- if (isInitialized) {
84
- return
85
- }
87
+ if (isInitialized) return
86
88
 
87
89
  val applicationContext = context.applicationContext
88
90
  appContext = applicationContext
@@ -102,19 +104,35 @@ object AuthAdapter {
102
104
  }
103
105
 
104
106
  try {
105
- System.loadLibrary("NitroAuth")
107
+ // The native library is already loaded by NitroAuthOnLoad.initializeNative()
108
+ // before this method is called from NitroAuthModule. We only need to wire
109
+ // the Android context so that native methods can call back into the JVM.
106
110
  nativeInitialize(applicationContext)
107
111
  isInitialized = true
108
112
  } catch (e: Exception) {
109
- Log.e(TAG, "Failed to load NitroAuth library", e)
113
+ Log.e(TAG, "Failed to initialize NitroAuth native bridge", e)
110
114
  }
111
115
  }
112
116
 
117
+ fun dispose() {
118
+ moduleScope.cancel()
119
+ moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
120
+
121
+ val app = appContext as? Application
122
+ lifecycleCallbacks?.let { app?.unregisterActivityLifecycleCallbacks(it) }
123
+ lifecycleCallbacks = null
124
+ currentActivity = null
125
+ appContext = null
126
+ googleSignInClient = null
127
+ isInitialized = false
128
+ }
129
+
113
130
  fun onSignInSuccess(account: GoogleSignInAccount, scopes: List<String>) {
114
131
  appContext ?: return
115
132
  val expirationTime = getJwtExpirationTimeMs(account.idToken)
116
133
  nativeOnLoginSuccess("google", account.email, account.displayName,
117
- account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode, scopes.toTypedArray(), expirationTime)
134
+ account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
135
+ scopes.toTypedArray(), expirationTime)
118
136
  }
119
137
 
120
138
  fun onSignInError(errorCode: Int, message: String?) {
@@ -144,12 +162,10 @@ object AuthAdapter {
144
162
  nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
145
163
  return
146
164
  }
147
-
148
165
  if (provider == "microsoft") {
149
166
  loginMicrosoft(context, scopes, loginHint, tenant, prompt)
150
167
  return
151
168
  }
152
-
153
169
  if (provider != "google") {
154
170
  nativeOnLoginError("unsupported_provider", "Unsupported provider: $provider")
155
171
  return
@@ -169,7 +185,6 @@ object AuthAdapter {
169
185
  loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker)
170
186
  return
171
187
  }
172
-
173
188
  loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap)
174
189
  }
175
190
 
@@ -253,19 +268,16 @@ object AuthAdapter {
253
268
  nativeOnLoginError(error, errorDescription)
254
269
  return
255
270
  }
256
-
257
271
  if (state != pendingState) {
258
272
  clearPkceState()
259
273
  nativeOnLoginError("invalid_state", "State mismatch - possible CSRF attack")
260
274
  return
261
275
  }
262
-
263
276
  if (code == null) {
264
277
  clearPkceState()
265
278
  nativeOnLoginError("unknown", "No authorization code in response")
266
279
  return
267
280
  }
268
-
269
281
  exchangeCodeForTokens(code)
270
282
  }
271
283
 
@@ -285,7 +297,7 @@ object AuthAdapter {
285
297
  val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, pendingMicrosoftB2cDomain)
286
298
  val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
287
299
 
288
- CoroutineScope(Dispatchers.IO).launch {
300
+ moduleScope.launch {
289
301
  try {
290
302
  val url = java.net.URL(tokenUrl)
291
303
  val connection = url.openConnection() as java.net.HttpURLConnection
@@ -300,7 +312,6 @@ object AuthAdapter {
300
312
  append("&grant_type=authorization_code")
301
313
  append("&code_verifier=${java.net.URLEncoder.encode(verifier, "UTF-8")}")
302
314
  }
303
-
304
315
  connection.outputStream.use { it.write(postData.toByteArray()) }
305
316
 
306
317
  val responseCode = connection.responseCode
@@ -310,11 +321,11 @@ object AuthAdapter {
310
321
  connection.errorStream?.bufferedReader()?.readText() ?: ""
311
322
  }
312
323
 
313
- CoroutineScope(Dispatchers.Main).launch {
324
+ withContext(Dispatchers.Main) {
314
325
  handleTokenResponse(responseCode, responseBody)
315
326
  }
316
327
  } catch (e: Exception) {
317
- CoroutineScope(Dispatchers.Main).launch {
328
+ withContext(Dispatchers.Main) {
318
329
  clearPkceState()
319
330
  nativeOnLoginError("network_error", e.message)
320
331
  }
@@ -352,8 +363,7 @@ object AuthAdapter {
352
363
  }
353
364
 
354
365
  val claims = decodeJwt(idToken)
355
- val tokenNonce = claims["nonce"]
356
- if (tokenNonce != pendingNonce) {
366
+ if (claims["nonce"] != pendingNonce) {
357
367
  clearPkceState()
358
368
  nativeOnLoginError("invalid_nonce", "Nonce mismatch - token may be replayed")
359
369
  return
@@ -362,24 +372,15 @@ object AuthAdapter {
362
372
  val email = claims["preferred_username"] ?: claims["email"]
363
373
  val name = claims["name"]
364
374
 
365
- if (refreshToken.isNotEmpty()) {
366
- inMemoryMicrosoftRefreshToken = refreshToken
367
- }
375
+ if (refreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = refreshToken
368
376
  inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
369
377
  listOf("openid", "email", "profile", "offline_access", "User.Read")
370
378
  }
371
379
 
372
380
  clearPkceState()
373
381
  nativeOnLoginSuccess(
374
- "microsoft",
375
- email,
376
- name,
377
- null,
378
- idToken,
379
- accessToken,
380
- null,
381
- pendingMicrosoftScopes.toTypedArray(),
382
- expirationTime
382
+ "microsoft", email, name, null, idToken, accessToken, null,
383
+ pendingMicrosoftScopes.toTypedArray(), expirationTime
383
384
  )
384
385
  } catch (e: Exception) {
385
386
  clearPkceState()
@@ -387,14 +388,6 @@ object AuthAdapter {
387
388
  }
388
389
  }
389
390
 
390
- private fun saveMicrosoftRefreshToken(refreshToken: String) {
391
- inMemoryMicrosoftRefreshToken = refreshToken
392
- }
393
-
394
- private fun getMicrosoftRefreshToken(): String? {
395
- return inMemoryMicrosoftRefreshToken
396
- }
397
-
398
391
  private fun clearPkceState() {
399
392
  pendingPkceVerifier = null
400
393
  pendingState = null
@@ -422,10 +415,7 @@ object AuthAdapter {
422
415
  }
423
416
 
424
417
  private fun getJwtExpirationTimeMs(idToken: String?): Long? {
425
- if (idToken.isNullOrEmpty()) {
426
- return null
427
- }
428
-
418
+ if (idToken.isNullOrEmpty()) return null
429
419
  val expSeconds = decodeJwt(idToken)["exp"]?.toLongOrNull() ?: return null
430
420
  return expSeconds * 1000
431
421
  }
@@ -449,7 +439,6 @@ object AuthAdapter {
449
439
  if (tenant.startsWith("https://")) {
450
440
  return if (tenant.endsWith("/")) tenant else "$tenant/"
451
441
  }
452
-
453
442
  return if (b2cDomain != null) {
454
443
  "https://$b2cDomain/tfp/$tenant/"
455
444
  } else {
@@ -470,7 +459,7 @@ object AuthAdapter {
470
459
  Log.w(TAG, "No Activity context available for One-Tap, falling back to legacy")
471
460
  return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
472
461
  }
473
-
462
+
474
463
  val credentialManager = CredentialManager.create(activity)
475
464
  val googleIdOption = GetGoogleIdOption.Builder()
476
465
  .setFilterByAuthorizedAccounts(false)
@@ -482,7 +471,7 @@ object AuthAdapter {
482
471
  .addCredentialOption(googleIdOption)
483
472
  .build()
484
473
 
485
- CoroutineScope(Dispatchers.Main).launch {
474
+ moduleScope.launch(Dispatchers.Main) {
486
475
  try {
487
476
  val result = credentialManager.getCredential(context = activity, request = request)
488
477
  handleCredentialResponse(result, scopes)
@@ -493,6 +482,7 @@ object AuthAdapter {
493
482
  }
494
483
  }
495
484
 
485
+ @Suppress("DEPRECATION")
496
486
  private fun loginLegacy(
497
487
  context: Context,
498
488
  clientId: String,
@@ -502,11 +492,7 @@ object AuthAdapter {
502
492
  ) {
503
493
  val ctx = appContext ?: context.applicationContext
504
494
  val intent = GoogleSignInActivity.createIntent(
505
- ctx,
506
- clientId,
507
- scopes.toTypedArray(),
508
- loginHint,
509
- forceAccountPicker
495
+ ctx, clientId, scopes.toTypedArray(), loginHint, forceAccountPicker
510
496
  )
511
497
  intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
512
498
  ctx.startActivity(intent)
@@ -535,8 +521,7 @@ object AuthAdapter {
535
521
  googleIdTokenCredential.displayName,
536
522
  googleIdTokenCredential.profilePictureUri?.toString(),
537
523
  googleIdTokenCredential.idToken,
538
- null,
539
- null,
524
+ null, null,
540
525
  scopes.toTypedArray(),
541
526
  expirationTime
542
527
  )
@@ -546,6 +531,9 @@ object AuthAdapter {
546
531
  }
547
532
  }
548
533
 
534
+ // requestScopesSync uses the legacy GoogleSignIn API to check the last signed-in account
535
+ // because Credential Manager has no equivalent for querying existing account state.
536
+ @Suppress("DEPRECATION")
549
537
  @JvmStatic
550
538
  fun requestScopesSync(context: Context, scopes: Array<String>) {
551
539
  val ctx = appContext ?: context.applicationContext
@@ -575,6 +563,9 @@ object AuthAdapter {
575
563
  nativeOnLoginError("unknown", "No user logged in")
576
564
  }
577
565
 
566
+ // refreshTokenSync uses the legacy silentSignIn because AuthorizationClient (the recommended
567
+ // replacement) requires an Activity context which is not always available at refresh time.
568
+ @Suppress("DEPRECATION")
578
569
  @JvmStatic
579
570
  fun refreshTokenSync(context: Context) {
580
571
  val ctx = appContext ?: context.applicationContext
@@ -596,18 +587,14 @@ object AuthAdapter {
596
587
  googleSignInClient!!.silentSignIn().addOnCompleteListener { task ->
597
588
  if (task.isSuccessful) {
598
589
  val acc = task.result
599
- nativeOnRefreshSuccess(
600
- acc?.idToken,
601
- null,
602
- getJwtExpirationTimeMs(acc?.idToken),
603
- )
590
+ nativeOnRefreshSuccess(acc?.idToken, null, getJwtExpirationTimeMs(acc?.idToken))
604
591
  } else {
605
592
  nativeOnRefreshError("network_error", task.exception?.message ?: "Silent sign-in failed")
606
593
  }
607
594
  }
608
595
  return
609
596
  }
610
- val refreshToken = getMicrosoftRefreshToken()
597
+ val refreshToken = inMemoryMicrosoftRefreshToken
611
598
  if (refreshToken != null) {
612
599
  refreshMicrosoftTokenForRefresh(ctx, refreshToken)
613
600
  return
@@ -618,37 +605,34 @@ object AuthAdapter {
618
605
  @JvmStatic
619
606
  fun hasPlayServices(context: Context): Boolean {
620
607
  val ctx = context.applicationContext ?: appContext ?: return false
621
- val availability = GoogleApiAvailability.getInstance()
622
- val result = availability.isGooglePlayServicesAvailable(ctx)
623
- return result == ConnectionResult.SUCCESS
608
+ return GoogleApiAvailability.getInstance()
609
+ .isGooglePlayServicesAvailable(ctx) == ConnectionResult.SUCCESS
624
610
  }
625
611
 
612
+ // logoutSync / revokeAccessSync use the legacy GoogleSignIn client because Credential Manager
613
+ // does not expose a sign-out or revoke API for the Google ID token flow.
614
+ @Suppress("DEPRECATION")
626
615
  @JvmStatic
627
616
  fun logoutSync(context: Context) {
628
617
  val ctx = appContext ?: context.applicationContext
629
618
  val clientId = getClientIdFromResources(ctx)
630
619
  if (clientId != null) {
631
620
  val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
632
- .requestIdToken(clientId)
633
- .requestServerAuthCode(clientId)
634
- .requestEmail()
635
- .build()
621
+ .requestIdToken(clientId).requestServerAuthCode(clientId).requestEmail().build()
636
622
  GoogleSignIn.getClient(ctx, gso).signOut()
637
623
  }
638
624
  inMemoryMicrosoftRefreshToken = null
639
625
  inMemoryMicrosoftScopes = listOf("openid", "email", "profile", "offline_access", "User.Read")
640
626
  }
641
627
 
628
+ @Suppress("DEPRECATION")
642
629
  @JvmStatic
643
630
  fun revokeAccessSync(context: Context) {
644
631
  val ctx = appContext ?: context.applicationContext
645
632
  val clientId = getClientIdFromResources(ctx)
646
633
  if (clientId != null) {
647
634
  val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
648
- .requestIdToken(clientId)
649
- .requestServerAuthCode(clientId)
650
- .requestEmail()
651
- .build()
635
+ .requestIdToken(clientId).requestServerAuthCode(clientId).requestEmail().build()
652
636
  GoogleSignIn.getClient(ctx, gso).revokeAccess()
653
637
  }
654
638
  inMemoryMicrosoftRefreshToken = null
@@ -663,14 +647,15 @@ object AuthAdapter {
663
647
  @JvmStatic
664
648
  fun restoreSession(context: Context) {
665
649
  val ctx = context.applicationContext ?: appContext ?: context
650
+ @Suppress("DEPRECATION")
666
651
  val account = GoogleSignIn.getLastSignedInAccount(ctx)
667
652
  if (account != null) {
668
653
  val expirationTime = getJwtExpirationTimeMs(account.idToken)
669
654
  nativeOnLoginSuccess("google", account.email, account.displayName,
670
- account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
671
- account.grantedScopes?.map { it.scopeUri }?.toTypedArray(), expirationTime)
655
+ account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
656
+ account.grantedScopes?.map { it.scopeUri }?.toTypedArray(), expirationTime)
672
657
  } else {
673
- val refreshToken = getMicrosoftRefreshToken()
658
+ val refreshToken = inMemoryMicrosoftRefreshToken
674
659
  if (refreshToken != null) {
675
660
  refreshMicrosoftToken(ctx, refreshToken)
676
661
  } else {
@@ -683,7 +668,7 @@ object AuthAdapter {
683
668
  val clientId = getMicrosoftClientIdFromResources(context)
684
669
  val tenant = getMicrosoftTenantFromResources(context) ?: "common"
685
670
  val b2cDomain = getMicrosoftB2cDomainFromResources(context)
686
-
671
+
687
672
  if (clientId == null) {
688
673
  nativeOnLoginError("configuration_error", "Microsoft Client ID is required for refresh")
689
674
  return
@@ -692,7 +677,7 @@ object AuthAdapter {
692
677
  val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
693
678
  val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
694
679
 
695
- CoroutineScope(Dispatchers.IO).launch {
680
+ moduleScope.launch {
696
681
  try {
697
682
  val url = java.net.URL(tokenUrl)
698
683
  val connection = url.openConnection() as java.net.HttpURLConnection
@@ -705,7 +690,6 @@ object AuthAdapter {
705
690
  append("&grant_type=refresh_token")
706
691
  append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
707
692
  }
708
-
709
693
  connection.outputStream.use { it.write(postData.toByteArray()) }
710
694
 
711
695
  val responseCode = connection.responseCode
@@ -715,7 +699,7 @@ object AuthAdapter {
715
699
  connection.errorStream?.bufferedReader()?.readText() ?: ""
716
700
  }
717
701
 
718
- CoroutineScope(Dispatchers.Main).launch {
702
+ withContext(Dispatchers.Main) {
719
703
  if (responseCode == 200) {
720
704
  val json = JSONObject(responseBody)
721
705
  val newIdToken = json.optString("id_token")
@@ -723,25 +707,23 @@ object AuthAdapter {
723
707
  val newRefreshToken = json.optString("refresh_token")
724
708
  val expiresIn = json.optLong("expires_in", 0)
725
709
  val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
726
-
727
710
  val claims = decodeJwt(newIdToken)
728
- val email = claims["preferred_username"] ?: claims["email"]
729
- val name = claims["name"]
730
711
 
731
- if (newRefreshToken.isNotEmpty()) {
732
- saveMicrosoftRefreshToken(newRefreshToken)
733
- }
712
+ if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
734
713
  inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
735
714
  listOf("openid", "email", "profile", "offline_access", "User.Read")
736
715
  }
737
716
 
738
- nativeOnLoginSuccess("microsoft", email, name, null, newIdToken, newAccessToken, null, null, expirationTime)
717
+ nativeOnLoginSuccess("microsoft",
718
+ claims["preferred_username"] ?: claims["email"],
719
+ claims["name"], null,
720
+ newIdToken, newAccessToken, null, null, expirationTime)
739
721
  } else {
740
722
  nativeOnLoginError("refresh_failed", "Microsoft token refresh failed")
741
723
  }
742
724
  }
743
725
  } catch (e: Exception) {
744
- CoroutineScope(Dispatchers.Main).launch {
726
+ withContext(Dispatchers.Main) {
745
727
  nativeOnLoginError("network_error", e.message)
746
728
  }
747
729
  }
@@ -752,32 +734,38 @@ object AuthAdapter {
752
734
  val clientId = getMicrosoftClientIdFromResources(context)
753
735
  val tenant = getMicrosoftTenantFromResources(context) ?: "common"
754
736
  val b2cDomain = getMicrosoftB2cDomainFromResources(context)
737
+
755
738
  if (clientId == null) {
756
739
  nativeOnRefreshError("configuration_error", "Microsoft Client ID not configured")
757
740
  return
758
741
  }
742
+
759
743
  val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
760
744
  val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
761
- CoroutineScope(Dispatchers.IO).launch {
745
+
746
+ moduleScope.launch {
762
747
  try {
763
748
  val url = java.net.URL(tokenUrl)
764
749
  val connection = url.openConnection() as java.net.HttpURLConnection
765
750
  connection.requestMethod = "POST"
766
751
  connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
767
752
  connection.doOutput = true
753
+
768
754
  val postData = buildString {
769
755
  append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
770
756
  append("&grant_type=refresh_token")
771
757
  append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
772
758
  }
773
759
  connection.outputStream.use { it.write(postData.toByteArray()) }
760
+
774
761
  val responseCode = connection.responseCode
775
762
  val responseBody = if (responseCode == 200) {
776
763
  connection.inputStream.bufferedReader().readText()
777
764
  } else {
778
765
  connection.errorStream?.bufferedReader()?.readText() ?: ""
779
766
  }
780
- CoroutineScope(Dispatchers.Main).launch {
767
+
768
+ withContext(Dispatchers.Main) {
781
769
  if (responseCode == 200) {
782
770
  val json = JSONObject(responseBody)
783
771
  val newIdToken = json.optString("id_token")
@@ -785,15 +773,12 @@ object AuthAdapter {
785
773
  val newRefreshToken = json.optString("refresh_token")
786
774
  val expiresIn = json.optLong("expires_in", 0)
787
775
  val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
788
- val claims = decodeJwt(newIdToken)
789
- val email = claims["preferred_username"] ?: claims["email"]
790
- val name = claims["name"]
791
- if (newRefreshToken.isNotEmpty()) {
792
- saveMicrosoftRefreshToken(newRefreshToken)
793
- }
776
+
777
+ if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
794
778
  inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
795
779
  listOf("openid", "email", "profile", "offline_access", "User.Read")
796
780
  }
781
+
797
782
  nativeOnRefreshSuccess(
798
783
  newIdToken.ifEmpty { null },
799
784
  newAccessToken.ifEmpty { null },
@@ -804,11 +789,10 @@ object AuthAdapter {
804
789
  }
805
790
  }
806
791
  } catch (e: Exception) {
807
- CoroutineScope(Dispatchers.Main).launch {
792
+ withContext(Dispatchers.Main) {
808
793
  nativeOnRefreshError("network_error", e.message)
809
794
  }
810
795
  }
811
796
  }
812
797
  }
813
-
814
798
  }
@@ -3,17 +3,24 @@ package com.auth
3
3
  import android.util.Log
4
4
  import com.facebook.react.bridge.ReactApplicationContext
5
5
  import com.facebook.react.bridge.ReactContextBaseJavaModule
6
+ import com.margelo.nitro.com.auth.NitroAuthOnLoad
6
7
 
7
8
  class NitroAuthModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
8
9
  override fun getName(): String = "NitroAuthModule"
9
10
 
10
11
  init {
11
12
  try {
13
+ // Load the native library first so that AuthAdapter's JNI methods are resolvable.
14
+ NitroAuthOnLoad.initializeNative()
12
15
  AuthAdapter.initialize(reactContext)
13
- com.margelo.nitro.com.auth.NitroAuthOnLoad.initializeNative()
14
16
  Log.d("NitroAuthModule", "NitroAuth initialized")
15
17
  } catch (e: Exception) {
16
18
  Log.e("NitroAuthModule", "Failed to initialize NitroAuth", e)
17
19
  }
18
20
  }
21
+
22
+ override fun invalidate() {
23
+ super.invalidate()
24
+ AuthAdapter.dispose()
25
+ }
19
26
  }