Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/android-native-client-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": patch
---

Fix native component auth state when syncing JS and native client changes.
5 changes: 5 additions & 0 deletions .changeset/great-rivers-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": patch
---

Fix Android native component initialization when the Expo native module does not expose React Native event listener bookkeeping methods, and make the generated iOS bridge compatible with Swift's explicit import visibility checks.
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,13 @@ class ClerkExpoModule : Module() {
getClientToken(promise)
}

AsyncFunction("syncFromJsClientToken") { clientToken: String?, sourceId: String?, promise: Promise ->
syncFromJsClientToken(clientToken, sourceId, promise)
AsyncFunction("syncFromJsClientToken") { clientToken: String?, sourceId: String?, shouldRefreshClient: Boolean?, promise: Promise ->
syncFromJsClientToken(
clientToken,
sourceId,
shouldRefreshClient ?: clientToken.isNullOrBlank(),
promise
)
}
}

Expand Down Expand Up @@ -169,7 +174,7 @@ class ClerkExpoModule : Module() {
// before resolving the configure call.
if (!bearerToken.isNullOrEmpty()) {
withTimeout(5_000L) {
Clerk.sessionFlow.first { it != null }
Clerk.clientFlow.first { it != null }
}
}
} catch (e: TimeoutCancellationException) {
Expand Down Expand Up @@ -230,10 +235,10 @@ class ClerkExpoModule : Module() {

try {
withTimeout(5_000L) {
Clerk.sessionFlow.first { it != null }
Clerk.clientFlow.first { it != null }
}
} catch (_: TimeoutCancellationException) {
debugLog(TAG, "configure - session did not appear after reconfigure token update")
debugLog(TAG, "configure - client did not appear after reconfigure token update")
}
}

Expand All @@ -251,13 +256,13 @@ class ClerkExpoModule : Module() {
debugLog(TAG, "configure - updateDeviceToken failed: ${result.error}")
}

// Wait for session to appear with the new token (up to 5s)
// Wait for client state to hydrate with the new token (up to 5s).
try {
withTimeout(5_000L) {
Clerk.sessionFlow.first { it != null }
Clerk.clientFlow.first { it != null }
}
} catch (_: TimeoutCancellationException) {
debugLog(TAG, "configure - session did not appear after token update")
debugLog(TAG, "configure - client did not appear after token update")
}
}

