react-native-nitro-auth 0.5.3 → 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 (59) hide show
  1. package/README.md +60 -30
  2. package/android/build.gradle +2 -5
  3. package/android/src/main/cpp/JniOnLoad.cpp +3 -1
  4. package/android/src/main/cpp/PlatformAuth+Android.cpp +95 -29
  5. package/android/src/main/java/com/auth/AuthAdapter.kt +124 -126
  6. package/android/src/main/java/com/auth/NitroAuthModule.kt +8 -1
  7. package/cpp/AuthCache.cpp +0 -44
  8. package/cpp/AuthCache.hpp +0 -7
  9. package/cpp/HybridAuth.cpp +20 -2
  10. package/cpp/HybridAuth.hpp +1 -0
  11. package/ios/AuthAdapter.swift +64 -28
  12. package/lib/commonjs/Auth.web.js +96 -43
  13. package/lib/commonjs/Auth.web.js.map +1 -1
  14. package/lib/commonjs/index.js +23 -1
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/service.js +33 -8
  17. package/lib/commonjs/service.js.map +1 -1
  18. package/lib/commonjs/use-auth.js +51 -54
  19. package/lib/commonjs/use-auth.js.map +1 -1
  20. package/lib/commonjs/utils/auth-error.js +37 -0
  21. package/lib/commonjs/utils/auth-error.js.map +1 -0
  22. package/lib/module/Auth.web.js +96 -43
  23. package/lib/module/Auth.web.js.map +1 -1
  24. package/lib/module/index.js +1 -0
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/service.js +33 -8
  27. package/lib/module/service.js.map +1 -1
  28. package/lib/module/use-auth.js +51 -54
  29. package/lib/module/use-auth.js.map +1 -1
  30. package/lib/module/utils/auth-error.js +30 -0
  31. package/lib/module/utils/auth-error.js.map +1 -0
  32. package/lib/typescript/commonjs/Auth.web.d.ts +7 -0
  33. package/lib/typescript/commonjs/Auth.web.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/index.d.ts +1 -0
  35. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/service.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/use-auth.d.ts +2 -1
  38. package/lib/typescript/commonjs/use-auth.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/utils/auth-error.d.ts +16 -0
  40. package/lib/typescript/commonjs/utils/auth-error.d.ts.map +1 -0
  41. package/lib/typescript/module/Auth.web.d.ts +7 -0
  42. package/lib/typescript/module/Auth.web.d.ts.map +1 -1
  43. package/lib/typescript/module/index.d.ts +1 -0
  44. package/lib/typescript/module/index.d.ts.map +1 -1
  45. package/lib/typescript/module/service.d.ts.map +1 -1
  46. package/lib/typescript/module/use-auth.d.ts +2 -1
  47. package/lib/typescript/module/use-auth.d.ts.map +1 -1
  48. package/lib/typescript/module/utils/auth-error.d.ts +16 -0
  49. package/lib/typescript/module/utils/auth-error.d.ts.map +1 -0
  50. package/nitrogen/generated/android/NitroAuthOnLoad.cpp +22 -17
  51. package/nitrogen/generated/android/NitroAuthOnLoad.hpp +13 -4
  52. package/package.json +7 -7
  53. package/react-native-nitro-auth.podspec +1 -1
  54. package/src/Auth.web.ts +124 -50
  55. package/src/index.ts +1 -0
  56. package/src/service.ts +34 -8
  57. package/src/use-auth.ts +81 -114
  58. package/src/utils/auth-error.ts +49 -0
  59. package/ios/KeychainStore.swift +0 -43
@@ -1,70 +1,78 @@
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
+
36
+ @Volatile
37
+ private var isInitialized = false
38
+
35
39
  private var appContext: Context? = null
36
40
  private var currentActivity: Activity? = null
37
41
  private var googleSignInClient: GoogleSignInClient? = null
42
+ private var lifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null
38
43
  private var pendingScopes: List<String> = emptyList()
