Skip to content

Commit 29ac155

Browse files
refactor(expo): align iOS native components with Expo Modules (#8976)
Co-authored-by: Robert Soriano <sorianorobertc@gmail.com>
1 parent d654e82 commit 29ac155

32 files changed

Lines changed: 230 additions & 266 deletions

.changeset/quiet-buttons-align.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/expo": minor
3+
---
4+
5+
Align the iOS native Clerk module and native views with Android by registering them through Expo Modules.

packages/expo/app.plugin.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
* 1. iOS is configured with the required deployment target and metadata
77
* 2. Android is configured with packaging exclusions for dependencies
88
*
9-
* Native modules are registered via Expo Modules autolinking on Android and
10-
* React Native autolinking on iOS (RCTViewManager).
9+
* Native modules and views are registered via Expo Modules autolinking.
1110
*/
1211
const {
1312
withXcodeProject,
@@ -187,8 +186,7 @@ const withClerkGoogleSignIn = config => {
187186
* 2. Android gets packaging exclusions for dependency conflicts
188187
* 3. Google Sign-In URL scheme is configured (if env var is set)
189188
*
190-
* Native modules are registered via Expo Modules autolinking on Android and
191-
* React Native autolinking on iOS (RCTViewManager).
189+
* Native modules and views are registered via Expo Modules autolinking.
192190
*/
193191
/**
194192
* Write ClerkKeychainService to Info.plist when keychainService is provided.

packages/expo/expo-module.config.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
{
22
"platforms": ["apple", "android"],
33
"apple": {
4-
"modules": ["ClerkGoogleSignInModule"]
4+
"modules": [
5+
"ClerkExpoModule",
6+
"ClerkAuthViewModule",
7+
"ClerkUserProfileViewModule",
8+
"ClerkUserButtonViewModule",
9+
"ClerkGoogleSignInModule"
10+
]
511
},
612
"android": {
713
"modules": [
Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,29 @@
1-
import React
1+
import ExpoModulesCore
22
import UIKit
33

44
public class ClerkAuthNativeView: ClerkNativeViewHost {
55
private var currentMode: String = "signInOrUp"
66
private var currentDismissible: Bool = true
77
private var didSendDismiss = false
88

9-
@objc var onAuthEvent: RCTBubblingEventBlock?
9+
let onAuthEvent = EventDispatcher()
1010

11-
@objc var mode: NSString? {
12-
didSet {
13-
let newMode = (mode as String?) ?? "signInOrUp"
14-
guard newMode != currentMode else { return }
15-
currentMode = newMode
16-
setNeedsHostedViewUpdate()
17-
}
11+
func setMode(_ mode: String?) {
12+
let newMode = mode ?? "signInOrUp"
13+
guard newMode != currentMode else { return }
14+
currentMode = newMode
15+
setNeedsHostedViewUpdate()
1816
}
1917

20-
@objc var isDismissible: NSNumber? {
21-
didSet {
22-
let newDismissible = isDismissible?.boolValue ?? true
23-
guard newDismissible != currentDismissible else { return }
24-
currentDismissible = newDismissible
25-
setNeedsHostedViewUpdate()
26-
}
18+
func setDismissible(_ isDismissible: Bool?) {
19+
let newDismissible = isDismissible ?? true
20+
guard newDismissible != currentDismissible else { return }
21+
currentDismissible = newDismissible
22+
setNeedsHostedViewUpdate()
2723
}
2824

2925
private func sendAuthEvent(type: ClerkNativeViewEvent) {
30-
onAuthEvent?(["type": type.rawValue])
26+
onAuthEvent(["type": type.rawValue])
3127
}
3228

3329
private func sendDismissIfNeeded() {
@@ -58,14 +54,20 @@ public class ClerkAuthNativeView: ClerkNativeViewHost {
5854
}
5955
}
6056

61-
@objc(ClerkAuthViewManager)
62-
class ClerkAuthViewManager: RCTViewManager {
57+
public class ClerkAuthViewModule: Module {
58+
public func definition() -> ModuleDefinition {
59+
Name("ClerkAuthView")
6360

64-
override static func requiresMainQueueSetup() -> Bool {
65-
return true
66-
}
61+
View(ClerkAuthNativeView.self) {
62+
Events("onAuthEvent")
6763

68-
override func view() -> UIView! {
69-
return ClerkAuthNativeView()
64+
Prop("mode") { (view: ClerkAuthNativeView, mode: String?) in
65+
view.setMode(mode)
66+
}
67+
68+
Prop("isDismissible") { (view: ClerkAuthNativeView, isDismissible: Bool?) in
69+
view.setDismissible(isDismissible)
70+
}
71+
}
7072
}
7173
}

packages/expo/ios/ClerkAuthViewManager.m

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/expo/ios/ClerkExpo.podspec

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Pod::Spec.new do |s|
3333
s.source = { git: 'https://github.com/clerk/javascript' }
3434
s.static_framework = true
3535

36+
s.dependency 'ExpoModulesCore'
37+
3638
s.pod_target_xcconfig = {
3739
'DEFINES_MODULE' => 'YES',
3840
'SWIFT_COMPILATION_MODE' => 'wholemodule'
@@ -50,14 +52,11 @@ Pod::Spec.new do |s|
5052
end
5153

5254
s.source_files = "ClerkNativeBridge.swift",
53-
"ClerkExpoModule.swift", "ClerkExpoModule.m",
55+
"ClerkExpoModule.swift",
5456
"ClerkNativeViewHost.swift",
5557
"ClerkAuthNativeView.swift",
56-
"ClerkAuthViewManager.m",
5758
"ClerkUserProfileNativeView.swift",
58-
"ClerkUserProfileViewManager.m",
59-
"ClerkUserButtonNativeView.swift",
60-
"ClerkUserButtonViewManager.m"
59+
"ClerkUserButtonNativeView.swift"
6160

6261
install_modules_dependencies(s)
6362
end

packages/expo/ios/ClerkExpoModule.m

Lines changed: 0 additions & 21 deletions
This file was deleted.

packages/expo/ios/ClerkExpoModule.swift

Lines changed: 63 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,113 +2,114 @@
22
// This module provides the configure function, client sync, and native view bridges.
33
// SwiftUI Clerk views are created by ClerkNativeBridge through the Clerk iOS SPM dependency.
44

5-
import UIKit
6-
import React
5+
import ExpoModulesCore
6+
import Foundation
77

88
// MARK: - Module
99

10-
@objc(ClerkExpo)
11-
class ClerkExpoModule: RCTEventEmitter {
12-
13-
private static var _hasListeners = false
14-
private static weak var sharedInstance: ClerkExpoModule?
15-
16-
override init() {
17-
super.init()
18-
ClerkExpoModule.sharedInstance = self
19-
ClerkNativeBridge.setClientChangedEmitter { body in
20-
Self.emitClientChanged(body)
21-
}
22-
}
23-
24-
@objc override static func requiresMainQueueSetup() -> Bool {
25-
return false
26-
}
27-
10+
public class ClerkExpoModule: Module {
2811
private static let nativeClientChangedEvent = "clerkNativeClientChanged"
2912

30-
override func supportedEvents() -> [String]! {
31-
return [Self.nativeClientChangedEvent]
32-
}
13+
private static weak var sharedInstance: ClerkExpoModule?
3314

34-
override func startObserving() {
35-
ClerkExpoModule._hasListeners = true
36-
}
15+
public func definition() -> ModuleDefinition {
16+
Name("ClerkExpo")
3717

38-
override func stopObserving() {
39-
ClerkExpoModule._hasListeners = false
40-
}
18+
Events(Self.nativeClientChangedEvent)
4119

42-
/// Emits a native client change event to JS from anywhere in the native layer.
43-
/// Used by native views to ask ClerkProvider to reload JS client state.
44-
static func emitClientChanged(_ body: [String: Any]? = nil) {
45-
let eventBody = body ?? [:]
20+
OnCreate {
21+
Self.sharedInstance = self
22+
ClerkNativeBridge.setClientChangedEmitter { body in
23+
Self.emitClientChanged(body)
24+
}
25+
}
4626

47-
guard let instance = sharedInstance else {
48-
return
27+
OnDestroy {
28+
if Self.sharedInstance === self {
29+
Self.sharedInstance = nil
30+
ClerkNativeBridge.setClientChangedEmitter(nil)
31+
}
4932
}
5033

51-
if let bridge = instance.bridge {
52-
bridge.enqueueJSCall("RCTDeviceEventEmitter", method: "emit", args: [nativeClientChangedEvent, eventBody], completion: nil)
53-
return
34+
AsyncFunction("configure") { (publishableKey: String, bearerToken: String?, promise: Promise) in
35+
self.configure(publishableKey, bearerToken: bearerToken, promise: promise)
5436
}
5537

56-
guard _hasListeners else {
57-
return
38+
AsyncFunction("getClientToken") { (promise: Promise) in
39+
self.getClientToken(promise: promise)
5840
}
5941

60-
instance.sendEvent(withName: nativeClientChangedEvent, body: eventBody)
42+
AsyncFunction("syncClientStateFromJs") {
43+
(deviceToken: String?,
44+
sourceId: String?,
45+
didChangeClient: Bool,
46+
didChangeDeviceToken: Bool,
47+
promise: Promise) in
48+
self.syncClientStateFromJs(
49+
deviceToken,
50+
sourceId: sourceId,
51+
didChangeClient: didChangeClient,
52+
didChangeDeviceToken: didChangeDeviceToken,
53+
promise: promise
54+
)
55+
}
6156
}
6257

6358
// MARK: - configure
6459

65-
@objc func configure(_ publishableKey: String,
66-
bearerToken: String?,
67-
resolve: @escaping RCTPromiseResolveBlock,
68-
reject: @escaping RCTPromiseRejectBlock) {
60+
private func configure(_ publishableKey: String, bearerToken: String?, promise: Promise) {
6961
Task {
7062
do {
7163
try await ClerkNativeBridge.shared.configure(publishableKey: publishableKey, bearerToken: bearerToken)
72-
resolve(nil)
64+
promise.resolve()
7365
} catch {
74-
reject("E_CONFIGURE_FAILED", error.localizedDescription, error)
66+
promise.reject("E_CONFIGURE_FAILED", error.localizedDescription)
7567
}
7668
}
7769
}
7870

7971
// MARK: - getClientToken
8072

81-
@objc func getClientToken(_ resolve: @escaping RCTPromiseResolveBlock,
82-
reject: @escaping RCTPromiseRejectBlock) {
73+
private func getClientToken(promise: Promise) {
8374
Task {
8475
let token = await ClerkNativeBridge.shared.getClientToken()
85-
resolve(token)
76+
promise.resolve(token)
8677
}
8778
}
8879

8980
// MARK: - syncClientStateFromJs
9081

91-
@objc func syncClientStateFromJs(_ deviceToken: Any?,
92-
sourceId: Any?,
93-
didChangeClient: Bool,
94-
didChangeDeviceToken: Bool,
95-
resolve: @escaping RCTPromiseResolveBlock,
96-
reject: @escaping RCTPromiseRejectBlock) {
97-
let normalizedDeviceToken = deviceToken as? String
98-
let normalizedSourceId = sourceId as? String
82+
private func syncClientStateFromJs(_ deviceToken: String?,
83+
sourceId: String?,
84+
didChangeClient: Bool,
85+
didChangeDeviceToken: Bool,
86+
promise: Promise) {
9987
Task {
10088
do {
10189
try await ClerkNativeBridge.shared.syncClientStateFromJs(
102-
deviceToken: normalizedDeviceToken,
103-
sourceId: normalizedSourceId,
90+
deviceToken: deviceToken,
91+
sourceId: sourceId,
10492
didChangeClient: didChangeClient,
10593
didChangeDeviceToken: didChangeDeviceToken
10694
)
107-
resolve(nil)
95+
promise.resolve()
10896
} catch {
109-
reject("E_SYNC_FROM_JS_FAILED", error.localizedDescription, error)
97+
promise.reject("E_SYNC_FROM_JS_FAILED", error.localizedDescription)
11098
}
11199
}
112100
}
113101

102+
/// Emits a native client change event to JS from anywhere in the native layer.
103+
/// Used by native views to ask ClerkProvider to reload JS client state.
104+
static func emitClientChanged(_ body: [String: Any]? = nil) {
105+
let eventBody = body ?? [:]
106+
107+
guard let instance = sharedInstance else {
108+
return
109+
}
110+
111+
DispatchQueue.main.async { [weak instance] in
112+
instance?.sendEvent(Self.nativeClientChangedEvent, eventBody)
113+
}
114+
}
114115
}

packages/expo/ios/ClerkNativeViewHost.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
import ExpoModulesCore
12
import UIKit
23

3-
public class ClerkNativeViewHost: UIView {
4+
public class ClerkNativeViewHost: ExpoView {
45
private lazy var hostingCoordinator = ClerkNativeHostingCoordinator(containerView: self)
56
private var hasInitialized: Bool = false
67
private var configuredObserver: NSObjectProtocol?
78

8-
override public init(frame: CGRect) {
9-
super.init(frame: frame)
9+
public required init(appContext: AppContext? = nil) {
10+
super.init(appContext: appContext)
1011
}
1112

13+
@available(*, unavailable)
1214
public required init?(coder: NSCoder) {
1315
fatalError("init(coder:) has not been implemented")
1416
}
Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React
1+
import ExpoModulesCore
22
import UIKit
33

44
public class ClerkUserButtonNativeView: ClerkNativeViewHost {
@@ -7,14 +7,10 @@ public class ClerkUserButtonNativeView: ClerkNativeViewHost {
77
}
88
}
99

10-
@objc(ClerkUserButtonViewManager)
11-
class ClerkUserButtonViewManager: RCTViewManager {
10+
public class ClerkUserButtonViewModule: Module {
11+
public func definition() -> ModuleDefinition {
12+
Name("ClerkUserButtonView")
1213

13-
override static func requiresMainQueueSetup() -> Bool {
14-
return true
15-
}
16-
17-
override func view() -> UIView! {
18-
return ClerkUserButtonNativeView()
14+
View(ClerkUserButtonNativeView.self) {}
1915
}
2016
}

0 commit comments

Comments
 (0)