Expand Down Expand Up @@ -285,7 +290,12 @@ class ClerkExpoModule : Module() {

// MARK: - syncFromJsClientToken

private fun syncFromJsClientToken(clientToken: String?, sourceId: String?, promise: Promise) {
private fun syncFromJsClientToken(
clientToken: String?,
sourceId: String?,
shouldRefreshClient: Boolean,
promise: Promise
) {
if (!Clerk.isInitialized.value) {
promise.resolve(null)
return
Expand All @@ -295,43 +305,65 @@ class ClerkExpoModule : Module() {
try {
jsOriginatedClientSyncDepth += 1
if (!clientToken.isNullOrBlank()) {
when (val result = Clerk.updateDeviceToken(clientToken)) {
val currentDeviceToken = try {
Clerk.getDeviceToken()
} catch (_: Exception) {
null
}

if (currentDeviceToken == clientToken) {
if (!shouldRefreshClient) {
emitSyncedClientChanged(sourceId)
promise.resolve(null)
return@launch
}
} else {
when (val result = Clerk.updateDeviceToken(clientToken)) {
is ClerkResult.Failure -> {
promise.reject(
"E_SYNC_FROM_JS_FAILED",
result.error?.firstMessage() ?: result.throwable?.message ?: "Client token sync failed",
null
)
return@launch
}
is ClerkResult.Success -> {
try {
withTimeout(5_000L) {
Clerk.clientFlow.first { it != null }
}
} catch (_: TimeoutCancellationException) {
debugLog(TAG, "syncFromJsClientToken - client did not appear after token update")
}
if (!shouldRefreshClient) {
emitSyncedClientChanged(sourceId)
promise.resolve(null)
return@launch
}
}
}
}
}

if (shouldRefreshClient) {
when (val result = Clerk.refreshClient()) {
is ClerkResult.Failure -> {
promise.reject(
"E_SYNC_FROM_JS_FAILED",
result.error?.firstMessage() ?: result.throwable?.message ?: "Client token sync failed",
result.error?.firstMessage() ?: result.throwable?.message ?: "Client refresh failed",
null
)
return@launch
}
is ClerkResult.Success -> {
try {
withTimeout(5_000L) {
Clerk.sessionFlow.first { it != null }
}
} catch (_: TimeoutCancellationException) {
debugLog(TAG, "syncFromJsClientToken - session did not appear after token update")
}
emitSyncedClientChanged(sourceId)
promise.resolve(null)
return@launch
}
}
return@launch
}

when (val result = Clerk.refreshClient()) {
is ClerkResult.Failure -> {
promise.reject(
"E_SYNC_FROM_JS_FAILED",
result.error?.firstMessage() ?: result.throwable?.message ?: "Client refresh failed",
null
)
}
is ClerkResult.Success -> {
emitSyncedClientChanged(sourceId)
promise.resolve(null)
}
}
emitSyncedClientChanged(sourceId)
promise.resolve(null)
} catch (e: Exception) {
promise.reject("E_SYNC_FROM_JS_FAILED", e.message ?: "Client token sync failed", e)
} finally {
Expand Down
1 change: 1 addition & 0 deletions packages/expo/ios/ClerkExpoModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ @interface RCT_EXTERN_MODULE(ClerkExpo, RCTEventEmitter)

RCT_EXTERN_METHOD(syncFromJsClientToken:(id)clientToken
sourceId:(id)sourceId
shouldRefreshClient:(id)shouldRefreshClient
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

Expand Down
11 changes: 9 additions & 2 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public protocol ClerkNativeBridgeProtocol {
// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?) async throws
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
Expand Down Expand Up @@ -154,6 +154,7 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func syncFromJsClientToken(_ clientToken: Any?,
sourceId: Any?,
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
Expand All @@ -163,9 +164,15 @@ class ClerkExpoModule: RCTEventEmitter {

let normalizedClientToken = clientToken as? String
let normalizedSourceId = sourceId as? String
let defaultShouldRefreshClient = normalizedClientToken?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true
let normalizedShouldRefreshClient = (shouldRefreshClient as? Bool) ?? defaultShouldRefreshClient
Task {
do {
try await bridge.syncFromJsClientToken(normalizedClientToken, sourceId: normalizedSourceId)
try await bridge.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
)
resolve(nil)
} catch {
reject("E_SYNC_FROM_JS_FAILED", error.localizedDescription, error)
Expand Down
58 changes: 26 additions & 32 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {

// MARK: - Native Bridge Implementation

public final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
public static let shared = ClerkNativeBridge()
final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
static let shared = ClerkNativeBridge()

private static let clerkLoadMaxAttempts = 30
private static let clerkLoadIntervalNs: UInt64 = 100_000_000
Expand Down Expand Up @@ -53,30 +53,30 @@ public final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

// Register this app-target bridge with the ClerkExpo module.
@MainActor public static func register() {
@MainActor static func register() {
shared.loadThemes()
clerkNativeBridge = shared
}

@MainActor
public func configure(publishableKey: String, bearerToken: String? = nil) async throws {
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Self.configuredPublishableKey = publishableKey
startClientObserver(reset: true)

let shouldWaitForSession = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedSessionIfNeeded(shouldWaitForSession)
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
return
}

if Self.clerkConfigured {
startClientObserver()
let shouldWaitForSession = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedSessionIfNeeded(shouldWaitForSession)
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
return
}
Expand All @@ -86,8 +86,8 @@ public final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
Clerk.configure(publishableKey: publishableKey, options: Self.makeClerkOptions())
startClientObserver()

let shouldWaitForSession = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedSessionIfNeeded(shouldWaitForSession)
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
}
Expand Down Expand Up @@ -175,20 +175,10 @@ public final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return .init(keychainConfig: .init(service: service), middleware: middleware)
}

@MainActor
private static func waitForLoadedSession() async {
// Wait for Clerk to finish loading (cached data + API refresh).
// The static configure() fires off async refreshes; poll until loaded.
for _ in 0..<clerkLoadMaxAttempts {
if Clerk.shared.isLoaded && Clerk.shared.session != nil && Clerk.shared.user != nil {
return
}
try? await Task.sleep(nanoseconds: clerkLoadIntervalNs)
}
}

@MainActor
private static func waitForLoadedClient() async {
// Wait for Clerk to finish loading client state from cached data + API refresh.
// The bridge sync contract is client-token based, not session based.
for _ in 0..<clerkLoadMaxAttempts {
if Clerk.shared.isLoaded {
return
Expand All @@ -198,20 +188,20 @@ public final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

@MainActor
private static func waitForLoadedSessionIfNeeded(_ shouldWait: Bool) async {
private static func waitForLoadedClientIfNeeded(_ shouldWait: Bool) async {
guard shouldWait else { return }
await waitForLoadedSession()
await waitForLoadedClient()
}

@MainActor
public func getClientToken() async -> String? {
func getClientToken() async -> String? {
guard Self.clerkConfigured else { return nil }
return Clerk.shared.deviceToken
}

// MARK: - Inline View Creation

public func makeAuthViewController(
func makeAuthViewController(
mode: String,
dismissible: Bool,
onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void
Expand All @@ -229,7 +219,7 @@ public final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
)
}

public func makeUserProfileViewController(
func makeUserProfileViewController(
dismissible: Bool,
onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void
) -> UIViewController? {
Expand All @@ -245,7 +235,7 @@ public final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
)
}

public func makeUserButtonViewController() -> UIViewController? {
func makeUserButtonViewController() -> UIViewController? {
guard Self.clerkConfigured else { return nil }

return makeHostingController(
Expand All @@ -257,13 +247,17 @@ public final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

@MainActor
public func syncFromJsClientToken(_ clientToken: String?, sourceId: String?) async throws {
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws {
guard Self.clerkConfigured else { return }

if let token = clientToken?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty {
_ = try await Clerk.shared.updateDeviceToken(token)
await Self.waitForLoadedSession()
} else {
if Clerk.shared.deviceToken != token {
_ = try await Clerk.shared.updateDeviceToken(token)
await Self.waitForLoadedClient()
}
}

if shouldRefreshClient {
_ = try await Clerk.shared.refreshClient()
await Self.waitForLoadedClient()
}
Expand Down
Loading
Loading