From 3ee729070a6f93a17c41c646e077ce413dfeaa3f Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sat, 19 Sep 2026 19:08:54 +0200 Subject: [PATCH] feat: add CredentialProviderViewController skeleton, strip unused storyboard UI Replace the Xcode boilerplate in AutoFill/CredentialProviderViewController.swift with real logic: prepareCredentialList(for:) builds ExtensionRootView (added in Task 19) inside a UIHostingController, provideCredentialWithoutUserInteraction(for:) silently unlocks via the shared Keychain token and completes/cancels the request. Strip the storyboard's now-unused static nav-bar/button/actions, keeping the same customClass so the system still instantiates this exact class. Also add AutoFill target membership for MyPass/Services/FileBookmarkService.swift and BiometricAuthService.swift (app-target files, referenced directly by the extension per the plan) via explicit PBXFileReference/PBXBuildFile entries, since Xcode's synchronized-group file system only auto-includes them in the MyPass target by default. Expected build state: exactly one error, "cannot find 'ExtensionRootView' in scope" (reported once per simulator architecture), until Task 19 adds that type. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf --- AutoFill/Base.lproj/MainInterface.storyboard | 29 ------ .../CredentialProviderViewController.swift | 92 +++++++++++-------- MyPass.xcodeproj/project.pbxproj | 20 +++- 3 files changed, 73 insertions(+), 68 deletions(-) diff --git a/AutoFill/Base.lproj/MainInterface.storyboard b/AutoFill/Base.lproj/MainInterface.storyboard index 255319b..4a328f1 100644 --- a/AutoFill/Base.lproj/MainInterface.storyboard +++ b/AutoFill/Base.lproj/MainInterface.storyboard @@ -17,36 +17,7 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/AutoFill/CredentialProviderViewController.swift b/AutoFill/CredentialProviderViewController.swift index 31ae3f6..d96bbad 100644 --- a/AutoFill/CredentialProviderViewController.swift +++ b/AutoFill/CredentialProviderViewController.swift @@ -6,53 +6,71 @@ // import AuthenticationServices +import SwiftUI +import MyPassCore -class CredentialProviderViewController: ASCredentialProviderViewController { +final class CredentialProviderViewController: ASCredentialProviderViewController { - /* - Prepare your UI to list available credentials for the user to choose from. The items in - 'serviceIdentifiers' describe the service the user is logging in to, so your extension can - prioritize the most relevant credentials in the list. - */ + private let session = VaultSession() + private let bookmarkService = FileBookmarkService() + private let keychainStore = KeychainStore(accessGroup: "org.antiloop222.mypass") + private let biometricService = BiometricAuthService() + + // Called when the user selects MyPass from the QuickType bar. override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { + let ids = serviceIdentifiers.map(\.identifier) + showUI(serviceIdentifiers: ids) } - /* - Implement this method if your extension supports showing credentials in the QuickType bar. - When the user selects a credential from your app, this method will be called with the - ASPasswordCredentialIdentity your app has previously saved to the ASCredentialIdentityStore. - Provide the password by completing the extension request with the associated ASPasswordCredential. - If using the credential would require showing custom UI for authenticating the user, cancel - the request with error code ASExtensionError.userInteractionRequired. - + // Called for inline QuickType suggestion (no UI shown). override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { - let databaseIsUnlocked = true - if (databaseIsUnlocked) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) - } else { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue)) + Task { + do { + try await unlockSilently() + let all = session.allEntries() + if let entry = all.first(where: { $0.id.uuidString == credentialIdentity.recordIdentifier }) { + let credential = ASPasswordCredential(user: entry.username, password: entry.password.reveal()) + self.extensionContext.completeRequest(withSelectedCredential: credential, completionHandler: nil) + } else { + self.extensionContext.cancelRequest(withError: ASExtensionError(.credentialIdentityNotFound)) + } + } catch { + self.extensionContext.cancelRequest(withError: ASExtensionError(.userInteractionRequired)) + } } } - */ - /* - Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with - ASExtensionError.userInteractionRequired. In this case, the system may present your extension's - UI and call this method. Show appropriate UI for authenticating the user then provide the password - by completing the extension request with the associated ASPasswordCredential. - - override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { - } - */ - - @IBAction func cancel(_ sender: AnyObject?) { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) + private func unlockSilently() async throws { + guard session.isLocked else { return } + let password = try keychainStore.load(for: "masterPassword") + let url = try bookmarkService.resolveURL() + defer { bookmarkService.stopAccess(url: url) } + try session.unlock(url: url, password: password) } - @IBAction func passwordSelected(_ sender: AnyObject?) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + private func showUI(serviceIdentifiers: [String]) { + let rootView = ExtensionRootView( + session: session, + serviceIdentifiers: serviceIdentifiers, + bookmarkService: bookmarkService, + keychainStore: keychainStore, + biometricService: biometricService, + onSelect: { [weak self] (entry: Entry) in + let credential = ASPasswordCredential( + user: entry.username, + password: entry.password.reveal() + ) + self?.extensionContext.completeRequest(withSelectedCredential: credential, completionHandler: nil) + }, + onCancel: { [weak self] in + self?.extensionContext.cancelRequest(withError: ASExtensionError(.userCanceled)) + } + ) + let host = UIHostingController(rootView: rootView) + addChild(host) + view.addSubview(host.view) + host.view.frame = view.bounds + host.view.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth, UIView.AutoresizingMask.flexibleHeight] + host.didMove(toParent: self) } - } diff --git a/MyPass.xcodeproj/project.pbxproj b/MyPass.xcodeproj/project.pbxproj index 7886a7f..95ecdca 100644 --- a/MyPass.xcodeproj/project.pbxproj +++ b/MyPass.xcodeproj/project.pbxproj @@ -7,9 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + EABAC2B148F845C8A20EC2CC /* FileBookmarkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBBB9F1BA62473294D2B5B5 /* FileBookmarkService.swift */; }; + 982CE38F924E4FD387BC2C20 /* BiometricAuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7DFA4A59640D7BF6B4960 /* BiometricAuthService.swift */; }; 205678DB2FC0EB5200251C6B /* MyPassCore in Frameworks */ = {isa = PBXBuildFile; productRef = 205678DA2FC0EB5200251C6B /* MyPassCore */; }; 2096B432305EC2B200C2F253 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 205678E32FC0F52A00251C6B /* AuthenticationServices.framework */; }; - 2096B43D305EC2B200C2F253 /* AutoFill.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2096B431305EC2B200C2F253 /* AutoFill.appex */; platformFilters = (ios, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 2096B43D305EC2B200C2F253 /* AutoFill.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2096B431305EC2B200C2F253 /* AutoFill.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 67DE2FCFC21FD6DF5E761C87 /* MyPassCore in Frameworks */ = {isa = PBXBuildFile; productRef = 205678DA2FC0EB5200251C6B /* MyPassCore */; }; /* End PBXBuildFile section */ @@ -52,6 +54,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + ADBBB9F1BA62473294D2B5B5 /* FileBookmarkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FileBookmarkService.swift; path = MyPass/Services/FileBookmarkService.swift; sourceTree = SOURCE_ROOT; }; + D6C7DFA4A59640D7BF6B4960 /* BiometricAuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BiometricAuthService.swift; path = MyPass/Services/BiometricAuthService.swift; sourceTree = SOURCE_ROOT; }; 205678932FBF9CBB00251C6B /* MyPass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyPass.app; sourceTree = BUILT_PRODUCTS_DIR; }; 205678A22FBF9CBC00251C6B /* MyPassTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyPassTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 205678AC2FBF9CBC00251C6B /* MyPassUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyPassUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -130,6 +134,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1A2B3C4D5E6F7A8B9C0D1E2F /* AutoFill Shared Services */ = { + isa = PBXGroup; + children = ( + ADBBB9F1BA62473294D2B5B5 /* FileBookmarkService.swift */, + D6C7DFA4A59640D7BF6B4960 /* BiometricAuthService.swift */, + ); + name = "AutoFill Shared Services"; + sourceTree = ""; + }; 2056788A2FBF9CBB00251C6B = { isa = PBXGroup; children = ( @@ -137,6 +150,7 @@ 205678A52FBF9CBC00251C6B /* MyPassTests */, 205678AF2FBF9CBC00251C6B /* MyPassUITests */, 2096B433305EC2B200C2F253 /* AutoFill */, + 1A2B3C4D5E6F7A8B9C0D1E2F /* AutoFill Shared Services */, 205678E22FC0F52A00251C6B /* Frameworks */, 205678942FBF9CBB00251C6B /* Products */, ); @@ -366,6 +380,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + EABAC2B148F845C8A20EC2CC /* FileBookmarkService.swift in Sources */, + 982CE38F924E4FD387BC2C20 /* BiometricAuthService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -384,7 +400,7 @@ }; 2096B43C305EC2B200C2F253 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - platformFilters = (ios, ); + platformFilter = ios; target = 2096B430305EC2B200C2F253 /* AutoFill */; targetProxy = 2096B43B305EC2B200C2F253 /* PBXContainerItemProxy */; };