Skip to content

Commit 7d7d425

Browse files
authored
revert(expo): two-way JS/native session sync (#8032) (#8065)
1 parent 617896c commit 7d7d425

10 files changed

Lines changed: 252 additions & 587 deletions

File tree

.changeset/native-session-sync-fixes.md

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/expo": patch
3+
---
4+
5+
Revert two-way JS/native session sync (#8032)

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.9"
21+
clerkAndroidApiVersion = "1.0.6"
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: 37 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ 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
98
import com.facebook.react.bridge.ActivityEventListener
109
import com.facebook.react.bridge.Promise
1110
import com.facebook.react.bridge.ReactApplicationContext
@@ -68,63 +67,41 @@ class ClerkExpoModule(reactContext: ReactApplicationContext) :
6867
try {
6968
publishableKey = pubKey
7069

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-
}
70+
// If the JS SDK has a bearer token, write it to the native SDK's
71+
// SharedPreferences so both SDKs share the same Clerk API client.
72+
if (!bearerToken.isNullOrEmpty()) {
73+
reactApplicationContext.getSharedPreferences("clerk_preferences", Context.MODE_PRIVATE)
74+
.edit()
75+
.putString("DEVICE_TOKEN", bearerToken)
76+
.apply()
77+
debugLog(TAG, "configure - wrote JS bearer token to native SharedPreferences")
78+
}
8079

81-
Clerk.initialize(reactApplicationContext, pubKey)
82-
83-
// Wait for initialization to complete with timeout
84-
try {
85-
withTimeout(10_000L) {
86-
Clerk.isInitialized.first { it }
87-
}
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
97-
}
80+
Clerk.initialize(reactApplicationContext, pubKey)
9881

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}")
82+
// Wait for initialization to complete with timeout
83+
try {
84+
withTimeout(10_000L) {
85+
Clerk.isInitialized.first { it }
86+
}
87+
} catch (e: TimeoutCancellationException) {
88+
val initError = Clerk.initializationError.value
89+
val message = if (initError != null) {
90+
"Clerk initialization timed out: ${initError.message}"
10391
} else {
104-
promise.resolve(null)
92+
"Clerk initialization timed out after 10 seconds"
10593
}
94+
promise.reject("E_TIMEOUT", message)
10695
return@launch
10796
}
10897

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}")
115-
}
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")
124-
}
98+
// Check for initialization errors
99+
val error = Clerk.initializationError.value
100+
if (error != null) {
101+
promise.reject("E_INIT_FAILED", "Failed to initialize Clerk SDK: ${error.message}")
102+
} else {
103+
promise.resolve(null)
125104
}
126-
127-
promise.resolve(null)
128105
} catch (e: Exception) {
129106
promise.reject("E_INIT_FAILED", "Failed to initialize Clerk SDK: ${e.message}", e)
130107
}
@@ -197,15 +174,15 @@ class ClerkExpoModule(reactContext: ReactApplicationContext) :
197174
@ReactMethod
198175
override fun getSession(promise: Promise) {
199176
if (!Clerk.isInitialized.value) {
200-
// Return null when not initialized (matches iOS behavior)
201-
// so callers can proceed to call configure() with a bearer token.
202-
promise.resolve(null)
177+
promise.reject("E_NOT_INITIALIZED", "Clerk SDK is not initialized. Call configure() first.")
203178
return
204179
}
205180

206181
val session = Clerk.session
207182
val user = Clerk.user
208183

184+
debugLog(TAG, "getSession - hasSession: ${session != null}, hasUser: ${user != null}")
185+
209186
val result = WritableNativeMap()
210187

211188
session?.let {
@@ -240,6 +217,7 @@ class ClerkExpoModule(reactContext: ReactApplicationContext) :
240217
try {
241218
val prefs = reactApplicationContext.getSharedPreferences("clerk_preferences", Context.MODE_PRIVATE)
242219
val deviceToken = prefs.getString("DEVICE_TOKEN", null)
220+
debugLog(TAG, "getClientToken - deviceToken: ${if (deviceToken != null) "found" else "null"}")
243221
promise.resolve(deviceToken)
244222
} catch (e: Exception) {
245223
debugLog(TAG, "getClientToken failed: ${e.message}")
@@ -252,8 +230,7 @@ class ClerkExpoModule(reactContext: ReactApplicationContext) :
252230
@ReactMethod
253231
override fun signOut(promise: Promise) {
254232
if (!Clerk.isInitialized.value) {
255-
// Resolve gracefully when not initialized (matches iOS behavior)
256-
promise.resolve(null)
233+
promise.reject("E_NOT_INITIALIZED", "Clerk SDK is not initialized. Call configure() first.")
257234
return
258235
}
259236

@@ -281,13 +258,17 @@ class ClerkExpoModule(reactContext: ReactApplicationContext) :
281258
}
282259

283260
private fun handleAuthResult(resultCode: Int, data: Intent?) {
261+
debugLog(TAG, "handleAuthResult - resultCode: $resultCode")
262+
284263
val promise = pendingAuthPromise ?: return
285264
pendingAuthPromise = null
286265

287266
if (resultCode == Activity.RESULT_OK) {
288267
val session = Clerk.session
289268
val user = Clerk.user
290269

270+
debugLog(TAG, "handleAuthResult - hasSession: ${session != null}, hasUser: ${user != null}")
271+
291272
val result = WritableNativeMap()
292273

293274
// Top-level sessionId for JS SDK compatibility (matches iOS response format)
@@ -315,6 +296,7 @@ class ClerkExpoModule(reactContext: ReactApplicationContext) :
315296

316297
promise.resolve(result)
317298
} else {
299+
debugLog(TAG, "handleAuthResult - user cancelled")
318300
val result = WritableNativeMap()
319301
result.putBoolean("cancelled", true)
320302
promise.resolve(result)

packages/expo/ios/ClerkExpoModule.swift

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ public protocol ClerkViewFactoryProtocol {
2222
// SDK operations
2323
func configure(publishableKey: String, bearerToken: String?) async throws
2424
func getSession() async -> [String: Any]?
25-
func getClientToken() -> String?
2625
func signOut() async throws
2726
}
2827

@@ -32,11 +31,9 @@ public protocol ClerkViewFactoryProtocol {
3231
class ClerkExpoModule: RCTEventEmitter {
3332

3433
private static var _hasListeners = false
35-
private static weak var sharedInstance: ClerkExpoModule?
3634

3735
override init() {
3836
super.init()
39-
ClerkExpoModule.sharedInstance = self
4037
}
4138

4239
@objc override static func requiresMainQueueSetup() -> Bool {
@@ -55,17 +52,6 @@ class ClerkExpoModule: RCTEventEmitter {
5552
ClerkExpoModule._hasListeners = false
5653
}
5754

58-
/// Emits an onAuthStateChange event to JS from anywhere in the native layer.
59-
/// Used by inline views (AuthView, UserProfileView) to notify ClerkProvider
60-
/// of auth state changes in addition to the view-level onAuthEvent callback.
61-
static func emitAuthStateChange(type: String, sessionId: String?) {
62-
guard _hasListeners, let instance = sharedInstance else { return }
63-
instance.sendEvent(withName: "onAuthStateChange", body: [
64-
"type": type,
65-
"sessionId": sessionId as Any,
66-
])
67-
}
68-
6955
/// Returns the topmost presented view controller, avoiding deprecated `keyWindow`.
7056
private static func topViewController() -> UIViewController? {
7157
guard let scene = UIApplication.shared.connectedScenes
@@ -188,12 +174,31 @@ class ClerkExpoModule: RCTEventEmitter {
188174

189175
@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
190176
reject: @escaping RCTPromiseRejectBlock) {
191-
guard let factory = clerkViewFactory else {
177+
// Use a custom keychain service if configured in Info.plist (for extension apps
178+
// sharing a keychain group). Falls back to the main bundle identifier.
179+
let keychainService: String = {
180+
if let custom = Bundle.main.object(forInfoDictionaryKey: "ClerkKeychainService") as? String, !custom.isEmpty {
181+
return custom
182+
}
183+
return Bundle.main.bundleIdentifier ?? ""
184+
}()
185+
186+
let query: [String: Any] = [
187+
kSecClass as String: kSecClassGenericPassword,
188+
kSecAttrService as String: keychainService,
189+
kSecAttrAccount as String: "clerkDeviceToken",
190+
kSecReturnData as String: true,
191+
kSecMatchLimit as String: kSecMatchLimitOne
192+
]
193+
194+
var result: AnyObject?
195+
let status = SecItemCopyMatching(query as CFDictionary, &result)
196+
197+
if status == errSecSuccess, let data = result as? Data {
198+
resolve(String(data: data, encoding: .utf8))
199+
} else {
192200
resolve(nil)
193-
return
194201
}
195-
196-
resolve(factory.getClientToken())
197202
}
198203

199204
// MARK: - signOut
@@ -272,12 +277,6 @@ public class ClerkAuthNativeView: UIView {
272277
let jsonData = (try? JSONSerialization.data(withJSONObject: data)) ?? Data()
273278
let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}"
274279
self?.onAuthEvent?(["type": eventName, "data": jsonString])
275-
276-
// Also emit module-level event so ClerkProvider's useNativeAuthEvents picks it up
277-
if eventName == "signInCompleted" || eventName == "signUpCompleted" {
278-
let sessionId = data["sessionId"] as? String
279-
ClerkExpoModule.emitAuthStateChange(type: "signedIn", sessionId: sessionId)
280-
}
281280
}
282281
) else { return }
283282

@@ -360,12 +359,6 @@ public class ClerkUserProfileNativeView: UIView {
360359
let jsonData = (try? JSONSerialization.data(withJSONObject: data)) ?? Data()
361360
let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}"
362361
self?.onProfileEvent?(["type": eventName, "data": jsonString])
363-
364-
// Also emit module-level event for sign-out detection
365-
if eventName == "signedOut" {
366-
let sessionId = data["sessionId"] as? String
367-
ClerkExpoModule.emitAuthStateChange(type: "signedOut", sessionId: sessionId)
368-
}
369362
}
370363
) else { return }
371364

0 commit comments

Comments
 (0)