39
44
  private var pendingMicrosoftScopes: List<String> = emptyList()
40
-
45
+
41
46
  private var pendingPkceVerifier: String? = null
42
47
  private var pendingState: String? = null
43
48
  private var pendingNonce: String? = null
44
49
  private var pendingMicrosoftTenant: String? = null
45
50
  private var pendingMicrosoftClientId: String? = null
46
51
  private var pendingMicrosoftB2cDomain: String? = null
47
-
52
+
48
53
  private var inMemoryMicrosoftRefreshToken: String? = null
49
54
  private var inMemoryMicrosoftScopes: List<String> =
50
55
  listOf("openid", "email", "profile", "offline_access", "User.Read")
51
56
 
57
+ // Module-scoped coroutine scope — cancelled on module invalidation via dispose().
58
+ private var moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
59
+
52
60
  @JvmStatic
53
61
  private external fun nativeInitialize(context: Context)
54
-
62
+
55
63
  @JvmStatic
56
64
  private external fun nativeOnLoginSuccess(
57
- provider: String,
58
- email: String?,
59
- name: String?,
60
- photo: String?,
65
+ provider: String,
66
+ email: String?,
67
+ name: String?,
68
+ photo: String?,
61
69
  idToken: String?,
62
70
  accessToken: String?,
63
71
  serverAuthCode: String?,
64
72
  scopes: Array<String>?,
65
73
  expirationTime: Long?
66
74
  )
67
-
75
+
68
76
  @JvmStatic
69
77
  private external fun nativeOnLoginError(error: String, underlyingError: String?)
70
78
 
@@ -74,33 +82,57 @@ object AuthAdapter {
74
82
  @JvmStatic
75
83
  private external fun nativeOnRefreshError(error: String, underlyingError: String?)
76
84
 
85
+ @Synchronized
77
86
  fun initialize(context: Context) {
78
- val app = context.applicationContext as? Application
79
- appContext = app
80
-
81
- app?.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
82
- override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity }
83
- override fun onActivityStarted(activity: Activity) { currentActivity = activity }
84
- override fun onActivityResumed(activity: Activity) { currentActivity = activity }
85
- override fun onActivityPaused(activity: Activity) { if (currentActivity == activity) currentActivity = null }
86
- override fun onActivityStopped(activity: Activity) { if (currentActivity == activity) currentActivity = null }
87
- override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
88
- override fun onActivityDestroyed(activity: Activity) { if (currentActivity == activity) currentActivity = null }
89
- })
87
+ if (isInitialized) return
88
+
89
+ val applicationContext = context.applicationContext
90
+ appContext = applicationContext
91
+
92
+ val app = applicationContext as? Application
93
+ if (app != null && lifecycleCallbacks == null) {
94
+ lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
95
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity }
96
+ override fun onActivityStarted(activity: Activity) { currentActivity = activity }
97
+ override fun onActivityResumed(activity: Activity) { currentActivity = activity }
98
+ override fun onActivityPaused(activity: Activity) { if (currentActivity == activity) currentActivity = null }
99
+ override fun onActivityStopped(activity: Activity) { if (currentActivity == activity) currentActivity = null }
100
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
101
+ override fun onActivityDestroyed(activity: Activity) { if (currentActivity == activity) currentActivity = null }
102
+ }
103
+ app.registerActivityLifecycleCallbacks(lifecycleCallbacks)
104
+ }
90
105
 
91
106
  try {
92
- System.loadLibrary("NitroAuth")
93
- nativeInitialize(appContext!!)
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.
110
+ nativeInitialize(applicationContext)
111
+ isInitialized = true
94
112
  } catch (e: Exception) {
95
- Log.e(TAG, "Failed to load NitroAuth library", e)
113
+ Log.e(TAG, "Failed to initialize NitroAuth native bridge", e)
96
114
  }
97
115
  }
