diff --git a/.claude/skills/implementing-ios-code/SKILL.md b/.claude/skills/implementing-ios-code/SKILL.md new file mode 100644 index 0000000000..5a19e84bd8 --- /dev/null +++ b/.claude/skills/implementing-ios-code/SKILL.md @@ -0,0 +1,75 @@ +--- +name: implementing-ios-code +description: Implement, write code, add a new screen, create a feature, new view, new processor, or wire up a new service in Bitwarden iOS. Use when asked to "implement", "write code", "add screen", "create feature", "new view", "new processor", "add service", or when translating a design doc into actual Swift code. +--- + +# Implementing iOS Code + +Use this skill to implement Bitwarden iOS features following established patterns. + +## Prerequisites + +- A plan should exist in `.claude/outputs/plans/.md`. If not, invoke `planning-ios-implementation` first. +- Read `Docs/Architecture.md` — it is the authoritative source for all patterns. This skill references it, not replaces it. + +## Step 1: Determine Scope + +From the plan, identify: +- Is this a new feature (full file-set) or modification of existing code? +- Which framework: `BitwardenShared`, `AuthenticatorShared`, or `BitwardenKit`? +- Which domain: `Auth/`, `Autofill/`, `Platform/`, `Tools/`, `Vault/`? + +See `templates.md` for file-set skeletons. + +## Step 2: Core Layer First + +Implement from the bottom up: + +**Data Models** (if needed) +- Request/Response types in `Core//Models/Request/` and `Response/` +- Enum types in `Core//Models/Enum/` + +**Persistence** (if needed) +- Vault sync data → CoreData via `DataStore` (add entities to `Bitwarden.xcdatamodeld`) +- Non-sensitive settings → `AppSettingsStore` (backed by UserDefaults) +- Credentials/keys → `KeychainRepository` +- All three are exposed through `StateService`. Prefer adding a separate protocol over extending `StateService`, `AppSettingsStore`, or `KeychainRepository` directly, to maintain interface segregation. + +**Services / Repositories** +- Define protocol with `// sourcery: AutoMockable` +- Implement `DefaultService` / `DefaultRepository` +- Add `Has` protocol +- See `templates.md` for service skeleton + +## Step 3: UI Layer (File-Set Pattern) + +For new screens, create all required files together (see `templates.md`): + +1. **Route** — Add case to the parent Coordinator's route enum +2. **Coordinator** — Navigation logic, screen instantiation, `Services` typealias +3. **State** — Value type (`struct`) holding all view-observable data +4. **Action** — Enum of user interactions handled synchronously in `receive(_:)` +5. **Effect** — Enum of async work handled in `perform(_:)` +6. **Processor** — `StateProcessor` subclass, business logic only +7. **View** — SwiftUI view using `store.binding`, `store.perform`, `@ObservedObject` + +## Step 4: Wire Dependency Injection + +After creating a new service/repository: +- Add `Has` conformance to `ServiceContainer` via extension +- Add `Has` to the `Services` typealias of any processor that needs it + +## Step 5: Security Check + +Before finishing: +- [ ] Vault data? → Must use `BitwardenSdk` for all encryption/decryption +- [ ] Storing credentials? → Must use `KeychainRepository`, not `AppSettingsStore` +- [ ] User input? → Must validate via `InputValidator` +- [ ] Surfacing errors? → Sensitive errors must implement `NonLoggableError` +- [ ] Running in an extension? → Check Argon2id memory if KDF is involved + +## Step 6: Documentation + +All new public types and methods require DocC (`///`) documentation. +Exceptions: protocol property/function implementations (docs live in the protocol), mock classes. +Use pragma marks to organize code. `// MARK: -` is used to denote different objects in the same file; `// MARK:` is used to denote different sections within an object. diff --git a/.claude/skills/implementing-ios-code/templates.md b/.claude/skills/implementing-ios-code/templates.md new file mode 100644 index 0000000000..cf616510f5 --- /dev/null +++ b/.claude/skills/implementing-ios-code/templates.md @@ -0,0 +1,320 @@ +# iOS Implementation Templates + +Minimal copy-paste skeletons derived from actual codebase patterns. +See `Docs/Architecture.md` for full pattern documentation. + +## New Feature File-Set + +When adding a new screen, create these files (replace `` throughout): + +``` +BitwardenShared/UI/// +├── Processor.swift +├── State.swift +├── Action.swift +├── Effect.swift +└── View.swift +``` + +Add a new case to the **parent** Coordinator's existing `Route` enum rather than creating a new `Route` file. + +**When to create a new Coordinator:** Most screens do NOT need their own coordinator. A single coordinator typically manages an entire feature flow with many routes (e.g., `AuthCoordinator` handles ~30 screens). Only create a new child coordinator when the flow introduces a new navigation container (e.g., a new modal or tab) or becomes complex enough to warrant isolation. When in doubt, add a route to the parent coordinator. + +--- + +## Coordinator Skeleton + +Based on: `BitwardenShared/UI/Auth/AuthCoordinator.swift` + +```swift +import BitwardenKit +import BitwardenResources +import SwiftUI + +// MARK: - Coordinator + +/// A coordinator that manages navigation in the flow. +/// +final class Coordinator: Coordinator, HasStackNavigator { + // MARK: Types + + typealias Services = Has + & Has + + // MARK: Properties + + private let services: Services + private(set) weak var stackNavigator: StackNavigator? + + // MARK: Initialization + + init(services: Services, stackNavigator: StackNavigator) { + self.services = services + self.stackNavigator = stackNavigator + } + + // MARK: Methods + + func navigate(to route: Route, context: AnyObject?) { + switch route { + case .dismiss: + stackNavigator?.dismiss() + case .: + show() + } + } + + func start() {} + + // MARK: Private + + private func show() { + let processor = Processor( + coordinator: asAnyCoordinator(), + services: services, + state: State(), + ) + let store = Store(processor: processor) + let view = View(store: store) + stackNavigator?.push(view) + } +} +``` + +--- + +## Processor Skeleton + +Based on: `BitwardenShared/UI/Auth/Landing/LandingProcessor.swift` + +```swift +import BitwardenKit + +// MARK: - Processor + +/// The processor used to manage state and handle actions for the screen. +/// +class Processor: StateProcessor<State, Action, Effect> { + // MARK: Types + + typealias Services = Has + & Has + + // MARK: Private Properties + + /// The coordinator that handles navigation. + private let coordinator: AnyCoordinator<Route, Event> + + /// The services required by this processor. + private let services: Services + + // MARK: Initialization + + init( + coordinator: AnyCoordinator<Route, Event>, + services: Services, + state: State, + ) { + self.coordinator = coordinator + self.services = services + super.init(state: state) + } + + // MARK: Methods + + override func perform(_ effect: Effect) async { + switch effect { + case .appeared: + await loadData() + } + } + + override func receive(_ action: Action) { + switch action { + case let .someValueChanged(newValue): + state.someValue = newValue + } + } + + // MARK: Private + + private func loadData() async { + do { + // use services..() + } catch { + coordinator.showErrorAlert(error: error) + } + } +} +``` + +--- + +## State / Action / Effect Skeletons + +Based on: `BitwardenShared/UI/Auth/Landing/Landing{State,Action,Effect}.swift` + +```swift +// MARK: - State + +/// An object that defines the current state of a `View`. +/// +struct State: Equatable { + // MARK: Properties + + var someValue: String = "" +} +``` + +```swift +// MARK: - Action + +/// Actions that can be processed by a `Processor`. +/// +enum Action: Equatable { + /// A value was changed by the user. + case someValueChanged(String) +} +``` + +```swift +// MARK: - Effect + +/// Effects that can be processed by a `Processor`. +/// +enum Effect: Equatable { + /// The view appeared on screen. + case appeared +} +``` + +--- + +## View Skeleton + +Based on: `BitwardenShared/UI/Auth/Landing/LandingView.swift` + +```swift +import BitwardenKit +import BitwardenResources +import SwiftUI + +// MARK: - View + +/// A view for . +/// +struct View: View { + // MARK: Properties + + /// The `Store` for this view. + @ObservedObject var store: Store<State, Action, Effect> + + // MARK: View + + var body: some View { + content + .task { + await store.perform(.appeared) + } + } + + // MARK: Private Views + + private var content: some View { + VStack { + // Example: text field backed by store state + BitwardenTextField( + title: Localizations.someLabel, + text: store.binding( + get: \.someValue, + send: Action.someValueChanged, + ), + ) + } + .navigationBarTitle(Localizations.featureTitle, displayMode: .inline) + } +} +``` + +--- + +## Service Skeleton + +Based on: `BitwardenShared/Core/Platform/Services/PasteboardService.swift` + +```swift +import BitwardenKit + +// MARK: - Service + +// sourcery: AutoMockable +/// A protocol for a service that . +/// +protocol Service: AnyObject { + /// Does something asynchronously. + /// + func doSomething() async throws +} + +// MARK: - DefaultService + +/// A default implementation of `Service`. +/// +class DefaultService: Service { + // MARK: Private Properties + + private let errorReporter: ErrorReporter + + // MARK: Initialization + + init(errorReporter: ErrorReporter) { + self.errorReporter = errorReporter + } + + // MARK: Methods + + func doSomething() async throws { + // Implementation + } +} + +// MARK: - HasService + +/// A protocol for an object that provides a `Service`. +/// +protocol HasService { + /// The service used for . + var Service: Service { get } +} +``` + +--- + +## Repository Pattern + +Repositories follow the same protocol + Default + Has* pattern as services, but operate on domain models and typically inject both a data store and a network client: + +```swift +// sourcery: AutoMockable +protocol Repository: AnyObject { + func fetchItems() async throws -> [] +} + +class DefaultRepository: Repository { + private let DataStore: DataStore + private let APIService: APIService + + init(DataStore: DataStore, APIService: APIService) { + self.DataStore = DataStore + self.APIService = APIService + } + + func fetchItems() async throws -> [] { + // Implementation + } +} + +protocol HasRepository { + var Repository: Repository { get } +} +```