@@ -16,6 +16,16 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
1616
1717 private static let clerkLoadMaxAttempts = 30
1818 private static let clerkLoadIntervalNs : UInt64 = 100_000_000
19+ private static var clerkConfigured = false
20+
21+ /// Resolves the keychain service name, checking ClerkKeychainService in Info.plist first
22+ /// (for extension apps sharing a keychain group), then falling back to the bundle identifier.
23+ private static var keychainService : String ? {
24+ if let custom = Bundle . main. object ( forInfoDictionaryKey: " ClerkKeychainService " ) as? String , !custom. isEmpty {
25+ return custom
26+ }
27+ return Bundle . main. bundleIdentifier
28+ }
1929
2030 private init ( ) { }
2131
@@ -30,12 +40,38 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
3040 // This handles the case where the user signed in via JS SDK but the native SDK
3141 // has no device token (e.g., after app reinstall or first launch).
3242 if let token = bearerToken, !token. isEmpty {
33- Self . writeNativeDeviceTokenIfNeeded ( token)
43+ let existingToken = Self . readNativeDeviceToken ( )
44+ Self . writeNativeDeviceToken ( token)
45+
46+ // If the device token changed (or didn't exist), clear stale cached client/environment.
47+ // A previous launch may have cached an anonymous client (no device token), and the
48+ // SDK would send both the new device token AND the stale client ID in API requests,
49+ // causing a 400 error. Clearing the cache forces a fresh client fetch using only
50+ // the device token.
51+ if existingToken != token {
52+ Self . clearCachedClerkData ( )
53+ }
3454 } else {
3555 Self . syncJSTokenToNativeKeychainIfNeeded ( )
3656 }
3757
38- Clerk . configure ( publishableKey: publishableKey)
58+ // If already configured with a new bearer token, refresh the client
59+ // to pick up the session associated with the device token we just wrote.
60+ // Clerk.configure() is a no-op on subsequent calls, so we use refreshClient().
61+ if Self . clerkConfigured, let token = bearerToken, !token. isEmpty {
62+ _ = try ? await Clerk . shared. refreshClient ( )
63+ return
64+ }
65+
66+ Self . clerkConfigured = true
67+ if let service = Self . keychainService {
68+ Clerk . configure (
69+ publishableKey: publishableKey,
70+ options: . init( keychainConfig: . init( service: service) )
71+ )
72+ } else {
73+ Clerk . configure ( publishableKey: publishableKey)
74+ }
3975
4076 // Wait for Clerk to finish loading (cached data + API refresh).
4177 // The static configure() fires off async refreshes; poll until loaded.
@@ -52,7 +88,7 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
5288 /// Both expo-secure-store and the native Clerk SDK use the iOS Keychain with the
5389 /// bundle identifier as the service name, making cross-SDK token sharing possible.
5490 private static func syncJSTokenToNativeKeychainIfNeeded( ) {
55- guard let service = Bundle . main . bundleIdentifier , !service. isEmpty else { return }
91+ guard let service = keychainService , !service. isEmpty else { return }
5692
5793 let jsTokenKey = " __clerk_client_jwt "
5894 let nativeTokenKey = " clerkDeviceToken "
@@ -97,35 +133,78 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
97133 SecItemAdd ( writeQuery as CFDictionary , nil )
98134 }
99135
100- /// Writes the provided bearer token as the native SDK's device token,
101- /// but only if the native SDK doesn't already have one.
102- private static func writeNativeDeviceTokenIfNeeded( _ token: String ) {
103- guard let service = Bundle . main. bundleIdentifier, !service. isEmpty else { return }
136+ /// Reads the native device token from keychain, if present.
137+ private static func readNativeDeviceToken( ) -> String ? {
138+ guard let service = keychainService, !service. isEmpty else { return nil }
104139
105- let nativeTokenKey = " clerkDeviceToken "
106-
107- // Check if native SDK already has a device token — don't overwrite
108- let checkQuery : [ String : Any ] = [
140+ var result : CFTypeRef ?
141+ let query : [ String : Any ] = [
109142 kSecClass as String : kSecClassGenericPassword,
110143 kSecAttrService as String : service,
111- kSecAttrAccount as String : nativeTokenKey ,
112- kSecReturnData as String : false ,
144+ kSecAttrAccount as String : " clerkDeviceToken " ,
145+ kSecReturnData as String : true ,
113146 kSecMatchLimit as String : kSecMatchLimitOne,
114147 ]
115- if SecItemCopyMatching ( checkQuery as CFDictionary , nil ) == errSecSuccess {
116- return
148+ guard SecItemCopyMatching ( query as CFDictionary , & result) == errSecSuccess,
149+ let data = result as? Data else { return nil }
150+ return String ( data: data, encoding: . utf8)
151+ }
152+
153+ /// Clears stale cached client and environment data from keychain.
154+ /// This prevents the native SDK from loading a stale anonymous client
155+ /// during initialization, which would conflict with a newly-synced device token.
156+ private static func clearCachedClerkData( ) {
157+ guard let service = keychainService, !service. isEmpty else { return }
158+
159+ for key in [ " cachedClient " , " cachedEnvironment " ] {
160+ let query : [ String : Any ] = [
161+ kSecClass as String : kSecClassGenericPassword,
162+ kSecAttrService as String : service,
163+ kSecAttrAccount as String : key,
164+ ]
165+ SecItemDelete ( query as CFDictionary )
117166 }
167+ }
118168
119- // Write the provided token as native device token
169+ /// Writes the provided bearer token as the native SDK's device token.
170+ /// If the native SDK already has a device token, it is updated with the new value.
171+ private static func writeNativeDeviceToken( _ token: String ) {
172+ guard let service = keychainService, !service. isEmpty else { return }
173+
174+ let nativeTokenKey = " clerkDeviceToken "
120175 guard let tokenData = token. data ( using: . utf8) else { return }
121- let writeQuery : [ String : Any ] = [
176+
177+ // Check if native SDK already has a device token
178+ let checkQuery : [ String : Any ] = [
122179 kSecClass as String : kSecClassGenericPassword,
123180 kSecAttrService as String : service,
124181 kSecAttrAccount as String : nativeTokenKey,
125- kSecValueData as String : tokenData ,
126- kSecAttrAccessible as String : kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ,
182+ kSecReturnData as String : false ,
183+ kSecMatchLimit as String : kSecMatchLimitOne ,
127184 ]
128- SecItemAdd ( writeQuery as CFDictionary , nil )
185+
186+ if SecItemCopyMatching ( checkQuery as CFDictionary , nil ) == errSecSuccess {
187+ // Update the existing token
188+ let updateQuery : [ String : Any ] = [
189+ kSecClass as String : kSecClassGenericPassword,
190+ kSecAttrService as String : service,
191+ kSecAttrAccount as String : nativeTokenKey,
192+ ]
193+ let updateAttributes : [ String : Any ] = [
194+ kSecValueData as String : tokenData,
195+ ]
196+ SecItemUpdate ( updateQuery as CFDictionary , updateAttributes as CFDictionary )
197+ } else {
198+ // Write a new token
199+ let writeQuery : [ String : Any ] = [
200+ kSecClass as String : kSecClassGenericPassword,
201+ kSecAttrService as String : service,
202+ kSecAttrAccount as String : nativeTokenKey,
203+ kSecValueData as String : tokenData,
204+ kSecAttrAccessible as String : kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
205+ ]
206+ SecItemAdd ( writeQuery as CFDictionary , nil )
207+ }
129208 }
130209
131210 public func createAuthViewController(
@@ -206,7 +285,7 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
206285
207286 @MainActor
208287 public func getSession( ) async -> [ String : Any ] ? {
209- guard let session = Clerk . shared. session else {
288+ guard Self . clerkConfigured , let session = Clerk . shared. session else {
210289 return nil
211290 }
212291
@@ -242,7 +321,7 @@ public class ClerkViewFactory: ClerkViewFactoryProtocol {
242321
243322 @MainActor
244323 public func signOut( ) async throws {
245- guard let sessionId = Clerk . shared. session? . id else { return }
324+ guard Self . clerkConfigured , let sessionId = Clerk . shared. session? . id else { return }
246325 try await Clerk . shared. auth. signOut ( sessionId: sessionId)
247326 }
248327}
0 commit comments