98
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
+
99
130
  fun onSignInSuccess(account: GoogleSignInAccount, scopes: List<String>) {
100
131
  appContext ?: return
101
132
  val expirationTime = getJwtExpirationTimeMs(account.idToken)
102
133
  nativeOnLoginSuccess("google", account.email, account.displayName,
103
- account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode, scopes.toTypedArray(), expirationTime)
134
+ account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
135
+ scopes.toTypedArray(), expirationTime)
104
136
  }
105
137
 
106
138
  fun onSignInError(errorCode: Int, message: String?) {
@@ -130,12 +162,10 @@ object AuthAdapter {
130
162
  nativeOnLoginError("unsupported_provider", "Apple Sign-In is not supported on Android.")
131
163
  return
132
164
  }
133
-
134
165
  if (provider == "microsoft") {
135
166
  loginMicrosoft(context, scopes, loginHint, tenant, prompt)
136
167
  return
137
168
  }
138
-
139
169
  if (provider != "google") {
140
170
  nativeOnLoginError("unsupported_provider", "Unsupported provider: $provider")
141
171
  return
@@ -155,7 +185,6 @@ object AuthAdapter {
155
185
  loginLegacy(context, clientId, requestedScopes, loginHint, forceAccountPicker)
156
186
  return
157
187
  }
158
-
159
188
  loginOneTap(context, clientId, requestedScopes, loginHint, forceAccountPicker, useOneTap)
160
189
  }
161
190
 
@@ -239,19 +268,16 @@ object AuthAdapter {
239
268
  nativeOnLoginError(error, errorDescription)
240
269
  return
241
270
  }
242
-
243
271
  if (state != pendingState) {
244
272
  clearPkceState()
245
273
  nativeOnLoginError("invalid_state", "State mismatch - possible CSRF attack")
246
274
  return
247
275
  }
248
-
249
276
  if (code == null) {
250
277
  clearPkceState()
251
278
  nativeOnLoginError("unknown", "No authorization code in response")
252
279
  return
253
280
  }
254
-
255
281
  exchangeCodeForTokens(code)
256
282
  }
257
283
 
@@ -271,7 +297,7 @@ object AuthAdapter {
271
297
  val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, pendingMicrosoftB2cDomain)
272
298
  val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
273
299
 
