Skip to content

Commit e1ad295

Browse files
committed
fix(expo): address final CodeRabbit review issues (round 3)
package.json: - Add app.plugin.d.ts to files array so TypeScript declarations are published to npm (#1 - TypeScript declarations won't be published) ios/ClerkViewFactory.swift (fallback): - Fix return type mismatch: createAuthView and createUserProfileView now return UIViewController? (matching protocol) instead of UIView? (#2 - iOS return type mismatch) - Return hostingController instead of hostingController.view to preserve SwiftUI lifecycle when used as child view controller - Add completionCalled guard with completeOnce() to both Auth and Profile wrapper view controllers (align with template) - Remove PII logging (print/NSLog statements with user data) Note: Issue #3 (Android mode parameter) is a false positive — the clerk-android AuthView composable signature is `fun AuthView(modifier: Modifier, clerkTheme: ClerkTheme?)` and does not accept a mode parameter. The SDK auto-determines the auth flow.
1 parent 2a5d8f1 commit e1ad295

2 files changed

Lines changed: 33 additions & 40 deletions

File tree

packages/expo/ios/ClerkViewFactory.swift

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,23 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
1717
// Register this factory with the ClerkExpo module
1818
public static func register() {
1919
clerkViewFactory = shared
20-
print("✅ [ClerkViewFactory] Registered with ClerkExpo module")
2120
}
2221

2322
@MainActor
2423
public func configure(publishableKey: String) async throws {
25-
print("🔧 [ClerkViewFactory] Configuring Clerk with key: \(publishableKey.prefix(20))...")
2624
Clerk.shared.configure(publishableKey: publishableKey)
27-
print("✅ [ClerkViewFactory] Clerk configured, now loading...")
2825

2926
// CRITICAL: Must call load() after configure() to restore session from keychain
30-
do {
31-
try await Clerk.shared.load()
32-
print("✅ [ClerkViewFactory] Clerk load() completed")
33-
} catch {
34-
print("❌ [ClerkViewFactory] Clerk load() failed: \(error)")
35-
}
27+
try await Clerk.shared.load()
3628

37-
// IMPORTANT: load() is async but session may be populated AFTER it returns
38-
// The SDK uses Combine/ObservableObject pattern - session is published asynchronously
39-
// We need to wait for the session to actually be populated
40-
print("⏳ [ClerkViewFactory] Waiting for session to be populated...")
41-
for i in 0..<30 { // Wait up to 3 seconds
29+
// load() is async but session may be populated AFTER it returns.
30+
// The SDK uses Combine/ObservableObject pattern — session is published asynchronously.
31+
for _ in 0..<30 { // Wait up to 3 seconds
4232
if Clerk.shared.session != nil {
43-
print("✅ [ClerkViewFactory] Session found after \(i * 100)ms: \(Clerk.shared.session?.id ?? "unknown")")
4433
return
4534
}
4635
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
4736
}
48-
print("⚠️ [ClerkViewFactory] No session found after 3s, session: \(Clerk.shared.session?.id ?? "none")")
4937
}
5038

5139
public func createAuthViewController(
@@ -88,7 +76,7 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
8876
mode: String,
8977
dismissable: Bool,
9078
onEvent: @escaping (String, [String: Any]) -> Void
91-
) -> UIView? {
79+
) -> UIViewController? {
9280
let authMode: AuthView.Mode
9381
switch mode {
9482
case "signIn":
@@ -107,41 +95,36 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
10795
)
10896
)
10997
hostingController.view.backgroundColor = .clear
110-
return hostingController.view
98+
return hostingController
11199
}
112100

113101
public func createUserProfileView(
114102
dismissable: Bool,
115103
onEvent: @escaping (String, [String: Any]) -> Void
116-
) -> UIView? {
104+
) -> UIViewController? {
117105
let hostingController = UIHostingController(
118106
rootView: ClerkInlineProfileWrapperView(
119107
dismissable: dismissable,
120108
onEvent: onEvent
121109
)
122110
)
123111
hostingController.view.backgroundColor = .clear
124-
return hostingController.view
112+
return hostingController
125113
}
126114

127115
@MainActor
128116
public func getSession() async -> [String: Any]? {
129117
guard let session = Clerk.shared.session else {
130-
print("📭 [ClerkViewFactory] No active session")
131118
return nil
132119
}
133-
print("✅ [ClerkViewFactory] Found active session: \(session.id)")
134120

135121
var result: [String: Any] = [
136122
"sessionId": session.id,
137123
"status": String(describing: session.status)
138124
]
139125

140126
// Include user details if available
141-
// Try to get user from session first, then fallback to Clerk.shared.user
142127
let user = session.user ?? Clerk.shared.user
143-
NSLog("🔍 [ClerkViewFactory] Clerk.shared.user: \(Clerk.shared.user?.id ?? "nil")")
144-
NSLog("🔍 [ClerkViewFactory] session.user: \(session.user?.id ?? "nil")")
145128

146129
if let user = user {
147130
var userDict: [String: Any] = [
@@ -160,18 +143,13 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
160143
userDict["primaryEmailAddress"] = firstEmail.emailAddress
161144
}
162145
result["user"] = userDict
163-
NSLog("✅ [ClerkViewFactory] User found: \(user.firstName ?? "N/A") \(user.lastName ?? "")")
164-
} else {
165-
NSLog("⚠️ [ClerkViewFactory] No user available - all sources returned nil")
166146
}
167147

168148
return result
169149
}
170150

171151
public func signOut() async throws {
172-
print("🔓 [ClerkViewFactory] Signing out...")
173152
try await Clerk.shared.signOut()
174-
print("✅ [ClerkViewFactory] Signed out successfully")
175153
}
176154
}
177155

