From 877e5619ff19352ad123f084c882eb76e5f5d588 Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Thu, 11 Jun 2026 22:07:18 -0400 Subject: [PATCH 1/3] feat(ios): resolve File Provider items via item(for:) lookup Implement item(for:) so the system can resolve any identifier to a FileProviderItem: return the root container item, decode folder/file identifiers and fetch their metadata from the Drive API, and map 401 responses to NSFileProviderError.notAuthenticated so the system can re-request credentials. --- .../FileProviderExtension.swift | 61 +++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/ios/InternxtFileProvider/FileProviderExtension.swift b/ios/InternxtFileProvider/FileProviderExtension.swift index c1802b584..f59fba9c9 100644 --- a/ios/InternxtFileProvider/FileProviderExtension.swift +++ b/ios/InternxtFileProvider/FileProviderExtension.swift @@ -9,31 +9,70 @@ import FileProvider import InternxtSwiftCore class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension { + private let rootDisplayName: String + required init(domain: NSFileProviderDomain) { + self.rootDisplayName = domain.displayName super.init() } - private func currentAuthToken() -> String? { - guard let data = SharedAuthKeychain.read(SharedAuthKeychain.photosTokenKey) else { return nil } - return String(data: data, encoding: .utf8) - } - func invalidate() { // TODO: cleanup any resources } func item(for identifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) -> Progress { - _ = currentAuthToken() + if identifier == .rootContainer { + let rootItem = FileProviderItem.root(displayName: rootDisplayName) + completionHandler(rootItem, nil) + return Progress() + } - // TODO: implement the actual lookup using the auth token + guard let decoded = FileProviderItemID.decode(identifier) else { + completionHandler(nil, NSFileProviderError(.noSuchItem)) + return Progress() + } - completionHandler(FileProviderItem(identifier: identifier), nil) - return Progress() + guard let driveAPI = DriveAPIFactory.make() else { + completionHandler(nil, NSFileProviderError(.notAuthenticated)) + return Progress() + } + + let progress = Progress(totalUnitCount: 1) + Task { + do { + let item = try await resolveItem(decoded, identifier: identifier, driveAPI: driveAPI) + progress.completedUnitCount = 1 + completionHandler(item, nil) + } catch { + completionHandler(nil, lookupError(from: error)) + } + } + return progress } - func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier, version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest, completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress { - _ = currentAuthToken() + private func resolveItem( + _ decoded: (kind: DriveItemKind, uuid: String), + identifier: NSFileProviderItemIdentifier, + driveAPI: DriveAPI + ) async throws -> FileProviderItem { + switch decoded.kind { + case .folder: + let meta = try await driveAPI.getFolderMetaByUuid(uuid: decoded.uuid) + return FileProviderItem(folderMeta: meta, identifier: identifier) + case .file: + let meta = try await driveAPI.getFileMetaByUuid(uuid: decoded.uuid) + return FileProviderItem(fileMeta: meta, identifier: identifier) + } + } + private func lookupError(from error: Error) -> Error { + if let apiError = error as? APIClientError, apiError.statusCode == 401 { + return NSFileProviderError(.notAuthenticated) + } + return error + } + + func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier, version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest, completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress { // TODO: implement fetching of the contents for the itemIdentifier at the specified version completionHandler(nil, nil, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])) From caa8f2aaccfedd3aa50102b0ce833f4d81cb3adf Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Thu, 11 Jun 2026 22:07:49 -0400 Subject: [PATCH 2/3] feat(auth): forward driveBaseUrl to shared keychain on credential write Persist the Drive API base URL in the App Group keychain when credentials are written, so the File Provider extension can build authenticated Drive requests. Add a wrapper test asserting the native module receives driveBaseUrl. --- ios/Internxt/InternxtAuthCredentialsModule.swift | 1 + src/services/native/InternxtAuthCredentialsModule.spec.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/ios/Internxt/InternxtAuthCredentialsModule.swift b/ios/Internxt/InternxtAuthCredentialsModule.swift index d5f7613f7..b19c5e249 100644 --- a/ios/Internxt/InternxtAuthCredentialsModule.swift +++ b/ios/Internxt/InternxtAuthCredentialsModule.swift @@ -56,6 +56,7 @@ class InternxtAuthCredentialsModule: NSObject { writeIfPresent(creds["rootFolderUuid"], to: SharedAuthKeychain.rootFolderIdKey) writeIfPresent(creds["bridgeUser"], to: SharedAuthKeychain.bridgeUserKey) writeIfPresent(creds["userId"], to: SharedAuthKeychain.userIdKey) + writeIfPresent(creds["driveBaseUrl"], to: SharedAuthKeychain.driveBaseUrlKey) } private func writeIfPresent(_ value: Any?, to sharedKey: String) { diff --git a/src/services/native/InternxtAuthCredentialsModule.spec.ts b/src/services/native/InternxtAuthCredentialsModule.spec.ts index f815ba694..7f5193263 100644 --- a/src/services/native/InternxtAuthCredentialsModule.spec.ts +++ b/src/services/native/InternxtAuthCredentialsModule.spec.ts @@ -45,6 +45,14 @@ describe('InternxtAuthCredentialsModule wrapper', () => { expect(setCredentials).toHaveBeenCalledWith(credentials); }); + it('when on iOS with the module present, then setCredentials forwards driveBaseUrl to the native module', async () => { + const { wrapper, setCredentials } = arrangePresentNative('ios'); + + await wrapper.setCredentials(credentials); + + expect(setCredentials.mock.calls[0][0]).toMatchObject({ driveBaseUrl: 'https://drive.example' }); + }); + it('when on iOS with the module present, then clearCredentials delegates to the native module', async () => { const { wrapper, clearCredentials } = arrangePresentNative('ios'); From 5bfc79f6dde858cdcaf7dad177c88abc40a8802d Mon Sep 17 00:00:00 2001 From: Francis Terrero Date: Thu, 11 Jun 2026 22:08:03 -0400 Subject: [PATCH 3/3] feat(ios): signal working set after domain registration Add FileProviderDomainManager.signalEnumeration to notify the working set enumerator, and call it once the File Provider domain is registered so the Files app refreshes Drive contents right after credentials change. Signal failures are logged and ignored to avoid blocking login. --- ios/Internxt/FileProviderDomainManager.swift | 14 ++++++++++++++ ios/Internxt/InternxtAuthCredentialsModule.swift | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ios/Internxt/FileProviderDomainManager.swift b/ios/Internxt/FileProviderDomainManager.swift index 2d714704e..60ae0360c 100644 --- a/ios/Internxt/FileProviderDomainManager.swift +++ b/ios/Internxt/FileProviderDomainManager.swift @@ -22,6 +22,20 @@ enum FileProviderDomainManager { } } + static func signalEnumeration(completion: @escaping (Error?) -> Void) { + guard let manager = NSFileProviderManager(for: domain) else { + completion(NSError( + domain: "FileProviderDomainManager", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "No NSFileProviderManager for domain \(domainIdentifier.rawValue)"] + )) + return + } + manager.signalEnumerator(for: .workingSet) { error in + completion(error) + } + } + private static func isAlreadyExists(_ error: Error?) -> Bool { guard let error = error as NSError? else { return false } return error.domain == NSCocoaErrorDomain && error.code == NSFileWriteFileExistsError diff --git a/ios/Internxt/InternxtAuthCredentialsModule.swift b/ios/Internxt/InternxtAuthCredentialsModule.swift index b19c5e249..2f8391722 100644 --- a/ios/Internxt/InternxtAuthCredentialsModule.swift +++ b/ios/Internxt/InternxtAuthCredentialsModule.swift @@ -26,7 +26,12 @@ class InternxtAuthCredentialsModule: NSObject { rejecter("E_REGISTER_DOMAIN", error.localizedDescription, error as NSError) return } - resolver(nil) + FileProviderDomainManager.signalEnumeration { signalError in + if let signalError = signalError { + NSLog("InternxtAuthCredentialsModule: signalEnumeration failed (ignored): \(signalError.localizedDescription)") + } + resolver(nil) + } } }