Skip to content
Open
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/expo-native-proxy-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/expo': patch
---

Native components now respect the `proxyUrl` passed to `<ClerkProvider>` and route Frontend API requests through the configured proxy. Applying the proxy requires a new app binary; a JS-only OTA update safely keeps the previous behavior.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import com.clerk.api.ui.ClerkTheme
import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand All @@ -36,12 +38,19 @@ private fun debugLog(tag: String, message: String) {
}
}

internal class ConfigureOptions : Record {
@Field val bearerToken: String? = null

@Field val proxyUrl: String? = null
}

class ClerkExpoModule : Module() {
private val coroutineScope = CoroutineScope(Dispatchers.Main)
private var clientStateObserverJob: Job? = null
private var lastObservedClientState: ClientStateSnapshot? = null
private var jsOriginatedClientSyncDepth = 0
private var configuredPublishableKey: String? = null
private var configuredProxyUrl: String? = null

private data class ClientStateSnapshot(
val client: Client?,
Expand Down Expand Up @@ -85,8 +94,13 @@ class ClerkExpoModule : Module() {
clientStateObserverJob = null
}

// Keeps the pre-proxy signature so OTA-updated JS on older binaries keeps working.
AsyncFunction("configure") { pubKey: String, bearerToken: String?, promise: Promise ->
configure(pubKey, bearerToken, promise)
configure(pubKey, bearerToken, null, promise)
}

AsyncFunction("configureWithOptions") { pubKey: String, options: ConfigureOptions, promise: Promise ->
configure(pubKey, options.bearerToken, options.proxyUrl, promise)
}

AsyncFunction("getClientToken") { promise: Promise ->
Expand All @@ -112,7 +126,7 @@ class ClerkExpoModule : Module() {
private val reactContext: Context?
get() = appContext.reactContext

private fun clerkConfigurationOptions(): ClerkConfigurationOptions {
private fun clerkConfigurationOptions(proxyUrl: String?): ClerkConfigurationOptions {
val hostSdkVersion = BuildConfig.CLERK_EXPO_VERSION.trim()
val customHeaders = buildMap {
put(HOST_SDK_HEADER, HOST_SDK)
Expand All @@ -121,7 +135,7 @@ class ClerkExpoModule : Module() {
}
}

return ClerkConfigurationOptions().withCustomHeaders(customHeaders)
return ClerkConfigurationOptions(proxyUrl = proxyUrl).withCustomHeaders(customHeaders)
}

private fun startClientStateObserver() {
Expand Down Expand Up @@ -207,7 +221,7 @@ class ClerkExpoModule : Module() {

// MARK: - configure

private fun configure(pubKey: String, bearerToken: String?, promise: Promise) {
private fun configure(pubKey: String, bearerToken: String?, proxyUrl: String?, promise: Promise) {
val context = reactContext ?: run {
promise.reject("E_INIT_FAILED", "React context is not available", null)
return
Expand All @@ -216,6 +230,7 @@ class ClerkExpoModule : Module() {
coroutineScope.launch {
try {
val normalizedBearerToken = bearerToken?.trim()?.takeIf { it.isNotEmpty() }
val normalizedProxyUrl = proxyUrl?.trim()?.takeIf { it.isNotEmpty() }

if (!Clerk.isInitialized.value) {
// First-time initialization — write the bearer token to SharedPreferences
Expand All @@ -227,7 +242,7 @@ class ClerkExpoModule : Module() {
.apply()
}

Clerk.initialize(context, pubKey, clerkConfigurationOptions())
Clerk.initialize(context, pubKey, clerkConfigurationOptions(normalizedProxyUrl))
startClientStateObserver()
// clerk-android registers ActivityLifecycleCallbacks during
// initialize(), but in React Native MainActivity has already passed
Expand Down Expand Up @@ -272,15 +287,16 @@ class ClerkExpoModule : Module() {
promise.reject("E_INIT_FAILED", "Failed to initialize Clerk SDK: ${error.message}", null)
} else {
configuredPublishableKey = pubKey
configuredProxyUrl = normalizedProxyUrl
lastObservedClientState = clientStateSnapshot()
promise.resolve(null)
}
return@launch
}

val activePublishableKey = configuredPublishableKey ?: Clerk.publishableKey
if (activePublishableKey != null && activePublishableKey != pubKey) {
Clerk.switchConfiguration(context, pubKey, clerkConfigurationOptions())
if (activePublishableKey != null && (activePublishableKey != pubKey || configuredProxyUrl != normalizedProxyUrl)) {
Clerk.switchConfiguration(context, pubKey, clerkConfigurationOptions(normalizedProxyUrl))
startClientStateObserver()
appContext.currentActivity?.let { Clerk.attachActivity(it) }
loadThemeFromAssets(context)
Expand Down Expand Up @@ -325,6 +341,7 @@ class ClerkExpoModule : Module() {
}

configuredPublishableKey = pubKey
configuredProxyUrl = normalizedProxyUrl
lastObservedClientState = clientStateSnapshot()
promise.resolve(null)
return@launch
Expand Down
19 changes: 16 additions & 3 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
import ExpoModulesCore
import Foundation

// MARK: - Records

struct ConfigureOptions: Record {
@Field var bearerToken: String?
@Field var proxyUrl: String?
}

// MARK: - Module

public class ClerkExpoModule: Module {
Expand All @@ -31,8 +38,13 @@ public class ClerkExpoModule: Module {
}
}

// Keeps the pre-proxy signature so OTA-updated JS on older binaries keeps working.
AsyncFunction("configure") { (publishableKey: String, bearerToken: String?, promise: Promise) in
self.configure(publishableKey, bearerToken: bearerToken, promise: promise)
self.configure(publishableKey, bearerToken: bearerToken, proxyUrl: nil, promise: promise)
}

AsyncFunction("configureWithOptions") { (publishableKey: String, options: ConfigureOptions, promise: Promise) in
self.configure(publishableKey, bearerToken: options.bearerToken, proxyUrl: options.proxyUrl, promise: promise)
}

AsyncFunction("getClientToken") { (promise: Promise) in
Expand All @@ -57,10 +69,11 @@ public class ClerkExpoModule: Module {

// MARK: - configure

private func configure(_ publishableKey: String, bearerToken: String?, promise: Promise) {
private func configure(_ publishableKey: String, bearerToken: String?, proxyUrl: String?, promise: Promise) {
Task {
do {
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(
publishableKey: publishableKey, bearerToken: bearerToken, proxyUrl: proxyUrl)
promise.resolve()
} catch {
promise.reject("E_CONFIGURE_FAILED", error.localizedDescription)
Expand Down
31 changes: 22 additions & 9 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ final class ClerkNativeBridge {
private static let clerkLoadIntervalNs: UInt64 = 100_000_000
private static var clerkConfigured = false
private static var configuredPublishableKey: String?
private static var configuredProxyUrl: String?

/// Parsed light and dark themes from Info.plist "ClerkTheme" dictionary.
var lightTheme: ClerkTheme?
Expand Down Expand Up @@ -101,19 +102,23 @@ final class ClerkNativeBridge {
}

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
func configure(publishableKey: String, bearerToken: String? = nil, proxyUrl: String? = nil) async throws {
configurationDepth += 1
defer {
lastObservedClientState = Self.clerkConfigured ? Self.clientStateSnapshot() : nil
configurationDepth = max(0, configurationDepth - 1)
}

let normalizedProxyUrl = Self.normalizedProxyUrl(proxyUrl)

loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
if Self.shouldReconfigure(for: publishableKey, proxyUrl: normalizedProxyUrl) {
try await Clerk.reconfigure(
publishableKey: publishableKey, options: Self.makeClerkOptions(proxyUrl: normalizedProxyUrl))
Self.clerkConfigured = true
Self.configuredPublishableKey = publishableKey
Self.configuredProxyUrl = normalizedProxyUrl
startClientObserver(reset: true)

let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
Expand All @@ -138,7 +143,8 @@ final class ClerkNativeBridge {

Self.clerkConfigured = true
Self.configuredPublishableKey = publishableKey
Clerk.configure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.configuredProxyUrl = normalizedProxyUrl
Clerk.configure(publishableKey: publishableKey, options: Self.makeClerkOptions(proxyUrl: normalizedProxyUrl))
startClientObserver()

let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
Expand Down Expand Up @@ -227,17 +233,24 @@ final class ClerkNativeBridge {
return true
}

private static func shouldReconfigure(for publishableKey: String) -> Bool {
private static func shouldReconfigure(for publishableKey: String, proxyUrl: String?) -> Bool {
guard clerkConfigured, let configuredPublishableKey else { return false }
return configuredPublishableKey != publishableKey
return configuredPublishableKey != publishableKey || configuredProxyUrl != proxyUrl
}

private static func normalizedProxyUrl(_ proxyUrl: String?) -> String? {
guard let trimmed = proxyUrl?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
return nil
}
return trimmed
}

private static func makeClerkOptions() -> Clerk.Options {
private static func makeClerkOptions(proxyUrl: String?) -> Clerk.Options {
let middleware = Clerk.Options.MiddlewareConfig(request: [ClerkExpoHeaderMiddleware()])
guard let service = keychainService else {
return .init(middleware: middleware)
return .init(proxyUrl: proxyUrl, middleware: middleware)
}
return .init(keychainConfig: .init(service: service), middleware: middleware)
return .init(keychainConfig: .init(service: service), proxyUrl: proxyUrl, middleware: middleware)
}

@MainActor
Expand Down
1 change: 1 addition & 0 deletions packages/expo/src/provider/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function ClerkProvider<TUi extends Ui = Ui>(props: ClerkProviderProps<TUi
const suppressJsClientChangedRef = useRef(0);
const { isMountedRef, isNativeClientReady } = useNativeClientBootstrap({
publishableKey: pk,
proxyUrl,
nativeRefreshFromJsControllerRef,
suppressTokenCacheNotificationsRef,
tokenCache: syncableTokenCache,
Expand Down
Loading
Loading