diff --git a/.changeset/remove-legacy-expo-native-sync.md b/.changeset/remove-legacy-expo-native-sync.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/remove-legacy-expo-native-sync.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt index 8d5d9e49506..b72c1801e40 100644 --- a/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt +++ b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt @@ -64,7 +64,7 @@ class ClerkAuthNativeView(context: Context, appContext: AppContext) : ClerkCompo private val onHostBack by EventDispatcher() init { - // At cold start, ClerkExpoModule.configure() may run before React's + // At cold start, ClerkExpoModule.configureNative() may run before React's // host-resume sync, so this view's construction is a reliable second hook. activity?.let { Clerk.attachActivity(it) } } diff --git a/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt index 84ec5128587..93c1030fcb5 100644 --- a/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt +++ b/packages/expo-native-components/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt @@ -32,20 +32,16 @@ import expo.modules.kotlin.modules.ModuleDefinition import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import org.json.JSONObject private const val TAG = "ClerkExpoModule" private const val NATIVE_AUTH_FLOW_CHANGED_EVENT = "clerkNativeAuthFlowChanged" -private const val NATIVE_CLIENT_CHANGED_EVENT = "clerkNativeClientChanged" private const val NATIVE_CLIENT_INVALIDATED_EVENT = "clerkNativeClientInvalidated" private const val HOST_SDK_HEADER = "x-clerk-host-sdk" private const val HOST_SDK_VERSION_HEADER = "x-clerk-host-sdk-version" @@ -204,9 +200,6 @@ internal fun biometricCredentialBridgeError( class ClerkExpoModule : Module() { private val coroutineScope = CoroutineScope(Dispatchers.Main) private var authFlowStateObserverJob: Job? = null - private var clientStateObserverJob: Job? = null - private var lastObservedClientState: ClientStateSnapshot? = null - private var jsOriginatedClientSyncDepth = 0 private var configuredPublishableKey: String? = null private val mainHandler = Handler(Looper.getMainLooper()) private var clientInvalidationObserverJob: Job? = null @@ -220,62 +213,23 @@ class ClerkExpoModule : Module() { val isAuthFlowComplete: Boolean ) - private data class ClientStateSnapshot( - val client: Client?, - val deviceToken: String? - ) - - private data class ClientStateChanges( - val client: Boolean, - val deviceToken: Boolean - ) - - companion object { - private var sharedInstance: ClerkExpoModule? = null - - fun emitClientChanged(sourceId: String? = null) { - val instance = sharedInstance ?: return - instance.sendEvent( - NATIVE_CLIENT_CHANGED_EVENT, - instance.clientChangedPayload( - sourceId = sourceId, - changes = ClientStateChanges(client = true, deviceToken = true) - ) - ) - } - } - override fun definition() = ModuleDefinition { Name("ClerkExpo") - Events(NATIVE_AUTH_FLOW_CHANGED_EVENT, NATIVE_CLIENT_CHANGED_EVENT, NATIVE_CLIENT_INVALIDATED_EVENT) + Events(NATIVE_AUTH_FLOW_CHANGED_EVENT, NATIVE_CLIENT_INVALIDATED_EVENT) OnCreate { - sharedInstance = this@ClerkExpoModule startAuthFlowStateObserver() } OnDestroy { - if (sharedInstance === this@ClerkExpoModule) { - sharedInstance = null - } authFlowStateObserverJob?.cancel() authFlowStateObserverJob = null - clientStateObserverJob?.cancel() - clientStateObserverJob = null clientInvalidationObserverJob?.cancel() clientInvalidationObserverJob = null mainHandler.removeCallbacksAndMessages(null) } - AsyncFunction("configure") { pubKey: String, bearerToken: String?, promise: Promise -> - configure(pubKey, bearerToken, promise) - } - - AsyncFunction("getClientToken") { promise: Promise -> - getClientToken(promise) - } - AsyncFunction("configureNative") { pubKey: String, seedDeviceToken: String?, promise: Promise -> configureNative(pubKey, seedDeviceToken, promise) } @@ -296,21 +250,6 @@ class ClerkExpoModule : Module() { promise.resolve(authFlowStatePayload()) } - AsyncFunction("syncClientStateFromJs") { - deviceToken: String?, - sourceId: String?, - didChangeClient: Boolean, - didChangeDeviceToken: Boolean, - promise: Promise -> - syncClientStateFromJs( - deviceToken, - sourceId, - didChangeClient, - didChangeDeviceToken, - promise - ) - } - AsyncFunction("getTrustedDeviceAvailability") { id: String?, identifierHint: String?, @@ -401,249 +340,6 @@ class ClerkExpoModule : Module() { ) } - private fun startClientStateObserver() { - if (clientStateObserverJob != null) { - return - } - - lastObservedClientState = clientStateSnapshot() - - clientStateObserverJob = coroutineScope.launch { - Clerk.clientFlow.collect { client -> - val previousClientState = lastObservedClientState - val newClientState = clientStateSnapshot(client) - - if (newClientState == previousClientState) { - return@collect - } - - lastObservedClientState = newClientState - if (jsOriginatedClientSyncDepth > 0) { - return@collect - } - - sendEvent( - NATIVE_CLIENT_CHANGED_EVENT, - clientChangedPayload( - deviceToken = newClientState.deviceToken, - changes = ClientStateChanges( - client = newClientState.client != previousClientState?.client, - deviceToken = newClientState.deviceToken != previousClientState?.deviceToken - ) - ) - ) - } - } - } - - private fun clientStateSnapshot(client: Client? = Clerk.clientFlow.value): ClientStateSnapshot { - return ClientStateSnapshot( - client = client, - deviceToken = try { - Clerk.getDeviceToken() - } catch (e: Exception) { - debugLog(TAG, "clientStateSnapshot - getDeviceToken failed: ${e.message}") - null - } - ) - } - - private fun clientChangedPayload( - sourceId: String? = null, - changes: ClientStateChanges, - deviceToken: String? = clientStateSnapshot().deviceToken - ): Map { - val result = mutableMapOf( - "changed" to mapOf( - "client" to changes.client, - "deviceToken" to changes.deviceToken - ), - "deviceToken" to deviceToken - ) - if (!sourceId.isNullOrEmpty()) { - result["sourceId"] = sourceId - } - return result - } - - private fun emitSyncedClientChanged( - sourceId: String?, - changes: ClientStateChanges, - snapshot: ClientStateSnapshot = clientStateSnapshot() - ) { - lastObservedClientState = snapshot - sendEvent( - NATIVE_CLIENT_CHANGED_EVENT, - clientChangedPayload( - sourceId = sourceId, - changes = changes, - deviceToken = snapshot.deviceToken - ) - ) - } - - // MARK: - configure - - private fun configure(pubKey: String, bearerToken: String?, promise: Promise) { - val context = reactContext ?: run { - promise.reject("E_INIT_FAILED", "React context is not available", null) - return - } - - coroutineScope.launch { - try { - val normalizedBearerToken = bearerToken?.trim()?.takeIf { it.isNotEmpty() } - - if (!Clerk.isInitialized.value) { - // First-time initialization — write the bearer token to SharedPreferences - // before initializing so the SDK boots with the correct client. - if (normalizedBearerToken != null) { - context.getSharedPreferences("clerk_preferences", Context.MODE_PRIVATE) - .edit() - .putString("DEVICE_TOKEN", normalizedBearerToken) - .apply() - } - - Clerk.initialize(context, pubKey, clerkConfigurationOptions()) - startClientStateObserver() - // clerk-android registers ActivityLifecycleCallbacks during - // initialize(), but in React Native MainActivity has already passed - // onResume() by the time mounts and we reach this - // line, so the callbacks miss the initial activity. Without seeding, - // the first Credential Manager call (Google sign-in / passkeys) - // fails with MissingActivity until the user backgrounds and - // foregrounds the app. currentActivity can be null here on - // cold start before React's host-resume sync — AuthView and - // UserProfile also call attachActivity() on mount as a backstop. - appContext.currentActivity?.let { Clerk.attachActivity(it) } - // Must be set AFTER Clerk.initialize() because initialize() - // resets customTheme to its `theme` parameter (default null). - loadThemeFromAssets(context) - - // Wait for initialization to complete with timeout - try { - withTimeout(10_000L) { - Clerk.isInitialized.first { it } - } - // If a bearer token was provided, wait for native client state to hydrate - // before resolving the configure call. - if (normalizedBearerToken != null) { - withTimeout(5_000L) { - Clerk.clientFlow.first { it != null } - } - } - } catch (e: TimeoutCancellationException) { - val initError = Clerk.initializationError.value - val message = if (initError != null) { - "Clerk initialization timed out: ${initError.message}" - } else { - "Clerk initialization timed out after 10 seconds" - } - promise.reject("E_TIMEOUT", message, null) - return@launch - } - - // Check for initialization errors - val error = Clerk.initializationError.value - if (error != null) { - promise.reject("E_INIT_FAILED", "Failed to initialize Clerk SDK: ${error.message}", null) - } else { - configuredPublishableKey = pubKey - lastObservedClientState = clientStateSnapshot() - promise.resolve(null) - } - return@launch - } - - val activePublishableKey = configuredPublishableKey ?: Clerk.publishableKey - if (activePublishableKey != null && activePublishableKey != pubKey) { - Clerk.switchConfiguration(context, pubKey, clerkConfigurationOptions()) - startClientStateObserver() - appContext.currentActivity?.let { Clerk.attachActivity(it) } - loadThemeFromAssets(context) - - try { - withTimeout(10_000L) { - Clerk.isInitialized.first { it } - } - } catch (e: TimeoutCancellationException) { - val initError = Clerk.initializationError.value - val message = if (initError != null) { - "Clerk reconfiguration timed out: ${initError.message}" - } else { - "Clerk reconfiguration timed out after 10 seconds" - } - promise.reject("E_TIMEOUT", message, null) - return@launch - } - - val error = Clerk.initializationError.value - if (error != null) { - promise.reject("E_RECONFIGURE_FAILED", "Failed to reconfigure Clerk SDK: ${error.message}", null) - return@launch - } - - if (normalizedBearerToken != null) { - val clientState = clientStateSnapshot() - if (clientState.deviceToken != normalizedBearerToken || clientState.client == null) { - val result = Clerk.updateDeviceToken(normalizedBearerToken) - if (result is ClerkResult.Failure) { - debugLog(TAG, "configure - updateDeviceToken after reconfigure failed: ${result.error}") - } - } - - try { - withTimeout(5_000L) { - Clerk.clientFlow.first { it != null } - } - } catch (_: TimeoutCancellationException) { - debugLog(TAG, "configure - client did not appear after reconfigure token update") - } - } - - configuredPublishableKey = pubKey - lastObservedClientState = clientStateSnapshot() - promise.resolve(null) - return@launch - } - - // Already initialized — use the public SDK API to update - // the device token and trigger a client/environment refresh. - startClientStateObserver() - if (normalizedBearerToken != null) { - val clientState = clientStateSnapshot() - val result = if ( - clientState.deviceToken != normalizedBearerToken || - clientState.client == null - ) { - Clerk.updateDeviceToken(normalizedBearerToken) - } else { - // A remounted JS runtime can have the same token while native - // client state is stale, so preserve one refresh in that case. - Clerk.refreshClient() - } - if (result is ClerkResult.Failure) { - debugLog(TAG, "configure - client refresh failed: ${result.error}") - } - - // Wait for client state to hydrate with the new token (up to 5s). - try { - withTimeout(5_000L) { - Clerk.clientFlow.first { it != null } - } - } catch (_: TimeoutCancellationException) { - debugLog(TAG, "configure - client did not appear after token update") - } - } - - lastObservedClientState = clientStateSnapshot() - promise.resolve(null) - } catch (e: Exception) { - promise.reject("E_INIT_FAILED", "Failed to initialize Clerk SDK: ${e.message}", e) - } - } - } - // MARK: - single-token client sync private fun configureNative(pubKey: String, seedDeviceToken: String?, promise: Promise) { @@ -672,11 +368,9 @@ class ClerkExpoModule : Module() { // Must follow initialize(), which resets customTheme. loadThemeFromAssets(context) } - startClientStateObserver() val didAdoptSeed = adoptSeedDeviceTokenIfNeeded(seedDeviceToken) startClientInvalidationObserver() - lastObservedClientState = clientStateSnapshot() if (didAdoptSeed) { // An initialization refresh started before the seed was stored is fenced off by the token change. launch { @@ -715,7 +409,16 @@ class ClerkExpoModule : Module() { } private fun clientFingerprint(client: Client? = Clerk.clientFlow.value): ClerkClientFingerprint { - return ClerkClientFingerprint.from(client, clientStateSnapshot(client).deviceToken) + return ClerkClientFingerprint.from(client, currentDeviceToken()) + } + + private fun currentDeviceToken(): String? { + return try { + Clerk.getDeviceToken() + } catch (e: Exception) { + debugLog(TAG, "getDeviceToken failed: ${e.message}") + null + } } private fun isClerkConfigured(): Boolean = Clerk.publishableKey != null @@ -778,21 +481,6 @@ class ClerkExpoModule : Module() { } } - // MARK: - getClientToken - - private fun getClientToken(promise: Promise) { - try { - // Use the SDK's public API which handles encrypted storage transparently. - // Direct SharedPreferences reads break on clerk-android >= 1.0.11 where - // DEVICE_TOKEN is encrypted via StorageCipher. - val deviceToken = Clerk.getDeviceToken() - promise.resolve(deviceToken) - } catch (e: Exception) { - debugLog(TAG, "getClientToken failed: ${e.message}") - promise.resolve(null) - } - } - // MARK: - biometric credentials private fun getBiometricCredentialAvailability( @@ -1062,100 +750,6 @@ class ClerkExpoModule : Module() { promise.reject(error.code, error.message, exception) } - // MARK: - syncClientStateFromJs - - private fun syncClientStateFromJs( - deviceToken: String?, - sourceId: String?, - didChangeClient: Boolean, - didChangeDeviceToken: Boolean, - promise: Promise - ) { - if (!Clerk.isInitialized.value) { - promise.resolve(null) - return - } - - coroutineScope.launch { - try { - jsOriginatedClientSyncDepth += 1 - val previousClientState = clientStateSnapshot() - var refreshedClientWhileUpdatingToken = false - - if (didChangeDeviceToken && !deviceToken.isNullOrBlank()) { - val currentDeviceToken = try { - Clerk.getDeviceToken() - } catch (_: Exception) { - null - } - - if (currentDeviceToken != deviceToken) { - when (val result = Clerk.updateDeviceToken(deviceToken)) { - is ClerkResult.Failure -> { - promise.reject( - "E_SYNC_FROM_JS_FAILED", - result.error?.firstMessage() ?: result.throwable?.message ?: "Device token sync failed", - null - ) - return@launch - } - is ClerkResult.Success -> { - refreshedClientWhileUpdatingToken = true - try { - withTimeout(5_000L) { - Clerk.clientFlow.first { it != null } - } - } catch (_: TimeoutCancellationException) { - debugLog(TAG, "syncClientStateFromJs - client did not appear after token update") - } - } - } - } - } - - if (!refreshedClientWhileUpdatingToken && (didChangeClient || didChangeDeviceToken)) { - 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 -> { - val newClientState = clientStateSnapshot() - emitSyncedClientChanged( - sourceId, - ClientStateChanges( - client = newClientState.client != previousClientState.client, - deviceToken = newClientState.deviceToken != previousClientState.deviceToken - ), - newClientState - ) - promise.resolve(null) - } - } - return@launch - } - - val newClientState = clientStateSnapshot() - emitSyncedClientChanged( - sourceId, - ClientStateChanges( - client = newClientState.client != previousClientState.client, - deviceToken = newClientState.deviceToken != previousClientState.deviceToken - ), - newClientState - ) - promise.resolve(null) - } catch (e: Exception) { - promise.reject("E_SYNC_FROM_JS_FAILED", e.message ?: "Client state sync failed", e) - } finally { - jsOriginatedClientSyncDepth = maxOf(0, jsOriginatedClientSyncDepth - 1) - } - } - } - // MARK: - Theme Loading private fun loadThemeFromAssets(context: Context) { diff --git a/packages/expo-native-components/ios/ClerkExpoModule.swift b/packages/expo-native-components/ios/ClerkExpoModule.swift index fb84e7f59a0..411bf2c7204 100644 --- a/packages/expo-native-components/ios/ClerkExpoModule.swift +++ b/packages/expo-native-components/ios/ClerkExpoModule.swift @@ -9,7 +9,6 @@ import Foundation public class ClerkExpoModule: Module { private static let nativeAuthFlowChangedEvent = "clerkNativeAuthFlowChanged" - private static let nativeClientChangedEvent = "clerkNativeClientChanged" private static let nativeClientInvalidatedEvent = "clerkNativeClientInvalidated" private static weak var sharedInstance: ClerkExpoModule? @@ -17,16 +16,13 @@ public class ClerkExpoModule: Module { public func definition() -> ModuleDefinition { Name("ClerkExpo") - Events(Self.nativeAuthFlowChangedEvent, Self.nativeClientChangedEvent, Self.nativeClientInvalidatedEvent) + Events(Self.nativeAuthFlowChangedEvent, Self.nativeClientInvalidatedEvent) OnCreate { Self.sharedInstance = self ClerkNativeBridge.setAuthFlowChangedEmitter { body in Self.emitAuthFlowChanged(body) } - ClerkNativeBridge.setClientChangedEmitter { body in - Self.emitClientChanged(body) - } ClerkNativeBridge.setClientInvalidatedEmitter { Self.emitClientInvalidated() } @@ -36,19 +32,10 @@ public class ClerkExpoModule: Module { if Self.sharedInstance === self { Self.sharedInstance = nil ClerkNativeBridge.setAuthFlowChangedEmitter(nil) - ClerkNativeBridge.setClientChangedEmitter(nil) ClerkNativeBridge.setClientInvalidatedEmitter(nil) } } - AsyncFunction("configure") { (publishableKey: String, bearerToken: String?, promise: Promise) in - self.configure(publishableKey, bearerToken: bearerToken, promise: promise) - } - - AsyncFunction("getClientToken") { (promise: Promise) in - self.getClientToken(promise: promise) - } - AsyncFunction("configureNative") { (publishableKey: String, seedDeviceToken: String?, promise: Promise) in Task { @MainActor in do { @@ -98,21 +85,6 @@ public class ClerkExpoModule: Module { self.getAuthFlowState(promise: promise) } - AsyncFunction("syncClientStateFromJs") { - (deviceToken: String?, - sourceId: String?, - didChangeClient: Bool, - didChangeDeviceToken: Bool, - promise: Promise) in - self.syncClientStateFromJs( - deviceToken, - sourceId: sourceId, - didChangeClient: didChangeClient, - didChangeDeviceToken: didChangeDeviceToken, - promise: promise - ) - } - AsyncFunction("getTrustedDeviceAvailability") { (id: String?, identifierHint: String?, promise: Promise) in self.getBiometricCredentialAvailability(id: id, identifierHint: identifierHint, promise: promise) @@ -172,28 +144,6 @@ public class ClerkExpoModule: Module { } } - // MARK: - configure - - private func configure(_ publishableKey: String, bearerToken: String?, promise: Promise) { - Task { - do { - try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken) - promise.resolve() - } catch { - promise.reject("E_CONFIGURE_FAILED", error.localizedDescription) - } - } - } - - // MARK: - getClientToken - - private func getClientToken(promise: Promise) { - Task { - let token = await ClerkNativeBridge.shared.getClientToken() - promise.resolve(token) - } - } - // MARK: - getAuthFlowState private func getAuthFlowState(promise: Promise) { @@ -203,28 +153,6 @@ public class ClerkExpoModule: Module { } } - // MARK: - syncClientStateFromJs - - private func syncClientStateFromJs(_ deviceToken: String?, - sourceId: String?, - didChangeClient: Bool, - didChangeDeviceToken: Bool, - promise: Promise) { - Task { - do { - try await ClerkNativeBridge.shared.syncClientStateFromJs( - deviceToken: deviceToken, - sourceId: sourceId, - didChangeClient: didChangeClient, - didChangeDeviceToken: didChangeDeviceToken - ) - promise.resolve() - } catch { - promise.reject("E_SYNC_FROM_JS_FAILED", error.localizedDescription) - } - } - } - // MARK: - Biometric credentials private func getBiometricCredentialAvailability(id: String?, identifierHint: String?, promise: Promise) { @@ -337,20 +265,6 @@ public class ClerkExpoModule: Module { promise.reject(descriptor.code, descriptor.message) } - /// Emits a native client change event to JS from anywhere in the native layer. - /// Used by native views to ask ClerkProvider to reload JS client state. - static func emitClientChanged(_ body: [String: Any]? = nil) { - let eventBody = body ?? [:] - - guard let instance = sharedInstance else { - return - } - - DispatchQueue.main.async { [weak instance] in - instance?.sendEvent(Self.nativeClientChangedEvent, eventBody) - } - } - static func emitClientInvalidated() { guard let instance = sharedInstance else { return diff --git a/packages/expo-native-components/ios/ClerkNativeBridge.swift b/packages/expo-native-components/ios/ClerkNativeBridge.swift index 0551e609f32..5c0ccbc5d19 100644 --- a/packages/expo-native-components/ios/ClerkNativeBridge.swift +++ b/packages/expo-native-components/ios/ClerkNativeBridge.swift @@ -397,7 +397,6 @@ func userProfileCustomPageLabel( private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events") private var clerkNativeAuthFlowChangedEmitter: (([String: Any]?) -> Void)? -private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)? private var clerkNativeClientInvalidatedEmitter: (() -> Void)? struct ClerkNativeErrorDescriptor { @@ -432,8 +431,6 @@ private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware { final class ClerkNativeBridge { static let shared = ClerkNativeBridge() - private static let clerkLoadMaxAttempts = 30 - private static let clerkLoadIntervalNs: UInt64 = 100_000_000 private static var clerkConfigured = false private static var configuredPublishableKey: String? @@ -441,12 +438,9 @@ final class ClerkNativeBridge { var lightTheme: ClerkTheme? var darkTheme: ClerkTheme? - private var clientObservationGeneration = 0 - private var lastObservedClientState: ClientStateSnapshot? private var authFlowObservationGeneration = 0 private var lastObservedAuthFlowState: AuthFlowStateSnapshot? private var configurationDepth = 0 - private var jsOriginatedClientSyncDepth = 0 private var clientInvalidationGeneration = 0 private var clientInvalidationTracker: ClerkClientInvalidationTracker? private var pendingURL: URL? @@ -454,23 +448,11 @@ final class ClerkNativeBridge { private init() {} - private struct ClientStateSnapshot: Equatable { - let client: Client? - let deviceToken: String? - } - private struct AuthFlowStateSnapshot: Equatable { let isLoaded: Bool let isAuthFlowComplete: Bool } - private struct ClientStateChanges { - let client: Bool - let deviceToken: Bool - - static let all = ClientStateChanges(client: true, deviceToken: true) - } - /// Resolves the keychain service name, checking ClerkKeychainService in Info.plist first /// (for extension apps sharing a keychain group), then falling back to the bundle identifier. private static var keychainService: String? { @@ -482,7 +464,6 @@ final class ClerkNativeBridge { @MainActor private func endConfiguration() { - lastObservedClientState = Self.clerkConfigured ? Self.clientStateSnapshot() : nil let authFlowState = Self.authFlowStateSnapshot() lastObservedAuthFlowState = authFlowState configurationDepth = max(0, configurationDepth - 1) @@ -496,55 +477,6 @@ final class ClerkNativeBridge { } } - @MainActor - func configure(publishableKey: String, bearerToken: String? = nil) async throws { - configurationDepth += 1 - defer { endConfiguration() } - - loadThemes() - - if Self.shouldReconfigure(for: publishableKey) { - try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions()) - Self.clerkConfigured = true - Self.configuredPublishableKey = publishableKey - startClientObserver(reset: true) - startAuthFlowObserver(reset: true) - - let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken) - await Self.waitForLoadedClientIfNeeded(shouldWaitForClient) - Self.postConfiguredNotification() - shouldFlushPendingURL = true - return - } - - if Self.clerkConfigured { - startClientObserver() - startAuthFlowObserver() - let didUpdateDeviceToken = try await Self.syncTokenState(bearerToken: bearerToken) - if didUpdateDeviceToken { - await Self.waitForLoadedClient() - } else if let token = bearerToken?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty { - // A remounted JS runtime can have the same token while native client - // state is stale, so preserve one refresh in that case. - _ = try await Clerk.shared.refreshClient() - await Self.waitForLoadedClient() - } - shouldFlushPendingURL = true - return - } - - Self.clerkConfigured = true - Self.configuredPublishableKey = publishableKey - Clerk.configure(publishableKey: publishableKey, options: Self.makeClerkOptions()) - startClientObserver() - startAuthFlowObserver() - - let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken) - await Self.waitForLoadedClientIfNeeded(shouldWaitForClient) - Self.postConfiguredNotification() - shouldFlushPendingURL = true - } - /// Configures ClerkKit without waiting for the client to load. ClerkKit's stored device token /// wins; `seedDeviceToken` is only adopted when ClerkKit has none. @MainActor @@ -558,17 +490,14 @@ final class ClerkNativeBridge { if Self.shouldReconfigure(for: publishableKey) { try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions()) Self.configuredPublishableKey = publishableKey - startClientObserver(reset: true) startAuthFlowObserver(reset: true) } else if Self.clerkConfigured { didConfigure = false - startClientObserver() startAuthFlowObserver() } else { Self.clerkConfigured = true Self.configuredPublishableKey = publishableKey Clerk.configure(publishableKey: publishableKey, options: Self.makeClerkOptions()) - startClientObserver() startAuthFlowObserver() } @@ -669,7 +598,7 @@ final class ClerkNativeBridge { /// `AuthView` only reaches `Clerk.handle(_:)` from `.onOpenURL`, which never fires for a UIKit-hosted controller. @MainActor func handle(url: URL) { - // A cold launch delivers the callback before, or partway through, JS calling `configure`. + // A cold launch delivers the callback before, or partway through, JS calling `configureNative`. guard Self.clerkConfigured, configurationDepth == 0 else { pendingURL = url return @@ -684,47 +613,6 @@ final class ClerkNativeBridge { } } - @MainActor - private func startClientObserver(reset: Bool = false) { - guard reset || clientObservationGeneration == 0 else { - return - } - - clientObservationGeneration += 1 - let generation = clientObservationGeneration - lastObservedClientState = Self.clientStateSnapshot() - observeClient(generation: generation) - } - - @MainActor - private func observeClient(generation: Int) { - withObservationTracking { - _ = Self.clientStateSnapshot() - } onChange: { [weak self] in - Task { @MainActor [weak self] in - await Task.yield() - - guard let self, generation == self.clientObservationGeneration else { return } - - let newClientState = Self.clientStateSnapshot() - if let previousClientState = self.lastObservedClientState, newClientState != previousClientState { - self.lastObservedClientState = newClientState - if self.configurationDepth == 0, self.jsOriginatedClientSyncDepth == 0 { - let payload = Self.clientChangedPayload( - changes: .init( - client: newClientState.client != previousClientState.client, - deviceToken: newClientState.deviceToken != previousClientState.deviceToken - ) - ) - Self.emitClientChanged(payload) - } - } - - self.observeClient(generation: generation) - } - } - } - @MainActor private func startAuthFlowObserver(reset: Bool = false) { guard reset || authFlowObservationGeneration == 0 else { @@ -779,46 +667,6 @@ final class ClerkNativeBridge { ] } - @MainActor - private static func clientStateSnapshot() -> ClientStateSnapshot { - let client = Clerk.shared.client - - return ClientStateSnapshot( - client: client, - deviceToken: Clerk.shared.deviceToken - ) - } - - @MainActor - private static func clientChangedPayload(sourceId: String? = nil, changes: ClientStateChanges = .all) -> [String: Any] { - var payload: [String: Any] = [:] - payload["changed"] = [ - "client": changes.client, - "deviceToken": changes.deviceToken, - ] - payload["deviceToken"] = Clerk.shared.deviceToken ?? NSNull() - if let sourceId, !sourceId.isEmpty { - payload["sourceId"] = sourceId - } - - return payload - } - - @MainActor - private static func syncTokenState(bearerToken: String?) async throws -> Bool { - await waitForLoadedClient() - - guard let token = bearerToken?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty - else { - return false - } - guard Clerk.shared.deviceToken != token || Clerk.shared.client == nil else { - return false - } - _ = try await Clerk.shared.updateDeviceToken(token) - return true - } - private static func shouldReconfigure(for publishableKey: String) -> Bool { guard clerkConfigured, let configuredPublishableKey else { return false } return configuredPublishableKey != publishableKey @@ -832,30 +680,6 @@ final class ClerkNativeBridge { return .init(keychainConfig: .init(service: service), middleware: middleware) } - @MainActor - private static func waitForLoadedClient() async { - // Wait for Clerk to finish loading client state from cached data + API refresh. - // The bridge sync contract is device-token based, not session based. - for _ in 0.. String? { - guard Self.clerkConfigured else { return nil } - return Clerk.shared.deviceToken - } - @MainActor func getAuthFlowState() -> [String: Any] { Self.authFlowStatePayload(Self.authFlowStateSnapshot()) @@ -1196,76 +1020,10 @@ final class ClerkNativeBridge { ) } - @MainActor - func syncClientStateFromJs( - deviceToken: String?, - sourceId: String?, - didChangeClient: Bool, - didChangeDeviceToken: Bool - ) async throws { - guard Self.clerkConfigured else { return } - - let previousClientState = Self.clientStateSnapshot() - var completedSuccessfully = false - jsOriginatedClientSyncDepth += 1 - defer { - let finalClientState = Self.clientStateSnapshot() - lastObservedClientState = finalClientState - jsOriginatedClientSyncDepth = max(0, jsOriginatedClientSyncDepth - 1) - - if !completedSuccessfully, finalClientState != previousClientState { - Self.emitClientChanged( - Self.clientChangedPayload( - changes: .init( - client: finalClientState.client != previousClientState.client, - deviceToken: finalClientState.deviceToken != previousClientState.deviceToken - ) - ) - ) - } - } - - var refreshedClientWhileUpdatingToken = false - - if didChangeDeviceToken, - let token = deviceToken?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty - { - if Clerk.shared.deviceToken != token { - _ = try await Clerk.shared.updateDeviceToken(token) - await Self.waitForLoadedClient() - refreshedClientWhileUpdatingToken = true - } - } - - if !refreshedClientWhileUpdatingToken, didChangeClient || didChangeDeviceToken { - _ = try await Clerk.shared.refreshClient() - await Self.waitForLoadedClient() - } - - let newClientState = Self.clientStateSnapshot() - lastObservedClientState = newClientState - Self.emitClientChanged( - Self.clientChangedPayload( - sourceId: sourceId, - changes: .init( - client: newClientState.client != previousClientState.client, - deviceToken: newClientState.deviceToken != previousClientState.deviceToken - ) - ) - ) - completedSuccessfully = true - } - private static func postConfiguredNotification() { NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil) } - static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) { - clerkNativeClientEventQueue.sync { - clerkNativeClientChangedEmitter = emitter - } - } - static func setClientInvalidatedEmitter(_ emitter: (() -> Void)?) { clerkNativeClientEventQueue.sync { clerkNativeClientInvalidatedEmitter = emitter @@ -1292,14 +1050,6 @@ final class ClerkNativeBridge { emitter?(body) } - /// Requests that ClerkProvider reload the JS client from native client state. - static func emitClientChanged(_ body: [String: Any]? = nil) { - let emitter = clerkNativeClientEventQueue.sync { - clerkNativeClientChangedEmitter - } - emitter?(body) - } - private static func authMode(from mode: String) -> AuthView.Mode { switch mode { case "signIn": diff --git a/packages/expo/src/specs/__tests__/androidSpecs.test.ts b/packages/expo/src/specs/__tests__/androidSpecs.test.ts index bbebc8f11c9..6ec0234bdbf 100644 --- a/packages/expo/src/specs/__tests__/androidSpecs.test.ts +++ b/packages/expo/src/specs/__tests__/androidSpecs.test.ts @@ -39,7 +39,7 @@ describe('android native module specs', () => { }); test('resolve the module registered under the expected name (development build)', async () => { - const clerkExpoModule = { configure: vi.fn() }; + const clerkExpoModule = { configureNative: vi.fn() }; const googleSignInModule = { signIn: vi.fn() }; mocks.available = { ClerkExpo: clerkExpoModule, ClerkGoogleSignIn: googleSignInModule };