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/tidy-expo-ios-import.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": minor
---

Fixes iOS development builds across Expo SDK versions by linking the Clerk iOS SDK through React Native's Swift Package Manager podspec support. This raises the minimum supported React Native version to 0.75, where that podspec SPM support is available; `@clerk/expo` already supports Expo SDK 53 and newer, and Expo SDK 53 ships with React Native 0.79.
384 changes: 5 additions & 379 deletions packages/expo/app.plugin.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkAuthNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,9 +46,7 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
}

override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeAuthViewController(
return ClerkNativeBridge.shared.makeAuthViewController(
mode: currentMode,
dismissible: currentDismissible,
onEvent: { [weak self] event, _ in
Expand Down
20 changes: 16 additions & 4 deletions packages/expo/ios/ClerkExpo.podspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ else
}
end

clerk_ios_repo = 'https://github.com/clerk/clerk-ios.git'
clerk_ios_version = '1.2.4'

Pod::Spec.new do |s|
s.name = 'ClerkExpo'
s.version = package['version']
Expand All@@ -35,10 +38,19 @@ Pod::Spec.new do |s|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

# Only include the module files in the pod (both Swift and ObjC bridges).
# ClerkNativeBridge.swift is injected into the app target by the config plugin
# because it uses `import ClerkKit` which is only available via SPM in the app target.
s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m",
if defined?(spm_dependency)
spm_dependency(
s,
url: clerk_ios_repo,
requirement: { :kind => 'exactVersion', :version => clerk_ios_version },
products: ['ClerkKit', 'ClerkKitUI']
)
else
raise 'ClerkExpo requires React Native 0.75 or newer for iOS Swift Package Manager dependencies.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep RN 0.73/0.74 pod installs working

For apps still on React Native 0.73/0.74 (which satisfied @clerk/expo's previous peer range and can receive this patch via a caret range), spm_dependency is not defined by RN's CocoaPods scripts, so this raise makes every iOS pod install fail even though those apps built with the old Xcode-project injection path. Because this is being released as a patch, please keep a fallback for those RN versions or avoid shipping this path to them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our supported expo range is >=53, so supporting 0.73 and 0.74 is already a mismatch. Expo 53 contains React Native 0.79

end

s.source_files = "ClerkNativeBridge.swift",
"ClerkExpoModule.swift", "ClerkExpoModule.m",
"ClerkNativeViewHost.swift",
"ClerkAuthNativeView.swift",
"ClerkAuthViewManager.m",
Expand Down
88 changes: 7 additions & 81 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,10 @@
// ClerkExpoModule - Native module for Clerk integration
// This module provides the configure function, client sync, and native view bridges.
// SwiftUI Clerk views are created by the app target through ClerkNativeBridge because
// the Clerk SDK (SPM) isn't accessible from the CocoaPods-backed React Native pod.
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.

import UIKit
import React

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

// Global registry for the app-target native bridge (set by the app target at startup)
public var clerkNativeBridge: ClerkNativeBridgeProtocol? {
didSet {
if clerkNativeBridge != nil {
emitClerkNativeBridgeReady()
}
}
}

// Protocol that the app target implements to provide Clerk SDK operations and SwiftUI views.
public protocol ClerkNativeBridgeProtocol {
// Inline rendering — returns UIViewController to preserve SwiftUI lifecycle
func makeAuthViewController(mode: String, dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserProfileViewController(dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserButtonViewController() -> UIViewController?

// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
func clerkNativeBridgeDidBecomeReady()
}

private let clerkNativeBridgeReadyObservers = NSHashTable<AnyObject>.weakObjects()

public func addClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.add(observer)
}

public func removeClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.remove(observer)
}

public func emitClerkNativeBridgeReady() {
let notifyObservers = {
for observer in clerkNativeBridgeReadyObservers.allObjects {
(observer as? ClerkNativeBridgeReadyObserver)?.clerkNativeBridgeDidBecomeReady()
}
}

if Thread.isMainThread {
notifyObservers()
} else {
DispatchQueue.main.async(execute: notifyObservers)
}
}

// MARK: - Module

@objc(ClerkExpo)
Expand All@@ -73,6 +16,9 @@ class ClerkExpoModule: RCTEventEmitter {
override init() {
super.init()
ClerkExpoModule.sharedInstance = self
ClerkNativeBridge.setClientChangedEmitter { body in
Self.emitClientChanged(body)
}
}

@objc override static func requiresMainQueueSetup() -> Bool {
Expand DownExpand Up@@ -120,14 +66,9 @@ class ClerkExpoModule: RCTEventEmitter {
bearerToken: String?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
reject("E_NOT_INITIALIZED", "Clerk not initialized. Make sure ClerkNativeBridge is registered.", nil)
return
}

Task {
do {
try await bridge.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
resolve(nil)
} catch {
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
Expand All@@ -139,13 +80,8 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

Task {
let token = await bridge.getClientToken()
let token = await ClerkNativeBridge.shared.getClientToken()
resolve(token)
}
}
Expand All@@ -157,18 +93,13 @@ class ClerkExpoModule: RCTEventEmitter {
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

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(
try await ClerkNativeBridge.shared.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
Expand All@@ -181,8 +112,3 @@ class ClerkExpoModule: RCTEventEmitter {
}

}

/// Requests that ClerkProvider reload the JS client from native client state.
public func emitClerkNativeClientChanged(_ body: [String: Any]? = nil) {
ClerkExpoModule.emitClientChanged(body)
}
65 changes: 46 additions & 19 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
// ClerkNativeBridge - Provides app-target Clerk SDK operations and SwiftUI view controllers to ClerkExpo.
// This file is injected into the app target by the config plugin.
// It uses the ClerkKit Swift package, which is only accessible from the app target.
// ClerkNativeBridge - Provides Clerk SDK operations and SwiftUI view controllers to ClerkExpo.

import UIKit
import SwiftUI
import Observation
@_spi(FrameworkIntegration) import ClerkKit
import ClerkKitUI
import ClerkExpo // Import the pod to access ClerkNativeBridgeProtocol

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

extension Notification.Name {
static let clerkNativeSDKDidConfigure = Notification.Name("com.clerk.expo.native-sdk.did-configure")
}

private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events")
private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)?

private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {
// Replaced by the config plugin when this bridge is copied into the app target.
private static let hostSdkVersion = "__CLERK_EXPO_VERSION__"
private static var hostSdkVersion: String? {
Bundle.main.object(forInfoDictionaryKey: "ClerkExpoVersion") as? String
}

func prepare(_ request: inout URLRequest) async throws {
request.addValue("expo", forHTTPHeaderField: "x-clerk-host-sdk")
request.addValue(Self.hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
if let hostSdkVersion = Self.hostSdkVersion, !hostSdkVersion.isEmpty {
request.addValue(hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
}
}
}

// MARK: - Native Bridge Implementation

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

private static let clerkLoadMaxAttempts = 30
Expand DownExpand Up@@ -52,14 +65,10 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return Bundle.main.bundleIdentifier
}

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

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Expand All@@ -69,7 +78,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
return
}

Expand All@@ -89,13 +98,13 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
}

@MainActor
private static func emitClientChangedIfReceivedToken(_ bearerToken: String?) {
guard let token = bearerToken, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
emitClerkNativeClientChanged(Self.clientChangedPayload())
Self.emitClientChanged(Self.clientChangedPayload())
}

@MainActor
Expand DownExpand Up@@ -124,7 +133,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
if newClientState != self.lastObservedClientState {
self.lastObservedClientState = newClientState
let payload = Self.clientChangedPayload()
emitClerkNativeClientChanged(payload)
Self.emitClientChanged(payload)
}

self.observeClient(generation: generation)
Expand DownExpand Up@@ -263,7 +272,25 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

lastObservedClientState = Self.clientStateSnapshot()
emitClerkNativeClientChanged(Self.clientChangedPayload(sourceId: sourceId))
Self.emitClientChanged(Self.clientChangedPayload(sourceId: sourceId))
}

private static func postConfiguredNotification() {
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
}

static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) {
clerkNativeClientEventQueue.sync {
clerkNativeClientChangedEmitter = emitter
}
}

/// 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 {
Expand Down
29 changes: 24 additions & 5 deletions packages/expo/ios/ClerkNativeViewHost.swift
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import UIKit

public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
public class ClerkNativeViewHost: UIView {
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
private var hasInitialized: Bool = false
private var configuredObserver: NSObjectProtocol?

override public init(frame: CGRect) {
super.init(frame: frame)
Expand All@@ -12,22 +13,26 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
fatalError("init(coder:) has not been implemented")
}

deinit {
removeConfiguredObserver()
}

override public func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else {
if hasInitialized {
hostedViewDidDetachFromWindow()
}
removeClerkNativeBridgeReadyObserver(self)
removeConfiguredObserver()
hostingCoordinator.detach()
hasInitialized = false
return
}

guard !hasInitialized else { return }
hasInitialized = true
addClerkNativeBridgeReadyObserver(self)
addConfiguredObserver()
hostedViewDidAttachToWindow()
updateHostedView()
}
Expand All@@ -51,8 +56,22 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {

func hostedViewDidDetachFromWindow() {}

public func clerkNativeBridgeDidBecomeReady() {
setNeedsHostedViewUpdate()
private func addConfiguredObserver() {
guard configuredObserver == nil else { return }

configuredObserver = NotificationCenter.default.addObserver(
forName: .clerkNativeSDKDidConfigure,
object: nil,
queue: .main
) { [weak self] _ in
self?.setNeedsHostedViewUpdate()
}
}

private func removeConfiguredObserver() {
guard let configuredObserver else { return }
NotificationCenter.default.removeObserver(configuredObserver)
self.configuredObserver = nil
}

private func updateHostedView() {
Expand Down
4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkUserButtonNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,7 @@ import UIKit

public class ClerkUserButtonNativeView: ClerkNativeViewHost {
override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeUserButtonViewController()
return ClerkNativeBridge.shared.makeUserButtonViewController()
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(expo): link iOS SDK through podspec SPM dependencies by mikepitre · Pull Request #8927 · clerk/javascript · GitHub
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/tidy-expo-ios-import.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": minor
---

Fixes iOS development builds across Expo SDK versions by linking the Clerk iOS SDK through React Native's Swift Package Manager podspec support. This raises the minimum supported React Native version to 0.75, where that podspec SPM support is available; `@clerk/expo` already supports Expo SDK 53 and newer, and Expo SDK 53 ships with React Native 0.79.
384 changes: 5 additions & 379 deletions packages/expo/app.plugin.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkAuthNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,9 +46,7 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
}

override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeAuthViewController(
return ClerkNativeBridge.shared.makeAuthViewController(
mode: currentMode,
dismissible: currentDismissible,
onEvent: { [weak self] event, _ in
Expand Down
20 changes: 16 additions & 4 deletions packages/expo/ios/ClerkExpo.podspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ else
}
end

clerk_ios_repo = 'https://github.com/clerk/clerk-ios.git'
clerk_ios_version = '1.2.4'

Pod::Spec.new do |s|
s.name = 'ClerkExpo'
s.version = package['version']
Expand All@@ -35,10 +38,19 @@ Pod::Spec.new do |s|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

# Only include the module files in the pod (both Swift and ObjC bridges).
# ClerkNativeBridge.swift is injected into the app target by the config plugin
# because it uses `import ClerkKit` which is only available via SPM in the app target.
s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m",
if defined?(spm_dependency)
spm_dependency(
s,
url: clerk_ios_repo,
requirement: { :kind => 'exactVersion', :version => clerk_ios_version },
products: ['ClerkKit', 'ClerkKitUI']
)
else
raise 'ClerkExpo requires React Native 0.75 or newer for iOS Swift Package Manager dependencies.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep RN 0.73/0.74 pod installs working

For apps still on React Native 0.73/0.74 (which satisfied @clerk/expo's previous peer range and can receive this patch via a caret range), spm_dependency is not defined by RN's CocoaPods scripts, so this raise makes every iOS pod install fail even though those apps built with the old Xcode-project injection path. Because this is being released as a patch, please keep a fallback for those RN versions or avoid shipping this path to them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our supported expo range is >=53, so supporting 0.73 and 0.74 is already a mismatch. Expo 53 contains React Native 0.79

end

s.source_files = "ClerkNativeBridge.swift",
"ClerkExpoModule.swift", "ClerkExpoModule.m",
"ClerkNativeViewHost.swift",
"ClerkAuthNativeView.swift",
"ClerkAuthViewManager.m",
Expand Down
88 changes: 7 additions & 81 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,10 @@
// ClerkExpoModule - Native module for Clerk integration
// This module provides the configure function, client sync, and native view bridges.
// SwiftUI Clerk views are created by the app target through ClerkNativeBridge because
// the Clerk SDK (SPM) isn't accessible from the CocoaPods-backed React Native pod.
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.

import UIKit
import React

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

// Global registry for the app-target native bridge (set by the app target at startup)
public var clerkNativeBridge: ClerkNativeBridgeProtocol? {
didSet {
if clerkNativeBridge != nil {
emitClerkNativeBridgeReady()
}
}
}

// Protocol that the app target implements to provide Clerk SDK operations and SwiftUI views.
public protocol ClerkNativeBridgeProtocol {
// Inline rendering — returns UIViewController to preserve SwiftUI lifecycle
func makeAuthViewController(mode: String, dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserProfileViewController(dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserButtonViewController() -> UIViewController?

// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
func clerkNativeBridgeDidBecomeReady()
}

private let clerkNativeBridgeReadyObservers = NSHashTable<AnyObject>.weakObjects()

public func addClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.add(observer)
}

public func removeClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.remove(observer)
}

public func emitClerkNativeBridgeReady() {
let notifyObservers = {
for observer in clerkNativeBridgeReadyObservers.allObjects {
(observer as? ClerkNativeBridgeReadyObserver)?.clerkNativeBridgeDidBecomeReady()
}
}

if Thread.isMainThread {
notifyObservers()
} else {
DispatchQueue.main.async(execute: notifyObservers)
}
}

// MARK: - Module

@objc(ClerkExpo)
Expand All@@ -73,6 +16,9 @@ class ClerkExpoModule: RCTEventEmitter {
override init() {
super.init()
ClerkExpoModule.sharedInstance = self
ClerkNativeBridge.setClientChangedEmitter { body in
Self.emitClientChanged(body)
}
}

@objc override static func requiresMainQueueSetup() -> Bool {
Expand DownExpand Up@@ -120,14 +66,9 @@ class ClerkExpoModule: RCTEventEmitter {
bearerToken: String?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
reject("E_NOT_INITIALIZED", "Clerk not initialized. Make sure ClerkNativeBridge is registered.", nil)
return
}

Task {
do {
try await bridge.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
resolve(nil)
} catch {
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
Expand All@@ -139,13 +80,8 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

Task {
let token = await bridge.getClientToken()
let token = await ClerkNativeBridge.shared.getClientToken()
resolve(token)
}
}
Expand All@@ -157,18 +93,13 @@ class ClerkExpoModule: RCTEventEmitter {
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

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(
try await ClerkNativeBridge.shared.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
Expand All@@ -181,8 +112,3 @@ class ClerkExpoModule: RCTEventEmitter {
}

}

/// Requests that ClerkProvider reload the JS client from native client state.
public func emitClerkNativeClientChanged(_ body: [String: Any]? = nil) {
ClerkExpoModule.emitClientChanged(body)
}
65 changes: 46 additions & 19 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
// ClerkNativeBridge - Provides app-target Clerk SDK operations and SwiftUI view controllers to ClerkExpo.
// This file is injected into the app target by the config plugin.
// It uses the ClerkKit Swift package, which is only accessible from the app target.
// ClerkNativeBridge - Provides Clerk SDK operations and SwiftUI view controllers to ClerkExpo.

import UIKit
import SwiftUI
import Observation
@_spi(FrameworkIntegration) import ClerkKit
import ClerkKitUI
import ClerkExpo // Import the pod to access ClerkNativeBridgeProtocol

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

extension Notification.Name {
static let clerkNativeSDKDidConfigure = Notification.Name("com.clerk.expo.native-sdk.did-configure")
}

private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events")
private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)?

private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {
// Replaced by the config plugin when this bridge is copied into the app target.
private static let hostSdkVersion = "__CLERK_EXPO_VERSION__"
private static var hostSdkVersion: String? {
Bundle.main.object(forInfoDictionaryKey: "ClerkExpoVersion") as? String
}

func prepare(_ request: inout URLRequest) async throws {
request.addValue("expo", forHTTPHeaderField: "x-clerk-host-sdk")
request.addValue(Self.hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
if let hostSdkVersion = Self.hostSdkVersion, !hostSdkVersion.isEmpty {
request.addValue(hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
}
}
}

// MARK: - Native Bridge Implementation

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

private static let clerkLoadMaxAttempts = 30
Expand DownExpand Up@@ -52,14 +65,10 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return Bundle.main.bundleIdentifier
}

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

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Expand All@@ -69,7 +78,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
return
}

Expand All@@ -89,13 +98,13 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
}

@MainActor
private static func emitClientChangedIfReceivedToken(_ bearerToken: String?) {
guard let token = bearerToken, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
emitClerkNativeClientChanged(Self.clientChangedPayload())
Self.emitClientChanged(Self.clientChangedPayload())
}

@MainActor
Expand DownExpand Up@@ -124,7 +133,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
if newClientState != self.lastObservedClientState {
self.lastObservedClientState = newClientState
let payload = Self.clientChangedPayload()
emitClerkNativeClientChanged(payload)
Self.emitClientChanged(payload)
}

self.observeClient(generation: generation)
Expand DownExpand Up@@ -263,7 +272,25 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

lastObservedClientState = Self.clientStateSnapshot()
emitClerkNativeClientChanged(Self.clientChangedPayload(sourceId: sourceId))
Self.emitClientChanged(Self.clientChangedPayload(sourceId: sourceId))
}

private static func postConfiguredNotification() {
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
}

static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) {
clerkNativeClientEventQueue.sync {
clerkNativeClientChangedEmitter = emitter
}
}

/// 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 {
Expand Down
29 changes: 24 additions & 5 deletions packages/expo/ios/ClerkNativeViewHost.swift
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import UIKit

public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
public class ClerkNativeViewHost: UIView {
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
private var hasInitialized: Bool = false
private var configuredObserver: NSObjectProtocol?

override public init(frame: CGRect) {
super.init(frame: frame)
Expand All@@ -12,22 +13,26 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
fatalError("init(coder:) has not been implemented")
}

deinit {
removeConfiguredObserver()
}

override public func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else {
if hasInitialized {
hostedViewDidDetachFromWindow()
}
removeClerkNativeBridgeReadyObserver(self)
removeConfiguredObserver()
hostingCoordinator.detach()
hasInitialized = false
return
}

guard !hasInitialized else { return }
hasInitialized = true
addClerkNativeBridgeReadyObserver(self)
addConfiguredObserver()
hostedViewDidAttachToWindow()
updateHostedView()
}
Expand All@@ -51,8 +56,22 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {

func hostedViewDidDetachFromWindow() {}

public func clerkNativeBridgeDidBecomeReady() {
setNeedsHostedViewUpdate()
private func addConfiguredObserver() {
guard configuredObserver == nil else { return }

configuredObserver = NotificationCenter.default.addObserver(
forName: .clerkNativeSDKDidConfigure,
object: nil,
queue: .main
) { [weak self] _ in
self?.setNeedsHostedViewUpdate()
}
}

private func removeConfiguredObserver() {
guard let configuredObserver else { return }
NotificationCenter.default.removeObserver(configuredObserver)
self.configuredObserver = nil
}

private func updateHostedView() {
Expand Down
4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkUserButtonNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,7 @@ import UIKit

public class ClerkUserButtonNativeView: ClerkNativeViewHost {
override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeUserButtonViewController()
return ClerkNativeBridge.shared.makeUserButtonViewController()
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(expo): link iOS SDK through podspec SPM dependencies by mikepitre · Pull Request #8927 · clerk/javascript · GitHub
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/tidy-expo-ios-import.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": minor
---

Fixes iOS development builds across Expo SDK versions by linking the Clerk iOS SDK through React Native's Swift Package Manager podspec support. This raises the minimum supported React Native version to 0.75, where that podspec SPM support is available; `@clerk/expo` already supports Expo SDK 53 and newer, and Expo SDK 53 ships with React Native 0.79.
384 changes: 5 additions & 379 deletions packages/expo/app.plugin.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkAuthNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,9 +46,7 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
}

override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeAuthViewController(
return ClerkNativeBridge.shared.makeAuthViewController(
mode: currentMode,
dismissible: currentDismissible,
onEvent: { [weak self] event, _ in
Expand Down
20 changes: 16 additions & 4 deletions packages/expo/ios/ClerkExpo.podspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ else
}
end

clerk_ios_repo = 'https://github.com/clerk/clerk-ios.git'
clerk_ios_version = '1.2.4'

Pod::Spec.new do |s|
s.name = 'ClerkExpo'
s.version = package['version']
Expand All@@ -35,10 +38,19 @@ Pod::Spec.new do |s|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

# Only include the module files in the pod (both Swift and ObjC bridges).
# ClerkNativeBridge.swift is injected into the app target by the config plugin
# because it uses `import ClerkKit` which is only available via SPM in the app target.
s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m",
if defined?(spm_dependency)
spm_dependency(
s,
url: clerk_ios_repo,
requirement: { :kind => 'exactVersion', :version => clerk_ios_version },
products: ['ClerkKit', 'ClerkKitUI']
)
else
raise 'ClerkExpo requires React Native 0.75 or newer for iOS Swift Package Manager dependencies.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep RN 0.73/0.74 pod installs working

For apps still on React Native 0.73/0.74 (which satisfied @clerk/expo's previous peer range and can receive this patch via a caret range), spm_dependency is not defined by RN's CocoaPods scripts, so this raise makes every iOS pod install fail even though those apps built with the old Xcode-project injection path. Because this is being released as a patch, please keep a fallback for those RN versions or avoid shipping this path to them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our supported expo range is >=53, so supporting 0.73 and 0.74 is already a mismatch. Expo 53 contains React Native 0.79

end

s.source_files = "ClerkNativeBridge.swift",
"ClerkExpoModule.swift", "ClerkExpoModule.m",
"ClerkNativeViewHost.swift",
"ClerkAuthNativeView.swift",
"ClerkAuthViewManager.m",
Expand Down
88 changes: 7 additions & 81 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,10 @@
// ClerkExpoModule - Native module for Clerk integration
// This module provides the configure function, client sync, and native view bridges.
// SwiftUI Clerk views are created by the app target through ClerkNativeBridge because
// the Clerk SDK (SPM) isn't accessible from the CocoaPods-backed React Native pod.
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.

import UIKit
import React

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

// Global registry for the app-target native bridge (set by the app target at startup)
public var clerkNativeBridge: ClerkNativeBridgeProtocol? {
didSet {
if clerkNativeBridge != nil {
emitClerkNativeBridgeReady()
}
}
}

// Protocol that the app target implements to provide Clerk SDK operations and SwiftUI views.
public protocol ClerkNativeBridgeProtocol {
// Inline rendering — returns UIViewController to preserve SwiftUI lifecycle
func makeAuthViewController(mode: String, dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserProfileViewController(dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserButtonViewController() -> UIViewController?

// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
func clerkNativeBridgeDidBecomeReady()
}

private let clerkNativeBridgeReadyObservers = NSHashTable<AnyObject>.weakObjects()

public func addClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.add(observer)
}

public func removeClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.remove(observer)
}

public func emitClerkNativeBridgeReady() {
let notifyObservers = {
for observer in clerkNativeBridgeReadyObservers.allObjects {
(observer as? ClerkNativeBridgeReadyObserver)?.clerkNativeBridgeDidBecomeReady()
}
}

if Thread.isMainThread {
notifyObservers()
} else {
DispatchQueue.main.async(execute: notifyObservers)
}
}

// MARK: - Module

@objc(ClerkExpo)
Expand All@@ -73,6 +16,9 @@ class ClerkExpoModule: RCTEventEmitter {
override init() {
super.init()
ClerkExpoModule.sharedInstance = self
ClerkNativeBridge.setClientChangedEmitter { body in
Self.emitClientChanged(body)
}
}

@objc override static func requiresMainQueueSetup() -> Bool {
Expand DownExpand Up@@ -120,14 +66,9 @@ class ClerkExpoModule: RCTEventEmitter {
bearerToken: String?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
reject("E_NOT_INITIALIZED", "Clerk not initialized. Make sure ClerkNativeBridge is registered.", nil)
return
}

Task {
do {
try await bridge.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
resolve(nil)
} catch {
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
Expand All@@ -139,13 +80,8 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

Task {
let token = await bridge.getClientToken()
let token = await ClerkNativeBridge.shared.getClientToken()
resolve(token)
}
}
Expand All@@ -157,18 +93,13 @@ class ClerkExpoModule: RCTEventEmitter {
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

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(
try await ClerkNativeBridge.shared.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
Expand All@@ -181,8 +112,3 @@ class ClerkExpoModule: RCTEventEmitter {
}

}

/// Requests that ClerkProvider reload the JS client from native client state.
public func emitClerkNativeClientChanged(_ body: [String: Any]? = nil) {
ClerkExpoModule.emitClientChanged(body)
}
65 changes: 46 additions & 19 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
// ClerkNativeBridge - Provides app-target Clerk SDK operations and SwiftUI view controllers to ClerkExpo.
// This file is injected into the app target by the config plugin.
// It uses the ClerkKit Swift package, which is only accessible from the app target.
// ClerkNativeBridge - Provides Clerk SDK operations and SwiftUI view controllers to ClerkExpo.

import UIKit
import SwiftUI
import Observation
@_spi(FrameworkIntegration) import ClerkKit
import ClerkKitUI
import ClerkExpo // Import the pod to access ClerkNativeBridgeProtocol

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

extension Notification.Name {
static let clerkNativeSDKDidConfigure = Notification.Name("com.clerk.expo.native-sdk.did-configure")
}

private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events")
private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)?

private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {
// Replaced by the config plugin when this bridge is copied into the app target.
private static let hostSdkVersion = "__CLERK_EXPO_VERSION__"
private static var hostSdkVersion: String? {
Bundle.main.object(forInfoDictionaryKey: "ClerkExpoVersion") as? String
}

func prepare(_ request: inout URLRequest) async throws {
request.addValue("expo", forHTTPHeaderField: "x-clerk-host-sdk")
request.addValue(Self.hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
if let hostSdkVersion = Self.hostSdkVersion, !hostSdkVersion.isEmpty {
request.addValue(hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
}
}
}

// MARK: - Native Bridge Implementation

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

private static let clerkLoadMaxAttempts = 30
Expand DownExpand Up@@ -52,14 +65,10 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return Bundle.main.bundleIdentifier
}

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

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Expand All@@ -69,7 +78,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
return
}

Expand All@@ -89,13 +98,13 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
}

@MainActor
private static func emitClientChangedIfReceivedToken(_ bearerToken: String?) {
guard let token = bearerToken, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
emitClerkNativeClientChanged(Self.clientChangedPayload())
Self.emitClientChanged(Self.clientChangedPayload())
}

@MainActor
Expand DownExpand Up@@ -124,7 +133,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
if newClientState != self.lastObservedClientState {
self.lastObservedClientState = newClientState
let payload = Self.clientChangedPayload()
emitClerkNativeClientChanged(payload)
Self.emitClientChanged(payload)
}

self.observeClient(generation: generation)
Expand DownExpand Up@@ -263,7 +272,25 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

lastObservedClientState = Self.clientStateSnapshot()
emitClerkNativeClientChanged(Self.clientChangedPayload(sourceId: sourceId))
Self.emitClientChanged(Self.clientChangedPayload(sourceId: sourceId))
}

private static func postConfiguredNotification() {
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
}

static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) {
clerkNativeClientEventQueue.sync {
clerkNativeClientChangedEmitter = emitter
}
}

/// 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 {
Expand Down
29 changes: 24 additions & 5 deletions packages/expo/ios/ClerkNativeViewHost.swift
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import UIKit

public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
public class ClerkNativeViewHost: UIView {
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
private var hasInitialized: Bool = false
private var configuredObserver: NSObjectProtocol?

override public init(frame: CGRect) {
super.init(frame: frame)
Expand All@@ -12,22 +13,26 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
fatalError("init(coder:) has not been implemented")
}

deinit {
removeConfiguredObserver()
}

override public func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else {
if hasInitialized {
hostedViewDidDetachFromWindow()
}
removeClerkNativeBridgeReadyObserver(self)
removeConfiguredObserver()
hostingCoordinator.detach()
hasInitialized = false
return
}

guard !hasInitialized else { return }
hasInitialized = true
addClerkNativeBridgeReadyObserver(self)
addConfiguredObserver()
hostedViewDidAttachToWindow()
updateHostedView()
}
Expand All@@ -51,8 +56,22 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {

func hostedViewDidDetachFromWindow() {}

public func clerkNativeBridgeDidBecomeReady() {
setNeedsHostedViewUpdate()
private func addConfiguredObserver() {
guard configuredObserver == nil else { return }

configuredObserver = NotificationCenter.default.addObserver(
forName: .clerkNativeSDKDidConfigure,
object: nil,
queue: .main
) { [weak self] _ in
self?.setNeedsHostedViewUpdate()
}
}

private func removeConfiguredObserver() {
guard let configuredObserver else { return }
NotificationCenter.default.removeObserver(configuredObserver)
self.configuredObserver = nil
}

private func updateHostedView() {
Expand Down
4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkUserButtonNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,7 @@ import UIKit

public class ClerkUserButtonNativeView: ClerkNativeViewHost {
override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeUserButtonViewController()
return ClerkNativeBridge.shared.makeUserButtonViewController()
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(expo): link iOS SDK through podspec SPM dependencies by mikepitre · Pull Request #8927 · clerk/javascript · GitHub
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/tidy-expo-ios-import.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": minor
---

Fixes iOS development builds across Expo SDK versions by linking the Clerk iOS SDK through React Native's Swift Package Manager podspec support. This raises the minimum supported React Native version to 0.75, where that podspec SPM support is available; `@clerk/expo` already supports Expo SDK 53 and newer, and Expo SDK 53 ships with React Native 0.79.
384 changes: 5 additions & 379 deletions packages/expo/app.plugin.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkAuthNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,9 +46,7 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
}

override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeAuthViewController(
return ClerkNativeBridge.shared.makeAuthViewController(
mode: currentMode,
dismissible: currentDismissible,
onEvent: { [weak self] event, _ in
Expand Down
20 changes: 16 additions & 4 deletions packages/expo/ios/ClerkExpo.podspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ else
}
end

clerk_ios_repo = 'https://github.com/clerk/clerk-ios.git'
clerk_ios_version = '1.2.4'

Pod::Spec.new do |s|
s.name = 'ClerkExpo'
s.version = package['version']
Expand All@@ -35,10 +38,19 @@ Pod::Spec.new do |s|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

# Only include the module files in the pod (both Swift and ObjC bridges).
# ClerkNativeBridge.swift is injected into the app target by the config plugin
# because it uses `import ClerkKit` which is only available via SPM in the app target.
s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m",
if defined?(spm_dependency)
spm_dependency(
s,
url: clerk_ios_repo,
requirement: { :kind => 'exactVersion', :version => clerk_ios_version },
products: ['ClerkKit', 'ClerkKitUI']
)
else
raise 'ClerkExpo requires React Native 0.75 or newer for iOS Swift Package Manager dependencies.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep RN 0.73/0.74 pod installs working

For apps still on React Native 0.73/0.74 (which satisfied @clerk/expo's previous peer range and can receive this patch via a caret range), spm_dependency is not defined by RN's CocoaPods scripts, so this raise makes every iOS pod install fail even though those apps built with the old Xcode-project injection path. Because this is being released as a patch, please keep a fallback for those RN versions or avoid shipping this path to them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our supported expo range is >=53, so supporting 0.73 and 0.74 is already a mismatch. Expo 53 contains React Native 0.79

end

s.source_files = "ClerkNativeBridge.swift",
"ClerkExpoModule.swift", "ClerkExpoModule.m",
"ClerkNativeViewHost.swift",
"ClerkAuthNativeView.swift",
"ClerkAuthViewManager.m",
Expand Down
88 changes: 7 additions & 81 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,10 @@
// ClerkExpoModule - Native module for Clerk integration
// This module provides the configure function, client sync, and native view bridges.
// SwiftUI Clerk views are created by the app target through ClerkNativeBridge because
// the Clerk SDK (SPM) isn't accessible from the CocoaPods-backed React Native pod.
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.

import UIKit
import React

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

// Global registry for the app-target native bridge (set by the app target at startup)
public var clerkNativeBridge: ClerkNativeBridgeProtocol? {
didSet {
if clerkNativeBridge != nil {
emitClerkNativeBridgeReady()
}
}
}

// Protocol that the app target implements to provide Clerk SDK operations and SwiftUI views.
public protocol ClerkNativeBridgeProtocol {
// Inline rendering — returns UIViewController to preserve SwiftUI lifecycle
func makeAuthViewController(mode: String, dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserProfileViewController(dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserButtonViewController() -> UIViewController?

// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
func clerkNativeBridgeDidBecomeReady()
}

private let clerkNativeBridgeReadyObservers = NSHashTable<AnyObject>.weakObjects()

public func addClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.add(observer)
}

public func removeClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.remove(observer)
}

public func emitClerkNativeBridgeReady() {
let notifyObservers = {
for observer in clerkNativeBridgeReadyObservers.allObjects {
(observer as? ClerkNativeBridgeReadyObserver)?.clerkNativeBridgeDidBecomeReady()
}
}

if Thread.isMainThread {
notifyObservers()
} else {
DispatchQueue.main.async(execute: notifyObservers)
}
}

// MARK: - Module

@objc(ClerkExpo)
Expand All@@ -73,6 +16,9 @@ class ClerkExpoModule: RCTEventEmitter {
override init() {
super.init()
ClerkExpoModule.sharedInstance = self
ClerkNativeBridge.setClientChangedEmitter { body in
Self.emitClientChanged(body)
}
}

@objc override static func requiresMainQueueSetup() -> Bool {
Expand DownExpand Up@@ -120,14 +66,9 @@ class ClerkExpoModule: RCTEventEmitter {
bearerToken: String?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
reject("E_NOT_INITIALIZED", "Clerk not initialized. Make sure ClerkNativeBridge is registered.", nil)
return
}

Task {
do {
try await bridge.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
resolve(nil)
} catch {
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
Expand All@@ -139,13 +80,8 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

Task {
let token = await bridge.getClientToken()
let token = await ClerkNativeBridge.shared.getClientToken()
resolve(token)
}
}
Expand All@@ -157,18 +93,13 @@ class ClerkExpoModule: RCTEventEmitter {
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

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(
try await ClerkNativeBridge.shared.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
Expand All@@ -181,8 +112,3 @@ class ClerkExpoModule: RCTEventEmitter {
}

}

/// Requests that ClerkProvider reload the JS client from native client state.
public func emitClerkNativeClientChanged(_ body: [String: Any]? = nil) {
ClerkExpoModule.emitClientChanged(body)
}
65 changes: 46 additions & 19 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
// ClerkNativeBridge - Provides app-target Clerk SDK operations and SwiftUI view controllers to ClerkExpo.
// This file is injected into the app target by the config plugin.
// It uses the ClerkKit Swift package, which is only accessible from the app target.
// ClerkNativeBridge - Provides Clerk SDK operations and SwiftUI view controllers to ClerkExpo.

import UIKit
import SwiftUI
import Observation
@_spi(FrameworkIntegration) import ClerkKit
import ClerkKitUI
import ClerkExpo // Import the pod to access ClerkNativeBridgeProtocol

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

extension Notification.Name {
static let clerkNativeSDKDidConfigure = Notification.Name("com.clerk.expo.native-sdk.did-configure")
}

private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events")
private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)?

private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {
// Replaced by the config plugin when this bridge is copied into the app target.
private static let hostSdkVersion = "__CLERK_EXPO_VERSION__"
private static var hostSdkVersion: String? {
Bundle.main.object(forInfoDictionaryKey: "ClerkExpoVersion") as? String
}

func prepare(_ request: inout URLRequest) async throws {
request.addValue("expo", forHTTPHeaderField: "x-clerk-host-sdk")
request.addValue(Self.hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
if let hostSdkVersion = Self.hostSdkVersion, !hostSdkVersion.isEmpty {
request.addValue(hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
}
}
}

// MARK: - Native Bridge Implementation

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

private static let clerkLoadMaxAttempts = 30
Expand DownExpand Up@@ -52,14 +65,10 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return Bundle.main.bundleIdentifier
}

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

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Expand All@@ -69,7 +78,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
return
}

Expand All@@ -89,13 +98,13 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
}

@MainActor
private static func emitClientChangedIfReceivedToken(_ bearerToken: String?) {
guard let token = bearerToken, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
emitClerkNativeClientChanged(Self.clientChangedPayload())
Self.emitClientChanged(Self.clientChangedPayload())
}

@MainActor
Expand DownExpand Up@@ -124,7 +133,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
if newClientState != self.lastObservedClientState {
self.lastObservedClientState = newClientState
let payload = Self.clientChangedPayload()
emitClerkNativeClientChanged(payload)
Self.emitClientChanged(payload)
}

self.observeClient(generation: generation)
Expand DownExpand Up@@ -263,7 +272,25 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

lastObservedClientState = Self.clientStateSnapshot()
emitClerkNativeClientChanged(Self.clientChangedPayload(sourceId: sourceId))
Self.emitClientChanged(Self.clientChangedPayload(sourceId: sourceId))
}

private static func postConfiguredNotification() {
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
}

static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) {
clerkNativeClientEventQueue.sync {
clerkNativeClientChangedEmitter = emitter
}
}

/// 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 {
Expand Down
29 changes: 24 additions & 5 deletions packages/expo/ios/ClerkNativeViewHost.swift
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import UIKit

public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
public class ClerkNativeViewHost: UIView {
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
private var hasInitialized: Bool = false
private var configuredObserver: NSObjectProtocol?

override public init(frame: CGRect) {
super.init(frame: frame)
Expand All@@ -12,22 +13,26 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
fatalError("init(coder:) has not been implemented")
}

deinit {
removeConfiguredObserver()
}

override public func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else {
if hasInitialized {
hostedViewDidDetachFromWindow()
}
removeClerkNativeBridgeReadyObserver(self)
removeConfiguredObserver()
hostingCoordinator.detach()
hasInitialized = false
return
}

guard !hasInitialized else { return }
hasInitialized = true
addClerkNativeBridgeReadyObserver(self)
addConfiguredObserver()
hostedViewDidAttachToWindow()
updateHostedView()
}
Expand All@@ -51,8 +56,22 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {

func hostedViewDidDetachFromWindow() {}

public func clerkNativeBridgeDidBecomeReady() {
setNeedsHostedViewUpdate()
private func addConfiguredObserver() {
guard configuredObserver == nil else { return }

configuredObserver = NotificationCenter.default.addObserver(
forName: .clerkNativeSDKDidConfigure,
object: nil,
queue: .main
) { [weak self] _ in
self?.setNeedsHostedViewUpdate()
}
}

private func removeConfiguredObserver() {
guard let configuredObserver else { return }
NotificationCenter.default.removeObserver(configuredObserver)
self.configuredObserver = nil
}

private func updateHostedView() {
Expand Down
4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkUserButtonNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,7 @@ import UIKit

public class ClerkUserButtonNativeView: ClerkNativeViewHost {
override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeUserButtonViewController()
return ClerkNativeBridge.shared.makeUserButtonViewController()
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(expo): link iOS SDK through podspec SPM dependencies by mikepitre · Pull Request #8927 · clerk/javascript · GitHub
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/tidy-expo-ios-import.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": minor
---

Fixes iOS development builds across Expo SDK versions by linking the Clerk iOS SDK through React Native's Swift Package Manager podspec support. This raises the minimum supported React Native version to 0.75, where that podspec SPM support is available; `@clerk/expo` already supports Expo SDK 53 and newer, and Expo SDK 53 ships with React Native 0.79.
384 changes: 5 additions & 379 deletions packages/expo/app.plugin.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkAuthNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,9 +46,7 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
}

override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeAuthViewController(
return ClerkNativeBridge.shared.makeAuthViewController(
mode: currentMode,
dismissible: currentDismissible,
onEvent: { [weak self] event, _ in
Expand Down
20 changes: 16 additions & 4 deletions packages/expo/ios/ClerkExpo.podspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ else
}
end

clerk_ios_repo = 'https://github.com/clerk/clerk-ios.git'
clerk_ios_version = '1.2.4'

Pod::Spec.new do |s|
s.name = 'ClerkExpo'
s.version = package['version']
Expand All@@ -35,10 +38,19 @@ Pod::Spec.new do |s|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

# Only include the module files in the pod (both Swift and ObjC bridges).
# ClerkNativeBridge.swift is injected into the app target by the config plugin
# because it uses `import ClerkKit` which is only available via SPM in the app target.
s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m",
if defined?(spm_dependency)
spm_dependency(
s,
url: clerk_ios_repo,
requirement: { :kind => 'exactVersion', :version => clerk_ios_version },
products: ['ClerkKit', 'ClerkKitUI']
)
else
raise 'ClerkExpo requires React Native 0.75 or newer for iOS Swift Package Manager dependencies.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep RN 0.73/0.74 pod installs working

For apps still on React Native 0.73/0.74 (which satisfied @clerk/expo's previous peer range and can receive this patch via a caret range), spm_dependency is not defined by RN's CocoaPods scripts, so this raise makes every iOS pod install fail even though those apps built with the old Xcode-project injection path. Because this is being released as a patch, please keep a fallback for those RN versions or avoid shipping this path to them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our supported expo range is >=53, so supporting 0.73 and 0.74 is already a mismatch. Expo 53 contains React Native 0.79

end

s.source_files = "ClerkNativeBridge.swift",
"ClerkExpoModule.swift", "ClerkExpoModule.m",
"ClerkNativeViewHost.swift",
"ClerkAuthNativeView.swift",
"ClerkAuthViewManager.m",
Expand Down
88 changes: 7 additions & 81 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,10 @@
// ClerkExpoModule - Native module for Clerk integration
// This module provides the configure function, client sync, and native view bridges.
// SwiftUI Clerk views are created by the app target through ClerkNativeBridge because
// the Clerk SDK (SPM) isn't accessible from the CocoaPods-backed React Native pod.
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.

import UIKit
import React

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

// Global registry for the app-target native bridge (set by the app target at startup)
public var clerkNativeBridge: ClerkNativeBridgeProtocol? {
didSet {
if clerkNativeBridge != nil {
emitClerkNativeBridgeReady()
}
}
}

// Protocol that the app target implements to provide Clerk SDK operations and SwiftUI views.
public protocol ClerkNativeBridgeProtocol {
// Inline rendering — returns UIViewController to preserve SwiftUI lifecycle
func makeAuthViewController(mode: String, dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserProfileViewController(dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserButtonViewController() -> UIViewController?

// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
func clerkNativeBridgeDidBecomeReady()
}

private let clerkNativeBridgeReadyObservers = NSHashTable<AnyObject>.weakObjects()

public func addClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.add(observer)
}

public func removeClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.remove(observer)
}

public func emitClerkNativeBridgeReady() {
let notifyObservers = {
for observer in clerkNativeBridgeReadyObservers.allObjects {
(observer as? ClerkNativeBridgeReadyObserver)?.clerkNativeBridgeDidBecomeReady()
}
}

if Thread.isMainThread {
notifyObservers()
} else {
DispatchQueue.main.async(execute: notifyObservers)
}
}

// MARK: - Module

@objc(ClerkExpo)
Expand All@@ -73,6 +16,9 @@ class ClerkExpoModule: RCTEventEmitter {
override init() {
super.init()
ClerkExpoModule.sharedInstance = self
ClerkNativeBridge.setClientChangedEmitter { body in
Self.emitClientChanged(body)
}
}

@objc override static func requiresMainQueueSetup() -> Bool {
Expand DownExpand Up@@ -120,14 +66,9 @@ class ClerkExpoModule: RCTEventEmitter {
bearerToken: String?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
reject("E_NOT_INITIALIZED", "Clerk not initialized. Make sure ClerkNativeBridge is registered.", nil)
return
}

Task {
do {
try await bridge.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
resolve(nil)
} catch {
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
Expand All@@ -139,13 +80,8 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

Task {
let token = await bridge.getClientToken()
let token = await ClerkNativeBridge.shared.getClientToken()
resolve(token)
}
}
Expand All@@ -157,18 +93,13 @@ class ClerkExpoModule: RCTEventEmitter {
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

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(
try await ClerkNativeBridge.shared.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
Expand All@@ -181,8 +112,3 @@ class ClerkExpoModule: RCTEventEmitter {
}

}

/// Requests that ClerkProvider reload the JS client from native client state.
public func emitClerkNativeClientChanged(_ body: [String: Any]? = nil) {
ClerkExpoModule.emitClientChanged(body)
}
65 changes: 46 additions & 19 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
// ClerkNativeBridge - Provides app-target Clerk SDK operations and SwiftUI view controllers to ClerkExpo.
// This file is injected into the app target by the config plugin.
// It uses the ClerkKit Swift package, which is only accessible from the app target.
// ClerkNativeBridge - Provides Clerk SDK operations and SwiftUI view controllers to ClerkExpo.

import UIKit
import SwiftUI
import Observation
@_spi(FrameworkIntegration) import ClerkKit
import ClerkKitUI
import ClerkExpo // Import the pod to access ClerkNativeBridgeProtocol

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

extension Notification.Name {
static let clerkNativeSDKDidConfigure = Notification.Name("com.clerk.expo.native-sdk.did-configure")
}

private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events")
private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)?

private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {
// Replaced by the config plugin when this bridge is copied into the app target.
private static let hostSdkVersion = "__CLERK_EXPO_VERSION__"
private static var hostSdkVersion: String? {
Bundle.main.object(forInfoDictionaryKey: "ClerkExpoVersion") as? String
}

func prepare(_ request: inout URLRequest) async throws {
request.addValue("expo", forHTTPHeaderField: "x-clerk-host-sdk")
request.addValue(Self.hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
if let hostSdkVersion = Self.hostSdkVersion, !hostSdkVersion.isEmpty {
request.addValue(hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
}
}
}

// MARK: - Native Bridge Implementation

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

private static let clerkLoadMaxAttempts = 30
Expand DownExpand Up@@ -52,14 +65,10 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return Bundle.main.bundleIdentifier
}

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

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Expand All@@ -69,7 +78,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
return
}

Expand All@@ -89,13 +98,13 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
}

@MainActor
private static func emitClientChangedIfReceivedToken(_ bearerToken: String?) {
guard let token = bearerToken, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
emitClerkNativeClientChanged(Self.clientChangedPayload())
Self.emitClientChanged(Self.clientChangedPayload())
}

@MainActor
Expand DownExpand Up@@ -124,7 +133,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
if newClientState != self.lastObservedClientState {
self.lastObservedClientState = newClientState
let payload = Self.clientChangedPayload()
emitClerkNativeClientChanged(payload)
Self.emitClientChanged(payload)
}

self.observeClient(generation: generation)
Expand DownExpand Up@@ -263,7 +272,25 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

lastObservedClientState = Self.clientStateSnapshot()
emitClerkNativeClientChanged(Self.clientChangedPayload(sourceId: sourceId))
Self.emitClientChanged(Self.clientChangedPayload(sourceId: sourceId))
}

private static func postConfiguredNotification() {
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
}

static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) {
clerkNativeClientEventQueue.sync {
clerkNativeClientChangedEmitter = emitter
}
}

/// 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 {
Expand Down
29 changes: 24 additions & 5 deletions packages/expo/ios/ClerkNativeViewHost.swift
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import UIKit

public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
public class ClerkNativeViewHost: UIView {
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
private var hasInitialized: Bool = false
private var configuredObserver: NSObjectProtocol?

override public init(frame: CGRect) {
super.init(frame: frame)
Expand All@@ -12,22 +13,26 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
fatalError("init(coder:) has not been implemented")
}

deinit {
removeConfiguredObserver()
}

override public func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else {
if hasInitialized {
hostedViewDidDetachFromWindow()
}
removeClerkNativeBridgeReadyObserver(self)
removeConfiguredObserver()
hostingCoordinator.detach()
hasInitialized = false
return
}

guard !hasInitialized else { return }
hasInitialized = true
addClerkNativeBridgeReadyObserver(self)
addConfiguredObserver()
hostedViewDidAttachToWindow()
updateHostedView()
}
Expand All@@ -51,8 +56,22 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {

func hostedViewDidDetachFromWindow() {}

public func clerkNativeBridgeDidBecomeReady() {
setNeedsHostedViewUpdate()
private func addConfiguredObserver() {
guard configuredObserver == nil else { return }

configuredObserver = NotificationCenter.default.addObserver(
forName: .clerkNativeSDKDidConfigure,
object: nil,
queue: .main
) { [weak self] _ in
self?.setNeedsHostedViewUpdate()
}
}

private func removeConfiguredObserver() {
guard let configuredObserver else { return }
NotificationCenter.default.removeObserver(configuredObserver)
self.configuredObserver = nil
}

private func updateHostedView() {
Expand Down
4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkUserButtonNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,7 @@ import UIKit

public class ClerkUserButtonNativeView: ClerkNativeViewHost {
override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeUserButtonViewController()
return ClerkNativeBridge.shared.makeUserButtonViewController()
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(expo): link iOS SDK through podspec SPM dependencies by mikepitre · Pull Request #8927 · clerk/javascript · GitHub
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/tidy-expo-ios-import.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": minor
---

Fixes iOS development builds across Expo SDK versions by linking the Clerk iOS SDK through React Native's Swift Package Manager podspec support. This raises the minimum supported React Native version to 0.75, where that podspec SPM support is available; `@clerk/expo` already supports Expo SDK 53 and newer, and Expo SDK 53 ships with React Native 0.79.
384 changes: 5 additions & 379 deletions packages/expo/app.plugin.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkAuthNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,9 +46,7 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
}

override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeAuthViewController(
return ClerkNativeBridge.shared.makeAuthViewController(
mode: currentMode,
dismissible: currentDismissible,
onEvent: { [weak self] event, _ in
Expand Down
20 changes: 16 additions & 4 deletions packages/expo/ios/ClerkExpo.podspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ else
}
end

clerk_ios_repo = 'https://github.com/clerk/clerk-ios.git'
clerk_ios_version = '1.2.4'

Pod::Spec.new do |s|
s.name = 'ClerkExpo'
s.version = package['version']
Expand All@@ -35,10 +38,19 @@ Pod::Spec.new do |s|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

# Only include the module files in the pod (both Swift and ObjC bridges).
# ClerkNativeBridge.swift is injected into the app target by the config plugin
# because it uses `import ClerkKit` which is only available via SPM in the app target.
s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m",
if defined?(spm_dependency)
spm_dependency(
s,
url: clerk_ios_repo,
requirement: { :kind => 'exactVersion', :version => clerk_ios_version },
products: ['ClerkKit', 'ClerkKitUI']
)
else
raise 'ClerkExpo requires React Native 0.75 or newer for iOS Swift Package Manager dependencies.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep RN 0.73/0.74 pod installs working

For apps still on React Native 0.73/0.74 (which satisfied @clerk/expo's previous peer range and can receive this patch via a caret range), spm_dependency is not defined by RN's CocoaPods scripts, so this raise makes every iOS pod install fail even though those apps built with the old Xcode-project injection path. Because this is being released as a patch, please keep a fallback for those RN versions or avoid shipping this path to them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our supported expo range is >=53, so supporting 0.73 and 0.74 is already a mismatch. Expo 53 contains React Native 0.79

end

s.source_files = "ClerkNativeBridge.swift",
"ClerkExpoModule.swift", "ClerkExpoModule.m",
"ClerkNativeViewHost.swift",
"ClerkAuthNativeView.swift",
"ClerkAuthViewManager.m",
Expand Down
88 changes: 7 additions & 81 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,10 @@
// ClerkExpoModule - Native module for Clerk integration
// This module provides the configure function, client sync, and native view bridges.
// SwiftUI Clerk views are created by the app target through ClerkNativeBridge because
// the Clerk SDK (SPM) isn't accessible from the CocoaPods-backed React Native pod.
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.

import UIKit
import React

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

// Global registry for the app-target native bridge (set by the app target at startup)
public var clerkNativeBridge: ClerkNativeBridgeProtocol? {
didSet {
if clerkNativeBridge != nil {
emitClerkNativeBridgeReady()
}
}
}

// Protocol that the app target implements to provide Clerk SDK operations and SwiftUI views.
public protocol ClerkNativeBridgeProtocol {
// Inline rendering — returns UIViewController to preserve SwiftUI lifecycle
func makeAuthViewController(mode: String, dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserProfileViewController(dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserButtonViewController() -> UIViewController?

// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
func clerkNativeBridgeDidBecomeReady()
}

private let clerkNativeBridgeReadyObservers = NSHashTable<AnyObject>.weakObjects()

public func addClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.add(observer)
}

public func removeClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.remove(observer)
}

public func emitClerkNativeBridgeReady() {
let notifyObservers = {
for observer in clerkNativeBridgeReadyObservers.allObjects {
(observer as? ClerkNativeBridgeReadyObserver)?.clerkNativeBridgeDidBecomeReady()
}
}

if Thread.isMainThread {
notifyObservers()
} else {
DispatchQueue.main.async(execute: notifyObservers)
}
}

// MARK: - Module

@objc(ClerkExpo)
Expand All@@ -73,6 +16,9 @@ class ClerkExpoModule: RCTEventEmitter {
override init() {
super.init()
ClerkExpoModule.sharedInstance = self
ClerkNativeBridge.setClientChangedEmitter { body in
Self.emitClientChanged(body)
}
}

@objc override static func requiresMainQueueSetup() -> Bool {
Expand DownExpand Up@@ -120,14 +66,9 @@ class ClerkExpoModule: RCTEventEmitter {
bearerToken: String?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
reject("E_NOT_INITIALIZED", "Clerk not initialized. Make sure ClerkNativeBridge is registered.", nil)
return
}

Task {
do {
try await bridge.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
resolve(nil)
} catch {
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
Expand All@@ -139,13 +80,8 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

Task {
let token = await bridge.getClientToken()
let token = await ClerkNativeBridge.shared.getClientToken()
resolve(token)
}
}
Expand All@@ -157,18 +93,13 @@ class ClerkExpoModule: RCTEventEmitter {
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

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(
try await ClerkNativeBridge.shared.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
Expand All@@ -181,8 +112,3 @@ class ClerkExpoModule: RCTEventEmitter {
}

}

/// Requests that ClerkProvider reload the JS client from native client state.
public func emitClerkNativeClientChanged(_ body: [String: Any]? = nil) {
ClerkExpoModule.emitClientChanged(body)
}
65 changes: 46 additions & 19 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
// ClerkNativeBridge - Provides app-target Clerk SDK operations and SwiftUI view controllers to ClerkExpo.
// This file is injected into the app target by the config plugin.
// It uses the ClerkKit Swift package, which is only accessible from the app target.
// ClerkNativeBridge - Provides Clerk SDK operations and SwiftUI view controllers to ClerkExpo.

import UIKit
import SwiftUI
import Observation
@_spi(FrameworkIntegration) import ClerkKit
import ClerkKitUI
import ClerkExpo // Import the pod to access ClerkNativeBridgeProtocol

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

extension Notification.Name {
static let clerkNativeSDKDidConfigure = Notification.Name("com.clerk.expo.native-sdk.did-configure")
}

private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events")
private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)?

private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {
// Replaced by the config plugin when this bridge is copied into the app target.
private static let hostSdkVersion = "__CLERK_EXPO_VERSION__"
private static var hostSdkVersion: String? {
Bundle.main.object(forInfoDictionaryKey: "ClerkExpoVersion") as? String
}

func prepare(_ request: inout URLRequest) async throws {
request.addValue("expo", forHTTPHeaderField: "x-clerk-host-sdk")
request.addValue(Self.hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
if let hostSdkVersion = Self.hostSdkVersion, !hostSdkVersion.isEmpty {
request.addValue(hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
}
}
}

// MARK: - Native Bridge Implementation

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

private static let clerkLoadMaxAttempts = 30
Expand DownExpand Up@@ -52,14 +65,10 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return Bundle.main.bundleIdentifier
}

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

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Expand All@@ -69,7 +78,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
return
}

Expand All@@ -89,13 +98,13 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
}

@MainActor
private static func emitClientChangedIfReceivedToken(_ bearerToken: String?) {
guard let token = bearerToken, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
emitClerkNativeClientChanged(Self.clientChangedPayload())
Self.emitClientChanged(Self.clientChangedPayload())
}

@MainActor
Expand DownExpand Up@@ -124,7 +133,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
if newClientState != self.lastObservedClientState {
self.lastObservedClientState = newClientState
let payload = Self.clientChangedPayload()
emitClerkNativeClientChanged(payload)
Self.emitClientChanged(payload)
}

self.observeClient(generation: generation)
Expand DownExpand Up@@ -263,7 +272,25 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

lastObservedClientState = Self.clientStateSnapshot()
emitClerkNativeClientChanged(Self.clientChangedPayload(sourceId: sourceId))
Self.emitClientChanged(Self.clientChangedPayload(sourceId: sourceId))
}

private static func postConfiguredNotification() {
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
}

static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) {
clerkNativeClientEventQueue.sync {
clerkNativeClientChangedEmitter = emitter
}
}

/// 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 {
Expand Down
29 changes: 24 additions & 5 deletions packages/expo/ios/ClerkNativeViewHost.swift
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import UIKit

public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
public class ClerkNativeViewHost: UIView {
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
private var hasInitialized: Bool = false
private var configuredObserver: NSObjectProtocol?

override public init(frame: CGRect) {
super.init(frame: frame)
Expand All@@ -12,22 +13,26 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
fatalError("init(coder:) has not been implemented")
}

deinit {
removeConfiguredObserver()
}

override public func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else {
if hasInitialized {
hostedViewDidDetachFromWindow()
}
removeClerkNativeBridgeReadyObserver(self)
removeConfiguredObserver()
hostingCoordinator.detach()
hasInitialized = false
return
}

guard !hasInitialized else { return }
hasInitialized = true
addClerkNativeBridgeReadyObserver(self)
addConfiguredObserver()
hostedViewDidAttachToWindow()
updateHostedView()
}
Expand All@@ -51,8 +56,22 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {

func hostedViewDidDetachFromWindow() {}

public func clerkNativeBridgeDidBecomeReady() {
setNeedsHostedViewUpdate()
private func addConfiguredObserver() {
guard configuredObserver == nil else { return }

configuredObserver = NotificationCenter.default.addObserver(
forName: .clerkNativeSDKDidConfigure,
object: nil,
queue: .main
) { [weak self] _ in
self?.setNeedsHostedViewUpdate()
}
}

private func removeConfiguredObserver() {
guard let configuredObserver else { return }
NotificationCenter.default.removeObserver(configuredObserver)
self.configuredObserver = nil
}

private func updateHostedView() {
Expand Down
4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkUserButtonNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,7 @@ import UIKit

public class ClerkUserButtonNativeView: ClerkNativeViewHost {
override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeUserButtonViewController()
return ClerkNativeBridge.shared.makeUserButtonViewController()
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(expo): link iOS SDK through podspec SPM dependencies by mikepitre · Pull Request #8927 · clerk/javascript · GitHub
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/tidy-expo-ios-import.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": minor
---

Fixes iOS development builds across Expo SDK versions by linking the Clerk iOS SDK through React Native's Swift Package Manager podspec support. This raises the minimum supported React Native version to 0.75, where that podspec SPM support is available; `@clerk/expo` already supports Expo SDK 53 and newer, and Expo SDK 53 ships with React Native 0.79.
384 changes: 5 additions & 379 deletions packages/expo/app.plugin.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkAuthNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,9 +46,7 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
}

override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeAuthViewController(
return ClerkNativeBridge.shared.makeAuthViewController(
mode: currentMode,
dismissible: currentDismissible,
onEvent: { [weak self] event, _ in
Expand Down
20 changes: 16 additions & 4 deletions packages/expo/ios/ClerkExpo.podspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ else
}
end

clerk_ios_repo = 'https://github.com/clerk/clerk-ios.git'
clerk_ios_version = '1.2.4'

Pod::Spec.new do |s|
s.name = 'ClerkExpo'
s.version = package['version']
Expand All@@ -35,10 +38,19 @@ Pod::Spec.new do |s|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

# Only include the module files in the pod (both Swift and ObjC bridges).
# ClerkNativeBridge.swift is injected into the app target by the config plugin
# because it uses `import ClerkKit` which is only available via SPM in the app target.
s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m",
if defined?(spm_dependency)
spm_dependency(
s,
url: clerk_ios_repo,
requirement: { :kind => 'exactVersion', :version => clerk_ios_version },
products: ['ClerkKit', 'ClerkKitUI']
)
else
raise 'ClerkExpo requires React Native 0.75 or newer for iOS Swift Package Manager dependencies.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep RN 0.73/0.74 pod installs working

For apps still on React Native 0.73/0.74 (which satisfied @clerk/expo's previous peer range and can receive this patch via a caret range), spm_dependency is not defined by RN's CocoaPods scripts, so this raise makes every iOS pod install fail even though those apps built with the old Xcode-project injection path. Because this is being released as a patch, please keep a fallback for those RN versions or avoid shipping this path to them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our supported expo range is >=53, so supporting 0.73 and 0.74 is already a mismatch. Expo 53 contains React Native 0.79

end

s.source_files = "ClerkNativeBridge.swift",
"ClerkExpoModule.swift", "ClerkExpoModule.m",
"ClerkNativeViewHost.swift",
"ClerkAuthNativeView.swift",
"ClerkAuthViewManager.m",
Expand Down
88 changes: 7 additions & 81 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,10 @@
// ClerkExpoModule - Native module for Clerk integration
// This module provides the configure function, client sync, and native view bridges.
// SwiftUI Clerk views are created by the app target through ClerkNativeBridge because
// the Clerk SDK (SPM) isn't accessible from the CocoaPods-backed React Native pod.
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.

import UIKit
import React

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

// Global registry for the app-target native bridge (set by the app target at startup)
public var clerkNativeBridge: ClerkNativeBridgeProtocol? {
didSet {
if clerkNativeBridge != nil {
emitClerkNativeBridgeReady()
}
}
}

// Protocol that the app target implements to provide Clerk SDK operations and SwiftUI views.
public protocol ClerkNativeBridgeProtocol {
// Inline rendering — returns UIViewController to preserve SwiftUI lifecycle
func makeAuthViewController(mode: String, dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserProfileViewController(dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserButtonViewController() -> UIViewController?

// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
func clerkNativeBridgeDidBecomeReady()
}

private let clerkNativeBridgeReadyObservers = NSHashTable<AnyObject>.weakObjects()

public func addClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.add(observer)
}

public func removeClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.remove(observer)
}

public func emitClerkNativeBridgeReady() {
let notifyObservers = {
for observer in clerkNativeBridgeReadyObservers.allObjects {
(observer as? ClerkNativeBridgeReadyObserver)?.clerkNativeBridgeDidBecomeReady()
}
}

if Thread.isMainThread {
notifyObservers()
} else {
DispatchQueue.main.async(execute: notifyObservers)
}
}

// MARK: - Module

@objc(ClerkExpo)
Expand All@@ -73,6 +16,9 @@ class ClerkExpoModule: RCTEventEmitter {
override init() {
super.init()
ClerkExpoModule.sharedInstance = self
ClerkNativeBridge.setClientChangedEmitter { body in
Self.emitClientChanged(body)
}
}

@objc override static func requiresMainQueueSetup() -> Bool {
Expand DownExpand Up@@ -120,14 +66,9 @@ class ClerkExpoModule: RCTEventEmitter {
bearerToken: String?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
reject("E_NOT_INITIALIZED", "Clerk not initialized. Make sure ClerkNativeBridge is registered.", nil)
return
}

Task {
do {
try await bridge.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
resolve(nil)
} catch {
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
Expand All@@ -139,13 +80,8 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

Task {
let token = await bridge.getClientToken()
let token = await ClerkNativeBridge.shared.getClientToken()
resolve(token)
}
}
Expand All@@ -157,18 +93,13 @@ class ClerkExpoModule: RCTEventEmitter {
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

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(
try await ClerkNativeBridge.shared.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
Expand All@@ -181,8 +112,3 @@ class ClerkExpoModule: RCTEventEmitter {
}

}

/// Requests that ClerkProvider reload the JS client from native client state.
public func emitClerkNativeClientChanged(_ body: [String: Any]? = nil) {
ClerkExpoModule.emitClientChanged(body)
}
65 changes: 46 additions & 19 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
// ClerkNativeBridge - Provides app-target Clerk SDK operations and SwiftUI view controllers to ClerkExpo.
// This file is injected into the app target by the config plugin.
// It uses the ClerkKit Swift package, which is only accessible from the app target.
// ClerkNativeBridge - Provides Clerk SDK operations and SwiftUI view controllers to ClerkExpo.

import UIKit
import SwiftUI
import Observation
@_spi(FrameworkIntegration) import ClerkKit
import ClerkKitUI
import ClerkExpo // Import the pod to access ClerkNativeBridgeProtocol

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

extension Notification.Name {
static let clerkNativeSDKDidConfigure = Notification.Name("com.clerk.expo.native-sdk.did-configure")
}

private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events")
private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)?

private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {
// Replaced by the config plugin when this bridge is copied into the app target.
private static let hostSdkVersion = "__CLERK_EXPO_VERSION__"
private static var hostSdkVersion: String? {
Bundle.main.object(forInfoDictionaryKey: "ClerkExpoVersion") as? String
}

func prepare(_ request: inout URLRequest) async throws {
request.addValue("expo", forHTTPHeaderField: "x-clerk-host-sdk")
request.addValue(Self.hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
if let hostSdkVersion = Self.hostSdkVersion, !hostSdkVersion.isEmpty {
request.addValue(hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
}
}
}

// MARK: - Native Bridge Implementation

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

private static let clerkLoadMaxAttempts = 30
Expand DownExpand Up@@ -52,14 +65,10 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return Bundle.main.bundleIdentifier
}

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

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Expand All@@ -69,7 +78,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
return
}

Expand All@@ -89,13 +98,13 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
}

@MainActor
private static func emitClientChangedIfReceivedToken(_ bearerToken: String?) {
guard let token = bearerToken, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
emitClerkNativeClientChanged(Self.clientChangedPayload())
Self.emitClientChanged(Self.clientChangedPayload())
}

@MainActor
Expand DownExpand Up@@ -124,7 +133,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
if newClientState != self.lastObservedClientState {
self.lastObservedClientState = newClientState
let payload = Self.clientChangedPayload()
emitClerkNativeClientChanged(payload)
Self.emitClientChanged(payload)
}

self.observeClient(generation: generation)
Expand DownExpand Up@@ -263,7 +272,25 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

lastObservedClientState = Self.clientStateSnapshot()
emitClerkNativeClientChanged(Self.clientChangedPayload(sourceId: sourceId))
Self.emitClientChanged(Self.clientChangedPayload(sourceId: sourceId))
}

private static func postConfiguredNotification() {
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
}

static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) {
clerkNativeClientEventQueue.sync {
clerkNativeClientChangedEmitter = emitter
}
}

/// 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 {
Expand Down
29 changes: 24 additions & 5 deletions packages/expo/ios/ClerkNativeViewHost.swift
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import UIKit

public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
public class ClerkNativeViewHost: UIView {
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
private var hasInitialized: Bool = false
private var configuredObserver: NSObjectProtocol?

override public init(frame: CGRect) {
super.init(frame: frame)
Expand All@@ -12,22 +13,26 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
fatalError("init(coder:) has not been implemented")
}

deinit {
removeConfiguredObserver()
}

override public func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else {
if hasInitialized {
hostedViewDidDetachFromWindow()
}
removeClerkNativeBridgeReadyObserver(self)
removeConfiguredObserver()
hostingCoordinator.detach()
hasInitialized = false
return
}

guard !hasInitialized else { return }
hasInitialized = true
addClerkNativeBridgeReadyObserver(self)
addConfiguredObserver()
hostedViewDidAttachToWindow()
updateHostedView()
}
Expand All@@ -51,8 +56,22 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {

func hostedViewDidDetachFromWindow() {}

public func clerkNativeBridgeDidBecomeReady() {
setNeedsHostedViewUpdate()
private func addConfiguredObserver() {
guard configuredObserver == nil else { return }

configuredObserver = NotificationCenter.default.addObserver(
forName: .clerkNativeSDKDidConfigure,
object: nil,
queue: .main
) { [weak self] _ in
self?.setNeedsHostedViewUpdate()
}
}

private func removeConfiguredObserver() {
guard let configuredObserver else { return }
NotificationCenter.default.removeObserver(configuredObserver)
self.configuredObserver = nil
}

private func updateHostedView() {
Expand Down
4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkUserButtonNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,7 @@ import UIKit

public class ClerkUserButtonNativeView: ClerkNativeViewHost {
override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeUserButtonViewController()
return ClerkNativeBridge.shared.makeUserButtonViewController()
}
}

Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(expo): link iOS SDK through podspec SPM dependencies by mikepitre · Pull Request #8927 · clerk/javascript · GitHub
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/tidy-expo-ios-import.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"@clerk/expo": minor
---

Fixes iOS development builds across Expo SDK versions by linking the Clerk iOS SDK through React Native's Swift Package Manager podspec support. This raises the minimum supported React Native version to 0.75, where that podspec SPM support is available; `@clerk/expo` already supports Expo SDK 53 and newer, and Expo SDK 53 ships with React Native 0.79.
384 changes: 5 additions & 379 deletions packages/expo/app.plugin.js

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkAuthNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,9 +46,7 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
}

override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeAuthViewController(
return ClerkNativeBridge.shared.makeAuthViewController(
mode: currentMode,
dismissible: currentDismissible,
onEvent: { [weak self] event, _ in
Expand Down
20 changes: 16 additions & 4 deletions packages/expo/ios/ClerkExpo.podspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,9 @@ else
}
end

clerk_ios_repo = 'https://github.com/clerk/clerk-ios.git'
clerk_ios_version = '1.2.4'

Pod::Spec.new do |s|
s.name = 'ClerkExpo'
s.version = package['version']
Expand All@@ -35,10 +38,19 @@ Pod::Spec.new do |s|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}

# Only include the module files in the pod (both Swift and ObjC bridges).
# ClerkNativeBridge.swift is injected into the app target by the config plugin
# because it uses `import ClerkKit` which is only available via SPM in the app target.
s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m",
if defined?(spm_dependency)
spm_dependency(
s,
url: clerk_ios_repo,
requirement: { :kind => 'exactVersion', :version => clerk_ios_version },
products: ['ClerkKit', 'ClerkKitUI']
)
else
raise 'ClerkExpo requires React Native 0.75 or newer for iOS Swift Package Manager dependencies.'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep RN 0.73/0.74 pod installs working

For apps still on React Native 0.73/0.74 (which satisfied @clerk/expo's previous peer range and can receive this patch via a caret range), spm_dependency is not defined by RN's CocoaPods scripts, so this raise makes every iOS pod install fail even though those apps built with the old Xcode-project injection path. Because this is being released as a patch, please keep a fallback for those RN versions or avoid shipping this path to them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our supported expo range is >=53, so supporting 0.73 and 0.74 is already a mismatch. Expo 53 contains React Native 0.79

end

s.source_files = "ClerkNativeBridge.swift",
"ClerkExpoModule.swift", "ClerkExpoModule.m",
"ClerkNativeViewHost.swift",
"ClerkAuthNativeView.swift",
"ClerkAuthViewManager.m",
Expand Down
88 changes: 7 additions & 81 deletions packages/expo/ios/ClerkExpoModule.swift
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,10 @@
// ClerkExpoModule - Native module for Clerk integration
// This module provides the configure function, client sync, and native view bridges.
// SwiftUI Clerk views are created by the app target through ClerkNativeBridge because
// the Clerk SDK (SPM) isn't accessible from the CocoaPods-backed React Native pod.
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.

import UIKit
import React

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

// Global registry for the app-target native bridge (set by the app target at startup)
public var clerkNativeBridge: ClerkNativeBridgeProtocol? {
didSet {
if clerkNativeBridge != nil {
emitClerkNativeBridgeReady()
}
}
}

// Protocol that the app target implements to provide Clerk SDK operations and SwiftUI views.
public protocol ClerkNativeBridgeProtocol {
// Inline rendering — returns UIViewController to preserve SwiftUI lifecycle
func makeAuthViewController(mode: String, dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserProfileViewController(dismissible: Bool, onEvent: @escaping (ClerkNativeViewEvent, [String: Any]) -> Void) -> UIViewController?
func makeUserButtonViewController() -> UIViewController?

// SDK operations
func configure(publishableKey: String, bearerToken: String?) async throws
func getClientToken() async -> String?
func syncFromJsClientToken(_ clientToken: String?, sourceId: String?, shouldRefreshClient: Bool) async throws
}

public protocol ClerkNativeBridgeReadyObserver: AnyObject {
func clerkNativeBridgeDidBecomeReady()
}

private let clerkNativeBridgeReadyObservers = NSHashTable<AnyObject>.weakObjects()

public func addClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.add(observer)
}

public func removeClerkNativeBridgeReadyObserver(_ observer: ClerkNativeBridgeReadyObserver) {
clerkNativeBridgeReadyObservers.remove(observer)
}

public func emitClerkNativeBridgeReady() {
let notifyObservers = {
for observer in clerkNativeBridgeReadyObservers.allObjects {
(observer as? ClerkNativeBridgeReadyObserver)?.clerkNativeBridgeDidBecomeReady()
}
}

if Thread.isMainThread {
notifyObservers()
} else {
DispatchQueue.main.async(execute: notifyObservers)
}
}

// MARK: - Module

@objc(ClerkExpo)
Expand All@@ -73,6 +16,9 @@ class ClerkExpoModule: RCTEventEmitter {
override init() {
super.init()
ClerkExpoModule.sharedInstance = self
ClerkNativeBridge.setClientChangedEmitter { body in
Self.emitClientChanged(body)
}
}

@objc override static func requiresMainQueueSetup() -> Bool {
Expand DownExpand Up@@ -120,14 +66,9 @@ class ClerkExpoModule: RCTEventEmitter {
bearerToken: String?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
reject("E_NOT_INITIALIZED", "Clerk not initialized. Make sure ClerkNativeBridge is registered.", nil)
return
}

Task {
do {
try await bridge.configure(publishableKey: publishableKey, bearerToken: bearerToken)
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
resolve(nil)
} catch {
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
Expand All@@ -139,13 +80,8 @@ class ClerkExpoModule: RCTEventEmitter {

@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

Task {
let token = await bridge.getClientToken()
let token = await ClerkNativeBridge.shared.getClientToken()
resolve(token)
}
}
Expand All@@ -157,18 +93,13 @@ class ClerkExpoModule: RCTEventEmitter {
shouldRefreshClient: Any?,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) {
guard let bridge = clerkNativeBridge else {
resolve(nil)
return
}

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(
try await ClerkNativeBridge.shared.syncFromJsClientToken(
normalizedClientToken,
sourceId: normalizedSourceId,
shouldRefreshClient: normalizedShouldRefreshClient
Expand All@@ -181,8 +112,3 @@ class ClerkExpoModule: RCTEventEmitter {
}

}

/// Requests that ClerkProvider reload the JS client from native client state.
public func emitClerkNativeClientChanged(_ body: [String: Any]? = nil) {
ClerkExpoModule.emitClientChanged(body)
}
65 changes: 46 additions & 19 deletions packages/expo/ios/ClerkNativeBridge.swift
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
// ClerkNativeBridge - Provides app-target Clerk SDK operations and SwiftUI view controllers to ClerkExpo.
// This file is injected into the app target by the config plugin.
// It uses the ClerkKit Swift package, which is only accessible from the app target.
// ClerkNativeBridge - Provides Clerk SDK operations and SwiftUI view controllers to ClerkExpo.

import UIKit
import SwiftUI
import Observation
@_spi(FrameworkIntegration) import ClerkKit
import ClerkKitUI
import ClerkExpo // Import the pod to access ClerkNativeBridgeProtocol

/// Events emitted by the native view wrappers to their React Native host views.
public enum ClerkNativeViewEvent: String {
/// Emitted by the Expo host view when app-owned dismissible content leaves the window.
case dismissed
}

extension Notification.Name {
static let clerkNativeSDKDidConfigure = Notification.Name("com.clerk.expo.native-sdk.did-configure")
}

private let clerkNativeClientEventQueue = DispatchQueue(label: "com.clerk.expo.native-client-events")
private var clerkNativeClientChangedEmitter: (([String: Any]?) -> Void)?

private struct ClerkExpoHeaderMiddleware: ClerkRequestMiddleware {
// Replaced by the config plugin when this bridge is copied into the app target.
private static let hostSdkVersion = "__CLERK_EXPO_VERSION__"
private static var hostSdkVersion: String? {
Bundle.main.object(forInfoDictionaryKey: "ClerkExpoVersion") as? String
}

func prepare(_ request: inout URLRequest) async throws {
request.addValue("expo", forHTTPHeaderField: "x-clerk-host-sdk")
request.addValue(Self.hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
if let hostSdkVersion = Self.hostSdkVersion, !hostSdkVersion.isEmpty {
request.addValue(hostSdkVersion, forHTTPHeaderField: "x-clerk-host-sdk-version")
}
}
}

// MARK: - Native Bridge Implementation

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

private static let clerkLoadMaxAttempts = 30
Expand DownExpand Up@@ -52,14 +65,10 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
return Bundle.main.bundleIdentifier
}

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

@MainActor
func configure(publishableKey: String, bearerToken: String? = nil) async throws {
loadThemes()

if Self.shouldReconfigure(for: publishableKey) {
try await Clerk.reconfigure(publishableKey: publishableKey, options: Self.makeClerkOptions())
Self.clerkConfigured = true
Expand All@@ -69,7 +78,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
return
}

Expand All@@ -89,13 +98,13 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
let shouldWaitForClient = try await Self.syncTokenState(bearerToken: bearerToken)
await Self.waitForLoadedClientIfNeeded(shouldWaitForClient)
Self.emitClientChangedIfReceivedToken(bearerToken)
emitClerkNativeBridgeReady()
Self.postConfiguredNotification()
}

@MainActor
private static func emitClientChangedIfReceivedToken(_ bearerToken: String?) {
guard let token = bearerToken, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
emitClerkNativeClientChanged(Self.clientChangedPayload())
Self.emitClientChanged(Self.clientChangedPayload())
}

@MainActor
Expand DownExpand Up@@ -124,7 +133,7 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
if newClientState != self.lastObservedClientState {
self.lastObservedClientState = newClientState
let payload = Self.clientChangedPayload()
emitClerkNativeClientChanged(payload)
Self.emitClientChanged(payload)
}

self.observeClient(generation: generation)
Expand DownExpand Up@@ -263,7 +272,25 @@ final class ClerkNativeBridge: ClerkNativeBridgeProtocol {
}

lastObservedClientState = Self.clientStateSnapshot()
emitClerkNativeClientChanged(Self.clientChangedPayload(sourceId: sourceId))
Self.emitClientChanged(Self.clientChangedPayload(sourceId: sourceId))
}

private static func postConfiguredNotification() {
NotificationCenter.default.post(name: .clerkNativeSDKDidConfigure, object: nil)
}

static func setClientChangedEmitter(_ emitter: (([String: Any]?) -> Void)?) {
clerkNativeClientEventQueue.sync {
clerkNativeClientChangedEmitter = emitter
}
}

/// 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 {
Expand Down
29 changes: 24 additions & 5 deletions packages/expo/ios/ClerkNativeViewHost.swift
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
import UIKit

public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
public class ClerkNativeViewHost: UIView {
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
private var hasInitialized: Bool = false
private var configuredObserver: NSObjectProtocol?

override public init(frame: CGRect) {
super.init(frame: frame)
Expand All@@ -12,22 +13,26 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {
fatalError("init(coder:) has not been implemented")
}

deinit {
removeConfiguredObserver()
}

override public func didMoveToWindow() {
super.didMoveToWindow()

guard window != nil else {
if hasInitialized {
hostedViewDidDetachFromWindow()
}
removeClerkNativeBridgeReadyObserver(self)
removeConfiguredObserver()
hostingCoordinator.detach()
hasInitialized = false
return
}

guard !hasInitialized else { return }
hasInitialized = true
addClerkNativeBridgeReadyObserver(self)
addConfiguredObserver()
hostedViewDidAttachToWindow()
updateHostedView()
}
Expand All@@ -51,8 +56,22 @@ public class ClerkNativeViewHost: UIView, ClerkNativeBridgeReadyObserver {

func hostedViewDidDetachFromWindow() {}

public func clerkNativeBridgeDidBecomeReady() {
setNeedsHostedViewUpdate()
private func addConfiguredObserver() {
guard configuredObserver == nil else { return }

configuredObserver = NotificationCenter.default.addObserver(
forName: .clerkNativeSDKDidConfigure,
object: nil,
queue: .main
) { [weak self] _ in
self?.setNeedsHostedViewUpdate()
}
}

private func removeConfiguredObserver() {
guard let configuredObserver else { return }
NotificationCenter.default.removeObserver(configuredObserver)
self.configuredObserver = nil
}

private func updateHostedView() {
Expand Down
4 changes: 1 addition & 3 deletions packages/expo/ios/ClerkUserButtonNativeView.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,7 @@ import UIKit

public class ClerkUserButtonNativeView: ClerkNativeViewHost {
override func makeHostedController() -> UIViewController? {
guard let bridge = clerkNativeBridge else { return nil }

return bridge.makeUserButtonViewController()
return ClerkNativeBridge.shared.makeUserButtonViewController()
}
}

Expand Down
Loading
Loading