Skip to content

Commit 4f8bd2d

Browse files
committed
fix(expo): replace reflection hack with Clerk.updateDeviceToken() API
Use the new public updateDeviceToken() method from clerk-android 1.0.9 instead of using reflection to reset ConfigurationManager._isInitialized. This properly saves the token and triggers a client/environment refresh.
1 parent 118eb56 commit 4f8bd2d

2 files changed

Lines changed: 46 additions & 91 deletions

File tree

packages/expo/android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ext {
1818
credentialsVersion = "1.3.0"
1919
googleIdVersion = "1.1.1"
2020
kotlinxCoroutinesVersion = "1.7.3"
21-
clerkAndroidApiVersion = "1.0.6"
21+
clerkAndroidApiVersion = "1.0.9"
2222
clerkAndroidUiVersion = "1.0.9"
2323
composeVersion = "1.7.0"
2424
activityComposeVersion = "1.9.0"

packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt

Lines changed: 45 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.content.Context
55
import android.content.Intent
66
import android.util.Log
77
import com.clerk.api.Clerk
8+
import com.clerk.api.network.serialization.ClerkResult
89
import com.facebook.react.bridge.ActivityEventListener
910
import com.facebook.react.bridge.Promise
1011
import com.facebook.react.bridge.ReactApplicationContext
@@ -14,7 +15,6 @@ import com.facebook.react.bridge.WritableNativeMap
1415
import kotlinx.coroutines.CoroutineScope
1516
import kotlinx.coroutines.Dispatchers
1617
import kotlinx.coroutines.TimeoutCancellationException
17-
import kotlinx.coroutines.flow.MutableStateFlow
1818
import kotlinx.coroutines.flow.first
1919
import kotlinx.coroutines.launch
2020
import kotlinx.coroutines.withTimeout
@@ -68,114 +68,69 @@ class ClerkExpoModule(reactContext: ReactApplicationContext) :
6868
try {
6969
publishableKey = pubKey
7070

71-
// If the JS SDK has a bearer token, write it to the native SDK's
72-
// SharedPreferences so both SDKs share the same Clerk API client.
73-
if (!bearerToken.isNullOrEmpty()) {
74-
reactApplicationContext.getSharedPreferences("clerk_preferences", Context.MODE_PRIVATE)
75-
.edit()
76-
.putString("DEVICE_TOKEN", bearerToken)
77-
.apply()
78-
}
71+
if (!Clerk.isInitialized.value) {
72+
// First-time initialization — write the bearer token to SharedPreferences
73+
// before initializing so the SDK boots with the correct client.
74+
if (!bearerToken.isNullOrEmpty()) {
75+
reactApplicationContext.getSharedPreferences("clerk_preferences", Context.MODE_PRIVATE)
76+
.edit()
77+
.putString("DEVICE_TOKEN", bearerToken)
78+
.apply()
79+
}
7980

80-
if (Clerk.isInitialized.value) {
81-
// Already initialized — force a client refresh so the SDK
82-
// picks up the new device token from SharedPreferences.
83-
forceClientRefresh()
81+
Clerk.initialize(reactApplicationContext, pubKey)
8482

85-
// Wait for session to appear with the new token (up to 5s)
83+
// Wait for initialization to complete with timeout
8684
try {
87-
withTimeout(5_000L) {
88-
Clerk.sessionFlow.first { it != null }
85+
withTimeout(10_000L) {
86+
Clerk.isInitialized.first { it }
8987
}
90-
} catch (_: TimeoutCancellationException) {
91-
debugLog(TAG, "configure - session did not appear after force refresh")
88+
} catch (e: TimeoutCancellationException) {
89+
val initError = Clerk.initializationError.value
90+
val message = if (initError != null) {
91+
"Clerk initialization timed out: ${initError.message}"
92+
} else {
93+
"Clerk initialization timed out after 10 seconds"
94+
}
95+
promise.reject("E_TIMEOUT", message)
96+
return@launch
9297
}
9398

94-
promise.resolve(null)
99+
// Check for initialization errors
100+
val error = Clerk.initializationError.value
101+
if (error != null) {
102+
promise.reject("E_INIT_FAILED", "Failed to initialize Clerk SDK: ${error.message}")
103+
} else {
104+
promise.resolve(null)
105+
}
95106
return@launch
96107
}
97108

98-
// First-time initialization
99-
Clerk.initialize(reactApplicationContext, pubKey)
100-
101-
// Wait for initialization to complete with timeout
102-
try {
103-
withTimeout(10_000L) {
104-
Clerk.isInitialized.first { it }
109+
// Already initialized — use the public SDK API to update
110+
// the device token and trigger a client/environment refresh.
111+
if (!bearerToken.isNullOrEmpty()) {
112+
val result = Clerk.updateDeviceToken(bearerToken)
113+
if (result is ClerkResult.Failure) {
114+
debugLog(TAG, "configure - updateDeviceToken failed: ${result.error}")
105115
}
106-
} catch (e: TimeoutCancellationException) {
107-
val initError = Clerk.initializationError.value
108-
val message = if (initError != null) {
109-
"Clerk initialization timed out: ${initError.message}"
110-
} else {
111-
"Clerk initialization timed out after 10 seconds"
116+
117+
// Wait for session to appear with the new token (up to 5s)
118+
try {
119+
withTimeout(5_000L) {
120+
Clerk.sessionFlow.first { it != null }
121+
}
122+
} catch (_: TimeoutCancellationException) {
123+
debugLog(TAG, "configure - session did not appear after token update")
112124
}
113-
promise.reject("E_TIMEOUT", message)
114-
return@launch
115125
}
116126

117-
// Check for initialization errors
118-
val error = Clerk.initializationError.value
119-
if (error != null) {
120-
promise.reject("E_INIT_FAILED", "Failed to initialize Clerk SDK: ${error.message}")
121-
} else {
122-
promise.resolve(null)
123-
}
127+
promise.resolve(null)
124128
} catch (e: Exception) {
125129
promise.reject("E_INIT_FAILED", "Failed to initialize Clerk SDK: ${e.message}", e)
126130
}
127131
}
128132
}
129133

130-
/**
131-
* Forces the Clerk SDK to re-fetch client/environment data from the API.
132-
*
133-
* This is needed when a new device token has been written to SharedPreferences
134-
* but the SDK was already initialized (so Clerk.initialize() is a no-op).
135-
*
136-
* Uses reflection to find the ConfigurationManager instance by type (field name
137-
* may vary across SDK versions), then sets _isInitialized to false so
138-
* reinitialize() proceeds with a fresh client/environment fetch.
139-
*/
140-
private fun forceClientRefresh() {
141-
try {
142-
// Find the ConfigurationManager field by type since the name may differ
143-
val clerkClass = Clerk::class.java
144-
var configManager: Any? = null
145-
146-
for (field in clerkClass.declaredFields) {
147-
field.isAccessible = true
148-
val fieldValue = field.get(Clerk)
149-
if (fieldValue != null && fieldValue.javaClass.name.contains("ConfigurationManager")) {
150-
configManager = fieldValue
151-
break
152-
}
153-
}
154-
155-
if (configManager == null) {
156-
debugLog(TAG, "forceClientRefresh - ConfigurationManager not found")
157-
return
158-
}
159-
160-
// Find _isInitialized field (MutableStateFlow<Boolean>) in ConfigurationManager
161-
// and set it to false so reinitialize() will proceed
162-
for (field in configManager.javaClass.declaredFields) {
163-
field.isAccessible = true
164-
val fieldValue = field.get(configManager)
165-
if (fieldValue is MutableStateFlow<*> && fieldValue.value is Boolean && fieldValue.value == true) {
166-
@Suppress("UNCHECKED_CAST")
167-
(fieldValue as MutableStateFlow<Boolean>).value = false
168-
Clerk.reinitialize()
169-
return
170-
}
171-
}
172-
173-
debugLog(TAG, "forceClientRefresh - _isInitialized flow not found")
174-
} catch (e: Exception) {
175-
debugLog(TAG, "forceClientRefresh failed: ${e.message}")
176-
}
177-
}
178-
179134
// MARK: - presentAuth
180135

181136
@ReactMethod

0 commit comments

Comments
 (0)