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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
This commit is contained in:
2026-09-19 19:08:54 +02:00
co-authored by Claude Sonnet 5
parent da5ad145b0
commit 3ee729070a
3 changed files with 73 additions and 68 deletions
@@ -17,36 +17,7 @@
<view key="view" contentMode="scaleToFill" id="BuU-Ak-iZz">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3wq-kG-lGu">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<items>
<navigationItem id="cbj-pk-SYj">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="bEZ-MG-jDy">
<connections>
<action selector="cancel:" destination="Xki-Si-B7m" id="6ap-3Q-iEX"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="a7v-ug-QzG">
<rect key="frame" x="87.5" y="327" width="199" height="33"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Return Example Password"/>
<connections>
<action selector="passwordSelected:" destination="Xki-Si-B7m" eventType="touchUpInside" id="ODd-lr-mud"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="Ky8-vK-JVj" firstAttribute="top" secondItem="3wq-kG-lGu" secondAttribute="top" id="BIN-jb-uNd"/>
<constraint firstItem="3wq-kG-lGu" firstAttribute="width" secondItem="BuU-Ak-iZz" secondAttribute="width" id="UkD-v4-BcH"/>
<constraint firstItem="a7v-ug-QzG" firstAttribute="centerY" secondItem="Ky8-vK-JVj" secondAttribute="centerY" id="fAC-0v-NFE"/>
<constraint firstItem="a7v-ug-QzG" firstAttribute="centerX" secondItem="3wq-kG-lGu" secondAttribute="centerX" id="io1-ZS-gwn"/>
<constraint firstItem="3wq-kG-lGu" firstAttribute="centerX" secondItem="BuU-Ak-iZz" secondAttribute="centerX" id="rtV-5c-0bl"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Ky8-vK-JVj"/>
</view>
</viewController>
+55 -37
View File
@@ -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)
}
}
+18 -2
View File
@@ -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 = "<group>";
};
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 */;
};