274
- CoroutineScope(Dispatchers.IO).launch {
300
+ moduleScope.launch {
275
301
  try {
276
302
  val url = java.net.URL(tokenUrl)
277
303
  val connection = url.openConnection() as java.net.HttpURLConnection
@@ -286,7 +312,6 @@ object AuthAdapter {
286
312
  append("&grant_type=authorization_code")
287
313
  append("&code_verifier=${java.net.URLEncoder.encode(verifier, "UTF-8")}")
288
314
  }
289
-
290
315
  connection.outputStream.use { it.write(postData.toByteArray()) }
291
316
 
292
317
  val responseCode = connection.responseCode
@@ -296,11 +321,11 @@ object AuthAdapter {
296
321
  connection.errorStream?.bufferedReader()?.readText() ?: ""
297
322
  }
298
323
 
299
- CoroutineScope(Dispatchers.Main).launch {
324
+ withContext(Dispatchers.Main) {
300
325
  handleTokenResponse(responseCode, responseBody)
301
326
  }
302
327
  } catch (e: Exception) {
303
- CoroutineScope(Dispatchers.Main).launch {
328
+ withContext(Dispatchers.Main) {
304
329
  clearPkceState()
305
330
  nativeOnLoginError("network_error", e.message)
306
331
  }
@@ -338,8 +363,7 @@ object AuthAdapter {
338
363
  }
339
364
 
340
365
  val claims = decodeJwt(idToken)
341
- val tokenNonce = claims["nonce"]
342
- if (tokenNonce != pendingNonce) {
366
+ if (claims["nonce"] != pendingNonce) {
343
367
  clearPkceState()
344
368
  nativeOnLoginError("invalid_nonce", "Nonce mismatch - token may be replayed")
345
369
  return
@@ -348,24 +372,15 @@ object AuthAdapter {
348
372
  val email = claims["preferred_username"] ?: claims["email"]
349
373
  val name = claims["name"]
350
374
 
351
- if (refreshToken.isNotEmpty()) {
352
- inMemoryMicrosoftRefreshToken = refreshToken
353
- }
375
+ if (refreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = refreshToken
354
376
  inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
355
377
  listOf("openid", "email", "profile", "offline_access", "User.Read")
356
378
  }
357
379
 
358
380
  clearPkceState()
359
381
  nativeOnLoginSuccess(
360
- "microsoft",
361
- email,
362
- name,
363
- null,
364
- idToken,
365
- accessToken,
366
- null,
367
- pendingMicrosoftScopes.toTypedArray(),
368
- expirationTime
382
+ "microsoft", email, name, null, idToken, accessToken, null,
383
+ pendingMicrosoftScopes.toTypedArray(), expirationTime
369
384
  )
370
385
  } catch (e: Exception) {
371
386
  clearPkceState()
@@ -373,14 +388,6 @@ object AuthAdapter {
373
388
  }
374
389
  }
375
390
 
376
- private fun saveMicrosoftRefreshToken(refreshToken: String) {
377
- inMemoryMicrosoftRefreshToken = refreshToken
378
- }
379
-
380
- private fun getMicrosoftRefreshToken(): String? {
381
- return inMemoryMicrosoftRefreshToken
382
- }
383
-
384
391
  private fun clearPkceState() {
385
392
  pendingPkceVerifier = null
386
393
  pendingState = null
@@ -408,10 +415,7 @@ object AuthAdapter {
408
415
  }
409
416
 
410
417
  private fun getJwtExpirationTimeMs(idToken: String?): Long? {
411
- if (idToken.isNullOrEmpty()) {
412
- return null
413
- }
414
-
418
+ if (idToken.isNullOrEmpty()) return null
415
419
  val expSeconds = decodeJwt(idToken)["exp"]?.toLongOrNull() ?: return null
416
420
  return expSeconds * 1000
417
421
  }
@@ -435,7 +439,6 @@ object AuthAdapter {
435
439
  if (tenant.startsWith("https://")) {
436
440
  return if (tenant.endsWith("/")) tenant else "$tenant/"
437
441
  }
438
-
439
442
  return if (b2cDomain != null) {
440
443
  "https://$b2cDomain/tfp/$tenant/"
441
444
  } else {
@@ -456,7 +459,7 @@ object AuthAdapter {
456
459
  Log.w(TAG, "No Activity context available for One-Tap, falling back to legacy")
457
460
  return loginLegacy(context, clientId, scopes, loginHint, forceAccountPicker)
458
461
  }
459
-
462
+
460
463
  val credentialManager = CredentialManager.create(activity)
461
464
  val googleIdOption = GetGoogleIdOption.Builder()
462
465
  .setFilterByAuthorizedAccounts(false)
@@ -468,7 +471,7 @@ object AuthAdapter {
468
471
  .addCredentialOption(googleIdOption)
469
472
  .build()
470
473
 
471
- CoroutineScope(Dispatchers.Main).launch {
474
+ moduleScope.launch(Dispatchers.Main) {
472
475
  try {
473
476
  val result = credentialManager.getCredential(context = activity, request = request)
474
477
  handleCredentialResponse(result, scopes)
@@ -479,6 +482,7 @@ object AuthAdapter {
479
482
  }
480
483
  }
481
484
 
485
+ @Suppress("DEPRECATION")
482
486
  private fun loginLegacy(
483
487
  context: Context,
484
488
  clientId: String,
@@ -488,11 +492,7 @@ object AuthAdapter {
488
492
  ) {
489
493
  val ctx = appContext ?: context.applicationContext
490
494
  val intent = GoogleSignInActivity.createIntent(
491
- ctx,
492
- clientId,
493
- scopes.toTypedArray(),
494
- loginHint,
495
- forceAccountPicker
495
+ ctx, clientId, scopes.toTypedArray(), loginHint, forceAccountPicker
496
496
  )
497
497
  intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
498
498
  ctx.startActivity(intent)
@@ -521,8 +521,7 @@ object AuthAdapter {
521
521
  googleIdTokenCredential.displayName,
522
522
  googleIdTokenCredential.profilePictureUri?.toString(),
523
523
  googleIdTokenCredential.idToken,
524
- null,
525
- null,
524
+ null, null,
526
525
  scopes.toTypedArray(),
527
526
  expirationTime
528
527
  )
@@ -532,6 +531,9 @@ object AuthAdapter {
532
531
  }
533
532
  }
534
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")
535
537
  @JvmStatic
536
538
  fun requestScopesSync(context: Context, scopes: Array<String>) {
537
539
  val ctx = appContext ?: context.applicationContext
@@ -561,6 +563,9 @@ object AuthAdapter {
561
563
  nativeOnLoginError("unknown", "No user logged in")
562
564
  }
563
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")
564
569
  @JvmStatic
565
570
  fun refreshTokenSync(context: Context) {
566
571
  val ctx = appContext ?: context.applicationContext
@@ -582,18 +587,14 @@ object AuthAdapter {
582
587
  googleSignInClient!!.silentSignIn().addOnCompleteListener { task ->
583
588
  if (task.isSuccessful) {
584
589
  val acc = task.result
585
- nativeOnRefreshSuccess(
586
- acc?.idToken,
587
- null,
588
- getJwtExpirationTimeMs(acc?.idToken),
589
- )
590
+ nativeOnRefreshSuccess(acc?.idToken, null, getJwtExpirationTimeMs(acc?.idToken))
590
591
  } else {
591
592
  nativeOnRefreshError("network_error", task.exception?.message ?: "Silent sign-in failed")
592
593
  }
593
594
  }
594
595
  return
595
596
  }
596
- val refreshToken = getMicrosoftRefreshToken()
597
+ val refreshToken = inMemoryMicrosoftRefreshToken
597
598
  if (refreshToken != null) {
598
599
  refreshMicrosoftTokenForRefresh(ctx, refreshToken)
599
600
  return
@@ -604,37 +605,34 @@ object AuthAdapter {
604
605
  @JvmStatic
605
606
  fun hasPlayServices(context: Context): Boolean {
606
607
  val ctx = context.applicationContext ?: appContext ?: return false
607
- val availability = GoogleApiAvailability.getInstance()
608
- val result = availability.isGooglePlayServicesAvailable(ctx)
609
- return result == ConnectionResult.SUCCESS
608
+ return GoogleApiAvailability.getInstance()
609
+ .isGooglePlayServicesAvailable(ctx) == ConnectionResult.SUCCESS
610
610
  }
611
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")
612
615
  @JvmStatic
613
616
  fun logoutSync(context: Context) {
614
617
  val ctx = appContext ?: context.applicationContext
615
618
  val clientId = getClientIdFromResources(ctx)
616
619
  if (clientId != null) {
617
620
  val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
618
- .requestIdToken(clientId)
619
- .requestServerAuthCode(clientId)
620
- .requestEmail()
621
- .build()
621
+ .requestIdToken(clientId).requestServerAuthCode(clientId).requestEmail().build()
622
622
  GoogleSignIn.getClient(ctx, gso).signOut()
623
623
  }
624
624
  inMemoryMicrosoftRefreshToken = null
625
625
  inMemoryMicrosoftScopes = listOf("openid", "email", "profile", "offline_access", "User.Read")
626
626
  }
627
627
 
628
+ @Suppress("DEPRECATION")
628
629
  @JvmStatic
629
630
  fun revokeAccessSync(context: Context) {
630
631
  val ctx = appContext ?: context.applicationContext
631
632
  val clientId = getClientIdFromResources(ctx)
632
633
  if (clientId != null) {
633
634
  val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
634
- .requestIdToken(clientId)
635
- .requestServerAuthCode(clientId)
636
- .requestEmail()
637
- .build()
635
+ .requestIdToken(clientId).requestServerAuthCode(clientId).requestEmail().build()
638
636
  GoogleSignIn.getClient(ctx, gso).revokeAccess()
639
637
  }
640
638
  inMemoryMicrosoftRefreshToken = null
@@ -649,14 +647,15 @@ object AuthAdapter {
649
647
  @JvmStatic
650
648
  fun restoreSession(context: Context) {
651
649
  val ctx = context.applicationContext ?: appContext ?: context
650
+ @Suppress("DEPRECATION")
652
651
  val account = GoogleSignIn.getLastSignedInAccount(ctx)
653
652
  if (account != null) {
654
653
  val expirationTime = getJwtExpirationTimeMs(account.idToken)
655
654
  nativeOnLoginSuccess("google", account.email, account.displayName,
656
- account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
657
- account.grantedScopes?.map { it.scopeUri }?.toTypedArray(), expirationTime)
655
+ account.photoUrl?.toString(), account.idToken, null, account.serverAuthCode,
656
+ account.grantedScopes?.map { it.scopeUri }?.toTypedArray(), expirationTime)
658
657
  } else {
659
- val refreshToken = getMicrosoftRefreshToken()
658
+ val refreshToken = inMemoryMicrosoftRefreshToken
660
659
  if (refreshToken != null) {
661
660
  refreshMicrosoftToken(ctx, refreshToken)
662
661
  } else {
@@ -669,7 +668,7 @@ object AuthAdapter {
669
668
  val clientId = getMicrosoftClientIdFromResources(context)
670
669
  val tenant = getMicrosoftTenantFromResources(context) ?: "common"
671
670
  val b2cDomain = getMicrosoftB2cDomainFromResources(context)
672
-
671
+
673
672
  if (clientId == null) {
674
673
  nativeOnLoginError("configuration_error", "Microsoft Client ID is required for refresh")
675
674
  return
@@ -678,7 +677,7 @@ object AuthAdapter {
678
677
  val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
679
678
  val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
680
679
 
681
- CoroutineScope(Dispatchers.IO).launch {
680
+ moduleScope.launch {
682
681
  try {
683
682
  val url = java.net.URL(tokenUrl)
684
683
  val connection = url.openConnection() as java.net.HttpURLConnection
@@ -691,7 +690,6 @@ object AuthAdapter {
691
690
  append("&grant_type=refresh_token")
692
691
  append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
693
692
  }
694
-
695
693
  connection.outputStream.use { it.write(postData.toByteArray()) }
696
694
 
697
695
  val responseCode = connection.responseCode
@@ -701,7 +699,7 @@ object AuthAdapter {
701
699
  connection.errorStream?.bufferedReader()?.readText() ?: ""
702
700
  }
703
701
 
704
- CoroutineScope(Dispatchers.Main).launch {
702
+ withContext(Dispatchers.Main) {
705
703
  if (responseCode == 200) {
706
704
  val json = JSONObject(responseBody)
707
705
  val newIdToken = json.optString("id_token")
@@ -709,25 +707,23 @@ object AuthAdapter {
709
707
  val newRefreshToken = json.optString("refresh_token")
710
708
  val expiresIn = json.optLong("expires_in", 0)
711
709
  val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
712
-
713
710
  val claims = decodeJwt(newIdToken)
714
- val email = claims["preferred_username"] ?: claims["email"]
715
- val name = claims["name"]
716
711
 
717
- if (newRefreshToken.isNotEmpty()) {
718
- saveMicrosoftRefreshToken(newRefreshToken)
719
- }
712
+ if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
720
713
  inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
721
714
  listOf("openid", "email", "profile", "offline_access", "User.Read")
722
715
  }
723
716
 
724
- 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)
725
721
  } else {
726
722
  nativeOnLoginError("refresh_failed", "Microsoft token refresh failed")
727
723
  }
728
724
  }
729
725
  } catch (e: Exception) {
730
- CoroutineScope(Dispatchers.Main).launch {
726
+ withContext(Dispatchers.Main) {
731
727
  nativeOnLoginError("network_error", e.message)
732
728
  }
733
729
  }
@@ -738,32 +734,38 @@ object AuthAdapter {
738
734
  val clientId = getMicrosoftClientIdFromResources(context)
739
735
  val tenant = getMicrosoftTenantFromResources(context) ?: "common"
740
736
  val b2cDomain = getMicrosoftB2cDomainFromResources(context)
737
+
741
738
  if (clientId == null) {
742
739
  nativeOnRefreshError("configuration_error", "Microsoft Client ID not configured")
743
740
  return
744
741
  }
742
+
745
743
  val authBaseUrl = getMicrosoftAuthBaseUrl(tenant, b2cDomain)
746
744
  val tokenUrl = "${authBaseUrl}oauth2/v2.0/token"
747
- CoroutineScope(Dispatchers.IO).launch {
745
+
746
+ moduleScope.launch {
748
747
  try {
749
748
  val url = java.net.URL(tokenUrl)
750
749
  val connection = url.openConnection() as java.net.HttpURLConnection
751
750
  connection.requestMethod = "POST"
752
751
  connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
753
752
  connection.doOutput = true
753
+
754
754
  val postData = buildString {
755
755
  append("client_id=${java.net.URLEncoder.encode(clientId, "UTF-8")}")
756
756
  append("&grant_type=refresh_token")
757
757
  append("&refresh_token=${java.net.URLEncoder.encode(refreshToken, "UTF-8")}")
758
758
  }
759
759
  connection.outputStream.use { it.write(postData.toByteArray()) }
760
+
760
761
  val responseCode = connection.responseCode
761
762
  val responseBody = if (responseCode == 200) {
762
763
  connection.inputStream.bufferedReader().readText()
763
764
  } else {
764
765
  connection.errorStream?.bufferedReader()?.readText() ?: ""
765
766
  }
766
- CoroutineScope(Dispatchers.Main).launch {
767
+
768
+ withContext(Dispatchers.Main) {
767
769
  if (responseCode == 200) {
768
770
  val json = JSONObject(responseBody)
769
771
  val newIdToken = json.optString("id_token")
@@ -771,15 +773,12 @@ object AuthAdapter {
771
773
  val newRefreshToken = json.optString("refresh_token")
772
774
  val expiresIn = json.optLong("expires_in", 0)
773
775
  val expirationTime = if (expiresIn > 0) System.currentTimeMillis() + expiresIn * 1000 else null
774
- val claims = decodeJwt(newIdToken)
775
- val email = claims["preferred_username"] ?: claims["email"]
776
- val name = claims["name"]
777
- if (newRefreshToken.isNotEmpty()) {
778
- saveMicrosoftRefreshToken(newRefreshToken)
779
- }
776
+
777
+ if (newRefreshToken.isNotEmpty()) inMemoryMicrosoftRefreshToken = newRefreshToken
780
778
  inMemoryMicrosoftScopes = pendingMicrosoftScopes.ifEmpty {
781
779
  listOf("openid", "email", "profile", "offline_access", "User.Read")
782
780
  }
781
+
783
782
  nativeOnRefreshSuccess(
784
783
  newIdToken.ifEmpty { null },
785
784
  newAccessToken.ifEmpty { null },
@@ -790,11 +789,10 @@ object AuthAdapter {
790
789
  }
791
790
  }
792
791
  } catch (e: Exception) {
793
- CoroutineScope(Dispatchers.Main).launch {
792
+ withContext(Dispatchers.Main) {
794
793
  nativeOnRefreshError("network_error", e.message)
795
794
  }
796
795
  }
797
796
  }
798
797
  }
799
-
800
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
  }