docs: add corrected implementation plan for AutoFill extension UI
Supersedes Tasks 18-20 of the original plan with fixed bundle identifiers (org.antiloop222, not com.christophevila), the correct AutoFill/ folder name (renamed from autofill/ during Phase 2), and a real bug fix found before implementation: the original Task 18 named its view controller class differently from what the extension's storyboard actually instantiates, which would have silently left the default boilerplate UI running instead of any real code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
This commit is contained in:
@@ -0,0 +1,489 @@
|
||||
# AutoFill Extension UI Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the actual AutoFill Credential Provider extension UI — biometric/password unlock, and a credential list filtered by the requesting app/website — completing Phase 5 (the last phase) of MyPass's original implementation plan.
|
||||
|
||||
**Architecture:** The `AutoFill` app-extension target (already wired up: App Group, Keychain Sharing, `MyPassCore` linked — see `docs/superpowers/plans/2026-05-21-mypass.md` Task 9) gets its own `ASCredentialProviderViewController` subclass that hosts a small SwiftUI view hierarchy (`ExtensionRootView` → unlock screen or `CredentialListView`), backed by its own `ExtensionViewModel`. It reuses `MyPassCore`'s `VaultSession`, `KeychainStore`, `FileBookmarkService` (app target), `BiometricAuthService` (app target), and `CredentialMatcher` directly — no new `MyPassCore` logic needed for this phase.
|
||||
|
||||
**Tech Stack:** Swift 5.9+, SwiftUI hosted inside `AuthenticationServices.ASCredentialProviderViewController` via `UIHostingController`, same `MyPassCore` package and app-target services as the rest of the project.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-05-21-mypass-design.md` — see "AutoFill Credential Provider Extension" section (runtime flow, onboarding, URL matching, extension constraints). Original task-by-task source for this phase is `docs/superpowers/plans/2026-05-21-mypass.md` Tasks 18-20 — this plan supersedes those three tasks with corrected identifiers, the correct extension folder name, and a storyboard-wiring fix (see below). Do not re-run Tasks 18-20 from that file; this plan replaces them.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Bundle identifier prefix is `org.antiloop222`, not `com.christophevila`** (the original Tasks 18-20 text predates the September 2026 rebrand — see `docs/superpowers/specs/2026-05-21-mypass-design.md`'s Architecture section). `KeychainStore(accessGroup:)` must be constructed with `"org.antiloop222.mypass"`.
|
||||
- **The extension's folder is `AutoFill/`, not `AutoFillExtension/`** (the target itself was renamed from Xcode's wizard-generated `autofill` to `AutoFill` during Phase 2 — see `docs/superpowers/plans/2026-05-21-mypass.md` Task 9's history). All file paths below are under `AutoFill/`.
|
||||
- **Storyboard class-name compatibility (a real bug found before this plan's tasks were written):** the extension's entry point is `AutoFill/Base.lproj/MainInterface.storyboard`, whose scene has `customClass="CredentialProviderViewController"` — the system instantiates a view controller by that exact class name at launch. The original Task 18 named its class `AutoFillViewController`, which the storyboard would never actually instantiate, silently leaving the default boilerplate ("Return Example Password" button, wired to methods that no longer exist) as the only thing the user ever sees. This plan's Task 18 keeps the class named `CredentialProviderViewController` to match the storyboard, and Task 18 also strips the storyboard's now-unused static button/navigation-bar/actions so nothing dangling points at removed selectors.
|
||||
- **Extension memory/lifecycle constraint (from the spec):** the KDBX file is parsed fresh on each extension invocation — no persistent cache inside the extension process.
|
||||
|
||||
---
|
||||
|
||||
## Task 18: CredentialProviderViewController (extension entry point)
|
||||
|
||||
**Files:**
|
||||
- Create: `AutoFill/CredentialProviderViewController.swift` (replaces the boilerplate content of the existing file of the same name — same class name, new implementation)
|
||||
- Modify: `AutoFill/Base.lproj/MainInterface.storyboard` (strip the unused static button/nav-bar/actions; keep the scene and its `customClass`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `VaultSession`, `KeychainStore`, `CredentialMatcher.filter(entries:for:)` (all `MyPassCore`, existing), `FileBookmarkService`, `BiometricAuthService` (app target, existing, from `docs/superpowers/plans/2026-05-21-mypass.md` Tasks 10-11).
|
||||
- Produces: `ExtensionRootView(session:serviceIdentifiers:bookmarkService:keychainStore:biometricService:onSelect:onCancel:)` — used by Task 19 (not yet defined; this task references it, Task 19 defines it, matching the existing project's own established pattern of forward-referencing a not-yet-created type across adjacent tasks — see `docs/superpowers/plans/2026-05-21-mypass.md` Task 18's own note "Build (will fail until ExtensionRootView is created in Task 19)").
|
||||
|
||||
- [ ] **Step 1: Write CredentialProviderViewController**
|
||||
|
||||
```swift
|
||||
// AutoFill/CredentialProviderViewController.swift
|
||||
import AuthenticationServices
|
||||
import SwiftUI
|
||||
import MyPassCore
|
||||
|
||||
final class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Called for inline QuickType suggestion (no UI shown).
|
||||
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private func showUI(serviceIdentifiers: [String]) {
|
||||
let rootView = ExtensionRootView(
|
||||
session: session,
|
||||
serviceIdentifiers: serviceIdentifiers,
|
||||
bookmarkService: bookmarkService,
|
||||
keychainStore: keychainStore,
|
||||
biometricService: biometricService,
|
||||
onSelect: { [weak self] 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 = [.flexibleWidth, .flexibleHeight]
|
||||
host.didMove(toParent: self)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Strip the storyboard's unused static UI**
|
||||
|
||||
Open `AutoFill/Base.lproj/MainInterface.storyboard` in a text editor (not Xcode's Interface Builder, to make an exact, reviewable diff). Replace the `<viewController>` element's contents so it has an empty view (no navigation bar, no button, no `<connections>` blocks referencing `cancel:`/`passwordSelected:`), while keeping the same `id`, `customClass="CredentialProviderViewController"`, and the scene/document structure exactly as-is otherwise. The full corrected file:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14092" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Xki-Si-B7m">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14081.1"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Credential Provider View Controller-->
|
||||
<scene sceneID="Uma-9u-xWV">
|
||||
<objects>
|
||||
<viewController id="Xki-Si-B7m" customClass="CredentialProviderViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<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"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="Ky8-vK-JVj"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="RwB-HB-TSk" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build (expected to fail until Task 19 creates ExtensionRootView)**
|
||||
|
||||
```bash
|
||||
xcodebuild -project MyPass.xcodeproj -scheme AutoFill -destination 'generic/platform=iOS Simulator' build 2>&1 | grep -i "error:"
|
||||
```
|
||||
|
||||
Expected: exactly one error, `cannot find 'ExtensionRootView' in scope` in `AutoFill/CredentialProviderViewController.swift`. No other errors.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add AutoFill/CredentialProviderViewController.swift AutoFill/Base.lproj/MainInterface.storyboard
|
||||
git commit -m "feat: add CredentialProviderViewController skeleton, strip unused storyboard UI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 19: ExtensionUnlockView + ExtensionRootView + ExtensionViewModel
|
||||
|
||||
**Files:**
|
||||
- Create: `AutoFill/Views/ExtensionUnlockView.swift`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `VaultSession`, `Entry`, `KDBXError` (`MyPassCore`), `FileBookmarkService`, `BiometricAuthService` (app target).
|
||||
- Produces: `ExtensionRootView` (used by Task 18, already committed — this task makes Task 18's forward reference resolve), `CredentialListView(vm:)` — used by Task 20 (not yet defined; same forward-reference pattern as Task 18 → 19).
|
||||
|
||||
- [ ] **Step 1: Write ExtensionUnlockView, ExtensionRootView, and ExtensionViewModel**
|
||||
|
||||
```swift
|
||||
// AutoFill/Views/ExtensionUnlockView.swift
|
||||
import SwiftUI
|
||||
import MyPassCore
|
||||
|
||||
/// Root view for the extension -- shows unlock screen or credential list depending on state.
|
||||
struct ExtensionRootView: View {
|
||||
@StateObject private var vm: ExtensionViewModel
|
||||
|
||||
init(
|
||||
session: VaultSession,
|
||||
serviceIdentifiers: [String],
|
||||
bookmarkService: FileBookmarkService,
|
||||
keychainStore: KeychainStore,
|
||||
biometricService: BiometricAuthService,
|
||||
onSelect: @escaping (Entry) -> Void,
|
||||
onCancel: @escaping () -> Void
|
||||
) {
|
||||
_vm = StateObject(wrappedValue: ExtensionViewModel(
|
||||
session: session,
|
||||
serviceIdentifiers: serviceIdentifiers,
|
||||
bookmarkService: bookmarkService,
|
||||
keychainStore: keychainStore,
|
||||
biometricService: biometricService,
|
||||
onSelect: onSelect,
|
||||
onCancel: onCancel
|
||||
))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
SwiftUI.Group {
|
||||
if vm.isLocked {
|
||||
extensionUnlockView
|
||||
} else {
|
||||
CredentialListView(vm: vm)
|
||||
}
|
||||
}
|
||||
.navigationTitle("MyPass")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel", action: vm.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task { await vm.tryBiometricUnlock() }
|
||||
}
|
||||
|
||||
private var extensionUnlockView: some View {
|
||||
VStack(spacing: 24) {
|
||||
Spacer()
|
||||
Image(systemName: "lock.shield.fill").font(.system(size: 48)).foregroundStyle(.tint)
|
||||
Text("Vault Locked").font(.headline)
|
||||
|
||||
if vm.canUseBiometrics {
|
||||
Button(action: { Task { await vm.tryBiometricUnlock() } }) {
|
||||
Label("Use Face ID / Touch ID", systemImage: "faceid")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
SecureField("Master Password", text: $vm.password)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.onSubmit { Task { await vm.unlockWithPassword() } }
|
||||
|
||||
Button("Unlock") { Task { await vm.unlockWithPassword() } }
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(vm.password.isEmpty || vm.isUnlocking)
|
||||
|
||||
if let msg = vm.errorMessage {
|
||||
Text(msg).foregroundStyle(.red).font(.caption).multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
if !vm.hasVault {
|
||||
Link(destination: URL(string: "mypass://unlock")!) {
|
||||
Label("Open MyPass to set up vault", systemImage: "arrow.up.right")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ExtensionViewModel: ObservableObject {
|
||||
@Published var password: String = ""
|
||||
@Published var errorMessage: String?
|
||||
@Published var isUnlocking: Bool = false
|
||||
|
||||
let session: VaultSession
|
||||
let serviceIdentifiers: [String]
|
||||
private let bookmarkService: FileBookmarkService
|
||||
private let keychainStore: KeychainStore
|
||||
private let biometricService: BiometricAuthService
|
||||
private let onSelect: (Entry) -> Void
|
||||
private let onCancel: () -> Void
|
||||
|
||||
init(
|
||||
session: VaultSession,
|
||||
serviceIdentifiers: [String],
|
||||
bookmarkService: FileBookmarkService,
|
||||
keychainStore: KeychainStore,
|
||||
biometricService: BiometricAuthService,
|
||||
onSelect: @escaping (Entry) -> Void,
|
||||
onCancel: @escaping () -> Void
|
||||
) {
|
||||
self.session = session
|
||||
self.serviceIdentifiers = serviceIdentifiers
|
||||
self.bookmarkService = bookmarkService
|
||||
self.keychainStore = keychainStore
|
||||
self.biometricService = biometricService
|
||||
self.onSelect = onSelect
|
||||
self.onCancel = onCancel
|
||||
}
|
||||
|
||||
var isLocked: Bool { session.isLocked }
|
||||
var hasVault: Bool { bookmarkService.hasBookmark }
|
||||
var canUseBiometrics: Bool { biometricService.isAvailable && keychainStore.exists(for: "masterPassword") }
|
||||
|
||||
var suggestedEntries: [Entry] {
|
||||
CredentialMatcher.filter(entries: session.allEntries(), for: serviceIdentifiers).suggested
|
||||
}
|
||||
|
||||
var allEntries: [Entry] {
|
||||
CredentialMatcher.filter(entries: session.allEntries(), for: serviceIdentifiers).all
|
||||
}
|
||||
|
||||
func select(_ entry: Entry) { onSelect(entry) }
|
||||
func cancel() { onCancel() }
|
||||
|
||||
func tryBiometricUnlock() async {
|
||||
guard canUseBiometrics, session.isLocked else { return }
|
||||
isUnlocking = true
|
||||
do {
|
||||
try await biometricService.authenticate(reason: "Unlock MyPass")
|
||||
try performUnlockFromKeychain()
|
||||
} catch {
|
||||
// Silently fail -- user can type password
|
||||
}
|
||||
isUnlocking = false
|
||||
}
|
||||
|
||||
func unlockWithPassword() async {
|
||||
isUnlocking = true
|
||||
errorMessage = nil
|
||||
do {
|
||||
let url = try bookmarkService.resolveURL()
|
||||
defer { bookmarkService.stopAccess(url: url) }
|
||||
try session.unlock(url: url, password: password)
|
||||
try keychainStore.save(password: password, for: "masterPassword")
|
||||
password = ""
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
isUnlocking = false
|
||||
}
|
||||
|
||||
private func performUnlockFromKeychain() throws {
|
||||
let pw = try keychainStore.load(for: "masterPassword")
|
||||
let url = try bookmarkService.resolveURL()
|
||||
defer { bookmarkService.stopAccess(url: url) }
|
||||
try session.unlock(url: url, password: pw)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: unlike the original Tasks 18-20 draft, `unlockWithPassword()`'s `catch` clause here uses the generic `error.localizedDescription` for ALL errors (not a special-cased `catch KDBXError.invalidPassword`), matching the fix already applied to the main app's `UnlockViewModel` (see `MyPass/ViewModels/UnlockViewModel.swift` and `MyPassCore/Sources/MyPassCore/KDBX/KDBXError.swift`'s `LocalizedError` conformance) -- `KDBXError` already produces a readable message for every case, so there is no need to special-case `.invalidPassword` here either.
|
||||
|
||||
- [ ] **Step 2: Build (expected to fail until Task 20 creates CredentialListView)**
|
||||
|
||||
```bash
|
||||
xcodebuild -project MyPass.xcodeproj -scheme AutoFill -destination 'generic/platform=iOS Simulator' build 2>&1 | grep -i "error:"
|
||||
```
|
||||
|
||||
Expected: exactly one error, `cannot find 'CredentialListView' in scope` in `AutoFill/Views/ExtensionUnlockView.swift`. No other errors (in particular, Task 18's `ExtensionRootView` forward-reference error should be gone now).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add AutoFill/Views/ExtensionUnlockView.swift
|
||||
git commit -m "feat: add ExtensionRootView and ExtensionViewModel"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 20: CredentialListView (extension)
|
||||
|
||||
**Files:**
|
||||
- Create: `AutoFill/Views/CredentialListView.swift`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ExtensionViewModel` (Task 19, already committed).
|
||||
- Produces: nothing further consumed by later tasks -- this is the last task in the phase.
|
||||
|
||||
- [ ] **Step 1: Write CredentialListView**
|
||||
|
||||
```swift
|
||||
// AutoFill/Views/CredentialListView.swift
|
||||
import SwiftUI
|
||||
import MyPassCore
|
||||
|
||||
struct CredentialListView: View {
|
||||
@ObservedObject var vm: ExtensionViewModel
|
||||
@State private var searchQuery: String = ""
|
||||
|
||||
private var displayedSuggested: [Entry] {
|
||||
searchQuery.isEmpty ? vm.suggestedEntries : []
|
||||
}
|
||||
|
||||
private var displayedAll: [Entry] {
|
||||
let entries = searchQuery.isEmpty ? vm.allEntries : vm.session.allEntries()
|
||||
guard !searchQuery.isEmpty else { return entries }
|
||||
let q = searchQuery.lowercased()
|
||||
return entries.filter {
|
||||
$0.title.lowercased().contains(q)
|
||||
|| $0.username.lowercased().contains(q)
|
||||
|| $0.url.lowercased().contains(q)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if !displayedSuggested.isEmpty {
|
||||
Section("Suggested") {
|
||||
ForEach(displayedSuggested) { entry in
|
||||
CredentialRow(entry: entry) { vm.select(entry) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(displayedSuggested.isEmpty ? "" : "All Entries") {
|
||||
if displayedAll.isEmpty {
|
||||
Text("No entries found").foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(displayedAll) { entry in
|
||||
CredentialRow(entry: entry) { vm.select(entry) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchQuery, prompt: "Search…")
|
||||
}
|
||||
}
|
||||
|
||||
private struct CredentialRow: View {
|
||||
let entry: Entry
|
||||
let onSelect: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: onSelect) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(entry.title).font(.body).foregroundStyle(.primary)
|
||||
Text(entry.username).font(.caption).foregroundStyle(.secondary)
|
||||
if !entry.url.isEmpty {
|
||||
Text(entry.url).font(.caption2).foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build -- should now succeed with zero errors**
|
||||
|
||||
```bash
|
||||
xcodebuild -project MyPass.xcodeproj -scheme AutoFill -destination 'generic/platform=iOS Simulator' build 2>&1 | grep -i "error:"
|
||||
```
|
||||
|
||||
Expected: no output (no errors). Also confirm the main app scheme still builds clean (this phase doesn't touch it, but the extension links against the same `MyPassCore` target, so a sanity check is cheap):
|
||||
|
||||
```bash
|
||||
xcodebuild -project MyPass.xcodeproj -scheme MyPass -destination 'generic/platform=iOS Simulator' build 2>&1 | grep -i "error:"
|
||||
```
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add AutoFill/Views/CredentialListView.swift
|
||||
git commit -m "feat: add CredentialListView for AutoFill extension"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 21: Final verification
|
||||
|
||||
**Files:** none (verification only).
|
||||
|
||||
- [ ] **Step 1: Build both the AutoFill extension scheme and the main app scheme clean**
|
||||
|
||||
```bash
|
||||
xcodebuild -project MyPass.xcodeproj -scheme AutoFill -destination 'generic/platform=iOS Simulator' build 2>&1 | tail -5
|
||||
xcodebuild -project MyPass.xcodeproj -scheme MyPass -destination 'generic/platform=iOS Simulator' build 2>&1 | tail -5
|
||||
```
|
||||
|
||||
Expected: `** BUILD SUCCEEDED **` for both.
|
||||
|
||||
- [ ] **Step 2: Manual smoke test in the Simulator**
|
||||
|
||||
Per the spec's own onboarding note, the extension must be manually enabled first: **Settings → Passwords → AutoFill Passwords → MyPass** (toggle on) on the simulator. Then:
|
||||
- Open Safari (or another app with a login form) and navigate to a site/field that matches a known entry's URL
|
||||
- Tap the field -- the QuickType bar should show MyPass's icon
|
||||
- Tap it -- the extension UI should appear, locked (unless a recent main-app unlock already populated the shared Keychain token)
|
||||
- Unlock with password (or Face ID, if biometrics are set up and enabled for this simulator)
|
||||
- Verify "Suggested" section shows entries matching the site's domain, "All Entries" shows the rest, and search narrows both
|
||||
- Tap an entry -- the field in the host app should be filled and the extension should dismiss
|
||||
- Tap Cancel from the unlock screen -- the extension should dismiss without filling anything
|
||||
|
||||
- [ ] **Step 3: Final commit (if the smoke test surfaced fixes)**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix: address issues found in AutoFill extension manual verification"
|
||||
```
|
||||
|
||||
(Skip this step if the smoke test found nothing to fix.)
|
||||
Reference in New Issue
Block a user