@@ -180,6 +158,7 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
180158
class ClerkAuthWrapperViewController: UIHostingController<ClerkAuthWrapperView> {
181159
private let completion: (Result<[String: Any], Error>) -> Void
182160
private var authEventTask: Task<Void, Never>?
161+
private var completionCalled = false
183162

184163
init(mode: AuthView.Mode, dismissable: Bool, completion: @escaping (Result<[String: Any], Error>) -> Void) {
185164
self.completion = completion
@@ -197,25 +176,31 @@ class ClerkAuthWrapperViewController: UIHostingController<ClerkAuthWrapperView>
197176
authEventTask?.cancel()
198177
}
199178

179+
private func completeOnce(_ result: Result<[String: Any], Error>) {
180+
guard !completionCalled else { return }
181+
completionCalled = true
182+
completion(result)
183+
}
184+
200185
private func subscribeToAuthEvents() {
201186
authEventTask = Task { @MainActor [weak self] in
202187
for await event in Clerk.shared.authEventEmitter.events {
203-
guard let self = self else { return }
188+
guard let self = self, !self.completionCalled else { return }
204189
switch event {
205190
case .signInCompleted(let signIn):
206191
if let sessionId = signIn.createdSessionId {
207-
self.completion(.success(["sessionId": sessionId, "type": "signIn"]))
192+
self.completeOnce(.success(["sessionId": sessionId, "type": "signIn"]))
208193
self.dismiss(animated: true)
209194
} else {
210-
self.completion(.failure(NSError(domain: "ClerkExpo", code: 4, userInfo: [NSLocalizedDescriptionKey: "Sign-in completed but no session was created"])))
195+
self.completeOnce(.failure(NSError(domain: "ClerkExpo", code: 4, userInfo: [NSLocalizedDescriptionKey: "Sign-in completed but no session was created"])))
211196
self.dismiss(animated: true)
212197
}
213198
case .signUpCompleted(let signUp):
214199
if let sessionId = signUp.createdSessionId {
215-
self.completion(.success(["sessionId": sessionId, "type": "signUp"]))
200+
self.completeOnce(.success(["sessionId": sessionId, "type": "signUp"]))
216201
self.dismiss(animated: true)
217202
} else {
218-
self.completion(.failure(NSError(domain: "ClerkExpo", code: 4, userInfo: [NSLocalizedDescriptionKey: "Sign-up completed but no session was created"])))
203+
self.completeOnce(.failure(NSError(domain: "ClerkExpo", code: 4, userInfo: [NSLocalizedDescriptionKey: "Sign-up completed but no session was created"])))
219204
self.dismiss(animated: true)
220205
}
221206
default:
@@ -224,7 +209,7 @@ class ClerkAuthWrapperViewController: UIHostingController<ClerkAuthWrapperView>
224209
}
225210
// Stream ended without a completion event
226211
guard let self = self else { return }
227-
self.completion(.failure(NSError(domain: "ClerkExpo", code: 5, userInfo: [NSLocalizedDescriptionKey: "Auth event stream ended unexpectedly"])))
212+
self.completeOnce(.failure(NSError(domain: "ClerkExpo", code: 5, userInfo: [NSLocalizedDescriptionKey: "Auth event stream ended unexpectedly"])))
228213
}
229214
}
230215
}
@@ -243,6 +228,7 @@ struct ClerkAuthWrapperView: View {
243228
class ClerkProfileWrapperViewController: UIHostingController<ClerkProfileWrapperView> {
244229
private let completion: (Result<[String: Any], Error>) -> Void
245230
private var authEventTask: Task<Void, Never>?
231+
private var completionCalled = false
246232

247233
init(dismissable: Bool, completion: @escaping (Result<[String: Any], Error>) -> Void) {
248234
self.completion = completion
@@ -260,21 +246,27 @@ class ClerkProfileWrapperViewController: UIHostingController<ClerkProfileWrapper
260246
authEventTask?.cancel()
261247
}
262248

249+
private func completeOnce(_ result: Result<[String: Any], Error>) {
250+
guard !completionCalled else { return }
251+
completionCalled = true
252+
completion(result)
253+
}
254+
263255
private func subscribeToAuthEvents() {
264256
authEventTask = Task { @MainActor [weak self] in
265257
for await event in Clerk.shared.authEventEmitter.events {
266-
guard let self = self else { return }
258+
guard let self = self, !self.completionCalled else { return }
267259
switch event {
268260
case .signedOut(let session):
269-
self.completion(.success(["sessionId": session.id]))
261+
self.completeOnce(.success(["sessionId": session.id]))
270262
self.dismiss(animated: true)
271263
default:
272264
break
273265
}
274266
}
275267
// Stream ended without a sign-out event
276268
guard let self = self else { return }
277-
self.completion(.failure(NSError(domain: "ClerkExpo", code: 5, userInfo: [NSLocalizedDescriptionKey: "Profile event stream ended unexpectedly"])))
269+
self.completeOnce(.failure(NSError(domain: "ClerkExpo", code: 5, userInfo: [NSLocalizedDescriptionKey: "Profile event stream ended unexpectedly"])))
278270
}
279271
}
280272
}

packages/expo/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@
9393
"google",
9494
"apple",
9595
"expo-module.config.json",
96-
"app.plugin.js"
96+
"app.plugin.js",
97+
"app.plugin.d.ts"
9798
],
9899
"scripts": {
99100
"build": "tsup",

0 commit comments

Comments
 (0)