{"owner":"bitwarden","repo":"android","hasSkills":true,"totalSkillsCount":35,"totalTokensCount":62874,"categories":["claude-rule","command-prompt","anthropic-skill","project-spec","cursor-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/CLAUDE.md",".claude/commands/plan-android-work.md",".claude/commands/review-android.md",".claude/commands/work-on-android.md",".claude/skills/build-test-verify/SKILL.md",".claude/skills/evaluating-sdk-internal-updates/SKILL.md",".claude/skills/implementing-android-code/SKILL.md",".claude/skills/implementing-android-code/templates.md",".claude/skills/interacting-with-android-device/SKILL.md",".claude/skills/planning-android-implementation/SKILL.md",".claude/skills/refining-android-requirements/SKILL.md",".claude/skills/reviewing-changes/SKILL.md",".claude/skills/reviewing-changes/checklists/bug-fix.md",".claude/skills/reviewing-changes/checklists/dependency-update.md",".claude/skills/reviewing-changes/checklists/feature-addition.md",".claude/skills/reviewing-changes/checklists/infrastructure.md",".claude/skills/reviewing-changes/checklists/refactoring.md",".claude/skills/reviewing-changes/checklists/ui-refinement.md",".claude/skills/reviewing-changes/examples/review-outputs.md",".claude/skills/reviewing-changes/reference/architectural-patterns.md",".claude/skills/reviewing-changes/reference/priority-framework.md",".claude/skills/reviewing-changes/reference/review-psychology.md",".claude/skills/reviewing-changes/reference/security-patterns.md",".claude/skills/reviewing-changes/reference/style-patterns.md",".claude/skills/reviewing-changes/reference/testing-patterns.md",".claude/skills/reviewing-changes/reference/ui-patterns.md",".claude/skills/testing-android-code/SKILL.md",".claude/skills/testing-android-code/examples/compose-screen-test-example.md",".claude/skills/testing-android-code/examples/repository-test-example.md",".claude/skills/testing-android-code/examples/viewmodel-test-example.md",".claude/skills/testing-android-code/references/critical-gotchas.md",".claude/skills/testing-android-code/references/flow-testing-patterns.md",".claude/skills/testing-android-code/references/test-base-classes.md","docs/ARCHITECTURE.md",".cursorrules (android-jetpack-compose-cursorrules-prompt-file.mdc)"],"skills":{".claude/CLAUDE.md":"# Bitwarden Android - Claude Code Configuration\n\nOfficial Android application for Bitwarden Password Manager and Bitwarden Authenticator, providing secure password management, two-factor authentication, and credential autofill services with zero-knowledge encryption.\n\n## Overview\n\n- Multi-module Android application: `:app` (Password Manager), `:authenticator` (2FA TOTP generator)\n- Zero-knowledge architecture: encryption/decryption happens client-side via Bitwarden SDK\n- Target users: End-users via Google Play Store and F-Droid\n\n### Key Concepts\n\n- **Zero-Knowledge Architecture**: Server never has access to unencrypted vault data or encryption keys\n- **Bitwarden SDK**: Rust-based cryptographic SDK handling all encryption/decryption operations\n- **DataState**: Wrapper for streaming data states (Loading, Loaded, Pending, Error, NoNetwork)\n- **Result Types**: Custom sealed classes for operation results (never throw exceptions from data layer)\n- **UDF (Unidirectional Data Flow)**: State flows down, actions flow up through ViewModels\n\n---\n\n## Architecture\n\n```\nUser Request (UI Action)\n         |\n    Screen (Compose)\n         |\n    ViewModel (State/Action/Event)\n         |\n    Repository (Business Logic)\n         |\n    +----+----+----+\n    |    |    |    |\n  Disk  Network  SDK\n   |      |      |\n Room  Retrofit  Bitwarden\n  DB    APIs     Rust SDK\n```\n\n### Key Principles\n\n1. **No Exceptions from Data Layer**: All suspending functions return `Result<T>` or custom sealed classes\n2. **State Hoisting to ViewModel**: All state that affects behavior must live in the ViewModel's state\n3. **Interface-Based DI**: All implementations use interface/`...Impl` pairs with Hilt injection\n4. **Encryption by Default**: All sensitive data encrypted via SDK before storage\n\n### Core Patterns\n\n- **BaseViewModel**: Enforces UDF with State/Action/Event pattern. See `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt`.\n- **Repository Result Pattern**: Type-safe error handling using custom sealed classes for discrete operations and `DataState<T>` wrapper for streaming data.\n- **Common Patterns**: Flow collection via `Internal` actions, error handling via `when` branches, `DataState` streaming with `.map { }` and `.stateIn()`.\n\n> For complete architecture patterns, code templates, and module organization, see `docs/ARCHITECTURE.md`.\n\n---\n\n## Development Guide\n\n### Workflow Skills\n\n> **Quick start**: Use the `bitwarden-tech-lead:bitwarden-tech-lead` agent (or `/plan-android-work <task>`) to refine\n> requirements and plan,\n> then the `bitwarden-software-engineer:bitwarden-software-engineer` agent (or `/work-on-android <task>`) for implementation,\n> then `/review-android <PR#>` to review the result.\n\n## Skills & Commands\n\n| Skill | Triggers |\n|-------|---------|\n| `build-test-verify` | \"build\", \"run tests\", \"lint\", \"format\", \"verify build\" |\n| `implementing-android-code` | \"implement\", \"write code\", \"add screen\", \"create feature\" |\n| `planning-android-implementation` | \"plan implementation\", \"architecture design\", \"phased task breakdown\" |\n| `refining-android-requirements` | \"refine requirements\", \"analyze ticket\", \"gap analysis\" |\n| `reviewing-changes` | \"review\", \"code review\", \"check PR\" |\n| `testing-android-code` | \"write tests\", \"add test coverage\", \"unit test\" |\n\n| Command | Usage |\n|---------|-------|\n| `/plan-android-work <task>` | Fetch ticket → refine requirements → design implementation approach |\n| `/work-on-android <task>` | Full workflow: implement → test → verify → preflight → commit → review → PR |\n| `/review-android <PR#>` | Full review workflow: PR context gathering → Android checklist → output |\n\n---\n\n## Security Rules\n\n**MANDATORY - These rules have no exceptions:**\n\n1. **Zero-Knowledge Architecture**: Never transmit unencrypted vault data or master passwords to the server. All encryption happens client-side via the Bitwarden SDK.\n2. **No Plaintext Key Storage**: Encryption keys must be stored using Android Keystore (biometric unlock) or encrypted with PIN/master password.\n3. **Sensitive Data Cleanup**: On logout, all sensitive data must be cleared from memory and storage via `UserLogoutManager.logout()`.\n4. **Input Validation**: Validate all user inputs before processing, especially URLs and credentials.\n5. **SDK Isolation**: Use scoped SDK sources (`ScopedVaultSdkSource`) to prevent cross-user crypto context leakage.\n\n---\n\n## Code Style & Standards\n\n- **Formatter**: Android Studio with `bitwarden-style.xml` | **Line Limit**: 100 chars | **Detekt**: Enabled\n- **Naming**: `camelCase` (vars/fns), `PascalCase` (classes), `SCREAMING_SNAKE_CASE` (constants), `...Impl` (implementations)\n- **KDoc**: Required for all public APIs\n- **String Resources**: Add new strings to `:ui` module (`ui/src/main/res/values/strings.xml`). Use typographic quotes/apostrophes (`\"` `\"` `'`) not escaped ASCII (`\\\"` `\\'`). Name each resource from its own text content in `snake_case` — not with generic suffixes (`_message`, `_title`). E.g., `one_or_more_email_addresses_are_incorrect`, not `invalid_email_addresses_message`.\n\n> For complete style rules (imports, formatting, documentation, Compose conventions), see `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n---\n\n## Anti-Patterns\n\nIn addition to the Key Principles above, follow these rules:\n\n### DO\n- Map async results to internal actions before updating state\n- Inject `Clock` for time-dependent operations\n- Return early to reduce nesting\n\n### DON'T\n- Update state directly inside coroutines (use internal actions)\n- Use `any` types or suppress null safety\n- Catch generic `Exception` (catch specific types)\n- Use `e.printStackTrace()` (use Timber logging)\n- Create new patterns when established ones exist\n- Skip KDoc for public APIs\n\n---\n\n## Quick Reference\n\n- **Code style**: Full rules: `docs/STYLE_AND_BEST_PRACTICES.md`\n- **Before writing code**: Use `implementing-android-code` skill for Bitwarden-specific patterns, gotchas, and templates\n- **Before writing tests**: Use `testing-android-code` skill for test patterns and templates\n- **Building/testing**: Use `build-test-verify` skill | App tests: `./gradlew app:testStandardDebugUnitTest`\n- **Before committing**: Use `bitwarden-delivery-tools:perform-preflight` skill, then `bitwarden-delivery-tools:committing-changes` skill for message format\n- **Code review**: Use `/review-android` for the full review workflow; `reviewing-changes` skill for checklist-only\n- **Creating PRs**: Use `bitwarden-delivery-tools:creating-pull-request` skill for PR workflow and templates\n- **Troubleshooting**: See `docs/TROUBLESHOOTING.md`\n- **Architecture**: `docs/ARCHITECTURE.md` | [Bitwarden SDK](https://github.com/bitwarden/sdk) | [Jetpack Compose](https://developer.android.com/jetpack/compose) | [Hilt DI](https://dagger.dev/hilt/)\n",".claude/commands/plan-android-work.md":"---\ndescription: Guided requirements refinement and implementation planning for Bitwarden Android\nargument-hint: <Jira ticket (PM-12345), Confluence URL, or free-text description>\n---\n\n# Android Planning Workflow\n\nYou are guiding the developer through requirements refinement and implementation planning for the Bitwarden Android project. The input to plan from is:\n\n**Input**: $ARGUMENTS\n\n## Prerequisites\n\n- **Jira/Confluence access**: Fetching tickets and wiki pages requires the `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin. If the plugin is not installed, Jira ticket IDs and Confluence URLs cannot be fetched automatically.\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** The user may skip phases that are not applicable. If starting from a partially completed plan, skip to the appropriate phase.\n\n### Phase 1: Ingest Requirements\n\nParse the input to detect and fetch all available sources:\n\n**Source Detection Rules:**\n- **Jira tickets** (patterns like `PM-\\d+`, `BWA-\\d+`, `EC-\\d+`): Fetch via `get_issue` and `get_issue_comments`. Also fetch linked issue summaries (parent epic, sub-tasks, blockers) for context.\n- **Confluence URLs** (containing `atlassian.net/wiki` or confluence page IDs): Extract page ID and fetch via `get_confluence_page`. If the page is a parent page, fetch child pages via `get_child_pages` and ask the user which are relevant.\n- **Free text**: Treat as direct requirements — no fetching needed.\n- **Multiple inputs**: All are first-class sources. Fetch each independently and consolidate.\n- **Tool unavailable**: If `get_issue`, `get_confluence_page`, or other Atlassian tools are not available, inform the user that the `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin is required and prompt them to install and configure it. In the meantime, ask the user to paste the relevant content directly. Treat pasted content as free-text input.\n\n**Present a structured summary:**\n1. Sources identified and fetched (with links)\n2. Raw requirements extracted from each source\n3. Initial scope assessment (small / medium / large)\n\n**Edge cases:**\n- Jira ticket with no description → flag as critical gap that Phase 2 must address\n- Multiple tickets → fetch all, consolidate, flag any contradictions\n- Ticket + free text → both treated as first-class; free text supplements ticket\n\n**Gate**: User confirms the summary is complete and may add additional sources or context before proceeding.\n\n### Phase 2: Refine Requirements\n\nInvoke the `refining-android-requirements` skill and use it to perform gap analysis on the raw requirements from Phase 1.\n\nThe skill will:\n1. Consolidate all sources into a working document\n2. Evaluate requirements against a structured rubric (functional, technical, security, UX, cross-cutting)\n3. Present categorized gaps as blocking or non-blocking questions\n4. After user answers, produce a structured specification with numbered IDs\n\n**Gate**: User approves the refined specification. They may request changes or provide additional answers.\n\n### Phase 3: Plan Implementation\n\nInvoke the `planning-android-implementation` skill and use it to design the implementation approach based on the refined spec from Phase 2.\n\nThe skill will:\n1. Classify the change type\n2. Explore the codebase for reference implementations and integration points\n3. Design the architecture with component relationships\n4. Produce a file inventory and phased implementation plan\n5. Assess risks and define verification criteria\n\n**Gate**: User reviews the implementation plan and may request changes to architecture, phasing, or scope.\n\n### Phase 4: Finalize & Save\n\nMerge the outputs from Phase 2 (specification) and Phase 3 (implementation plan) into a single design document using this template:\n\n```markdown\n# [Feature Name] — Design Document\n\n**Feature**: [concise description]\n**Date**: [current date]\n**Status**: Ready for Implementation\n**Jira**: [ticket ID if available]\n**Sources**: [list of all input sources with links]\n\n---\n\n## Requirements Specification\n\n[Full output from Phase 2 — the refined specification with numbered IDs]\n\n---\n\n## Implementation Plan\n\n[Full output from Phase 3 — architecture, file inventory, phases, risks]\n\n---\n\n## Executing This Plan\n\nTo implement this plan, run:\n\n    /work-on-android [ticket or feature reference]\n\nReference this design document during implementation for architecture decisions,\nfile locations, and phase ordering.\n```\n\n**Save the document:**\n- With ticket: `.claude/outputs/plans/PM-XXXXX-FEATURE-NAME-PLAN.md`\n- Without ticket: `.claude/outputs/plans/FEATURE-NAME-PLAN.md`\n- Feature name should be uppercase with hyphens (e.g., `BIOMETRIC-TIMEOUT-CONFIG-PLAN.md`)\n- Create the output directory if it does not exist\n\n**On completion**: Present the saved file path and remind the user they can execute the plan with `/work-on-android`.\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- When fetching from Jira/Confluence, summarize what was found rather than dumping raw content.\n- Questions in Phase 2 should be specific and actionable, not generic.\n- The implementation plan in Phase 3 should reference concrete files in the codebase, not abstract descriptions.\n",".claude/commands/review-android.md":"---\ndescription: Guided Android code review workflow through context gathering, Android-specific review, and output\nargument-hint: [PR# | PR URL | \"local\"]\n---\n\n# Android Code Review Workflow\n\nYou are guiding the developer through a comprehensive Android code review for the Bitwarden Android project.\n\n**Input**: $ARGUMENTS\n\n## Prerequisites\n\n- **Jira/Confluence access**: The `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin is required to fetch linked Jira tickets. If unavailable, skip ticket context.\n- **GitHub CLI**: Required for fetching PR metadata. Verify with `gh auth status`.\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** The user may skip phases that are not applicable.\n\n### Phase 1: Ingest\n\nParse the input to determine review context:\n\n**Source Detection Rules:**\n- **PR number** (`123`, `PR #123`, `https://github.com/.../pull/123`): Extract the numeric ID. Fetch PR metadata via `gh pr view <N> --json title,body,headRefName,baseRefName,author,files`. Fetch existing review threads to avoid duplicate comments via `gh api graphql` with `reviewThreads(first: 100)`.\n- **\"local\"** or no argument: Review current branch changes via `git diff main...HEAD` and `git log main...HEAD --oneline --no-merges`.\n- **No input**: Ask the user whether to review a PR (provide number/URL) or local branch changes.\n\n**Additional context:**\n- Detect Jira ticket references in PR title/body (patterns like `PM-\\d+`, `BWA-\\d+`). Fetch via `get_issue` if the MCP plugin is available.\n- Summarize what was fetched rather than dumping raw content.\n\n**Present a structured summary:**\n1. What is being reviewed (PR title/number, branch, or local changes description)\n2. Jira ticket context if found (summary and acceptance criteria)\n3. Files changed (count and modules affected)\n4. Existing review thread count (PR reviews only — avoids duplicate comments)\n\n**Gate**: User confirms the summary is complete before proceeding.\n\n### Phase 2: Review\n\nInvoke the `reviewing-changes` skill and use it to perform the Android-specific code review. Use the PR context from Phase 1 (change type, files affected, modules, Jira requirements) to inform the skill's change type detection and checklist selection.\n\nThe skill will:\n1. Detect the change type based on files and PR context from Phase 1\n2. Load the appropriate type-specific checklist\n3. Execute the multi-pass review strategy\n4. Consult reference materials as needed\n\n**Before advancing**: Share a summary of key findings (critical issues if any, overall assessment) and confirm the user is ready to output the review.\n\n### Phase 3: Output\n\nWrite the completed review to local files:\n\n- `review-summary.md` — Overall assessment (APPROVE / REQUEST CHANGES) plus critical issues list\n- `review-inline-comments.md` — All inline findings with `<details>` tags\n\nFollow the exact output format from `.claude/skills/reviewing-changes/examples/review-outputs.md`.\n\nFor PR reviews: offer to post the review to GitHub using `gh pr review <N> --comment -b \"$(cat review-summary.md)\"` for the summary. For inline comments, use the GitHub API or the `bitwarden-code-review` plugin if installed.\n\n**Before advancing**: Confirm the files were written successfully and ask if the user wants to post to GitHub (PR reviews only).\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- Never proceed to another phase without user confirmation.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- If starting from a partially completed review (e.g., review already written), skip to the appropriate phase.\n",".claude/commands/work-on-android.md":"---\ndescription: Guided Android development workflow through all lifecycle phases\nargument-hint: <task description, plan, or Jira ticket reference>\n---\n\n# Android Development Workflow\n\nYou are guiding the developer through a complete Android development lifecycle for the Bitwarden Android project. The task to work on is:\n\n**Task**: $ARGUMENTS\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** If a phase fails (tests fail, lint errors, etc.), loop on that phase until resolved before advancing. The user may skip phases that are not applicable.\n\n### Phase 1: Implement\n\nInvoke `Skill(implementing-android-code)` to guide your implementation of the task. Understand what needs to be done, explore the relevant code, and write the implementation.\n\n**Before advancing**: Summarize what was implemented and confirm the user is ready to move to testing.\n\n### Phase 2: Test\n\nInvoke `Skill(testing-android-code)` to write tests for the changes made in Phase 1. Follow the project's test patterns and conventions.\n\n**Before advancing**: Summarize what tests were written and confirm readiness for build verification.\n\n### Phase 3: Build & Verify\n\nInvoke `Skill(build-test-verify)` to run tests, lint, and detekt. Ensure everything passes.\n\n**If failures occur**: Fix the issues and re-run verification. Do not advance until all checks pass.\n\n**Before advancing**: Report build/test/lint results and confirm readiness for self-review.\n\n### Phase 4: Self-Review\n\nInvoke `Skill(bitwarden-delivery-tools:perform-preflight)` to perform a quality gate check on all changes. Address any issues found.\n\n**Before advancing**: Share the self-review results and confirm readiness to commit.\n\n### Phase 5: Commit\n\nInvoke `Skill(bitwarden-delivery-tools:committing-changes)` to stage and commit the changes with a properly formatted commit message.\n\n**Before advancing**: Confirm the commit was successful and ask if the user wants to proceed to review and PR creation, or stop here.\n\n### Phase 6: Review\n\n**Pre-requisites:**\n- `bitwarden-code-review` from the [Bitwarden Plugin Marketplace](https://github.com/bitwarden/ai-plugins) must be installed in order to perform this phase. If it is not installed prompt the user to install it, or skip the review phase.\n\nLaunch a subagent with the `/bitwarden-code-review:code-review-local` command to perform a **local** code review of the committed diff. Validate and address any issues found before proceeding.\n\n**Before advancing**: Share review findings and confirm readiness for PR creation.\n\n### Phase 7: Pull Request\n\nPrompt the user to invoke `Skill(bitwarden-delivery-tools:creating-pull-request)` to create the pull request with proper description and formatting. **Create as a draft PR by default** unless the user has explicitly requested a ready-for-review PR.\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- Never proceed to another phase without user confirmation.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- If starting from a partially completed task (e.g., code already written), skip to the appropriate phase.\n",".claude/skills/build-test-verify/SKILL.md":"---\nname: build-test-verify\nversion: 0.1.0\ndescription: Build, test, lint, and deploy commands for the Bitwarden Android project. Use when running tests, building APKs/AABs, running lint/detekt, deploying, using fastlane, or discovering codebase structure. Triggered by \"run tests\", \"build\", \"gradle\", \"lint\", \"detekt\", \"deploy\", \"fastlane\", \"assemble\", \"verify\", \"coverage\".\n---\n\n# Build, Test & Verify\n\n## Environment Setup\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `GITHUB_TOKEN` | Yes (CI) | GitHub Packages auth for SDK (`read:packages` scope) |\n| Build flavors | - | `standard` (Play Store), `fdroid` (no Google services) |\n| Build types | - | `debug`, `beta`, `release` |\n\nIf builds fail resolving the Bitwarden SDK, verify `GITHUB_TOKEN` in `user.properties` or environment and check connectivity to `maven.pkg.github.com`.\n\n---\n\n## Building\n\n```bash\n# Debug builds\n./gradlew app:assembleDebug\n./gradlew authenticator:assembleDebug\n\n# Release builds (requires signing keys)\n./gradlew app:assembleStandardRelease\n./gradlew app:bundleStandardRelease\n\n# F-Droid builds\n./gradlew app:assembleFdroidRelease\n```\n\n---\n\n## Running Tests\n\n**IMPORTANT**: The app module uses the `standard` flavor. Always use `testStandardDebugUnitTest`, NOT `testDebugUnitTest`.\n\n**IMPORTANT**: Always pipe test output through a filter that captures failures on the first run. Gradle suppresses detailed failure output by default, so use `2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30` to see pass/fail results and assertion details without needing a second run.\n\n```bash\n# App module tests (correct flavor!)\n./gradlew app:testStandardDebugUnitTest 2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30\n\n# Run specific test classes\n./gradlew app:testStandardDebugUnitTest --tests \"com.x8bit.bitwarden.SomeTest\" 2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30\n\n# Run all unit tests across all modules\n./gradlew test\n\n# Individual shared modules (no flavor needed)\n./gradlew :core:test\n./gradlew :data:test\n./gradlew :network:test\n./gradlew :ui:test\n\n# Authenticator module\n./gradlew authenticator:testStandardDebugUnitTest\n```\n\n### Reading Test Reports\n\nIf you need full failure details beyond what grep captures, check the HTML test report:\n\n```bash\n# After a test run, open the report at:\n# app/build/reports/tests/testStandardDebugUnitTest/index.html\n# Or read individual failure XML:\nfind app/build/test-results -name \"*.xml\" -exec grep -l \"failure\" {} \\;\n```\n\n### Test Structure\n\n```\napp/src/test/                    # App unit tests\napp/src/testFixtures/            # App test utilities\ncore/src/testFixtures/           # Core test utilities (FakeDispatcherManager)\ndata/src/testFixtures/           # Data test utilities (FakeSharedPreferences)\nnetwork/src/testFixtures/        # Network test utilities (BaseServiceTest)\nui/src/testFixtures/             # UI test utilities (BaseViewModelTest, BaseComposeTest)\n```\n\n### Test Quick Reference\n\n- **Dispatcher Control**: `FakeDispatcherManager` from `:core:testFixtures`\n- **MockK**: `mockk<T> { every { } returns }`, `coEvery { }` for suspend\n- **Flow Testing**: Turbine with `stateEventFlow()` helper from `BaseViewModelTest`\n- **Time Control**: Inject `Clock` for deterministic time testing\n\n---\n\n## Lint & Static Analysis\n\n**IMPORTANT**: Prefer running detekt on modified files only — a full project scan is slow and unnecessary during development. The project supports a `-Pprecommit=true` flag that limits detekt to staged files.\n\n**IMPORTANT**: Always pipe detekt output through a filter to capture errors on the first run. Detekt prints violation details to stderr/stdout but Gradle can obscure them. Use the grep pattern below to see violations immediately.\n\n```bash\n# Detekt on staged files only (preferred during development)\ngit add -u && ./gradlew -Pprecommit=true detekt 2>&1 | grep -E \"FAILED|BUILD|Line |Rule |Signature|detekt\" | head -40\n\n# Detekt on all files (full scan, use sparingly)\n./gradlew detekt 2>&1 | grep -E \"FAILED|BUILD|Line |Rule |Signature|detekt\" | head -40\n\n# Android Lint\n./gradlew lint\n\n# Full validation suite (detekt + lint + tests + coverage)\n./fastlane check\n```\n\n### How `-Pprecommit=true` Works\n\nThe root `build.gradle.kts` configures detekt tasks to use `git diff --name-only --cached` when this property is set, limiting analysis to staged files only. This is the same mechanism used by the project's pre-commit hook. Stage your changes with `git add` before running.\n\n---\n\n## Codebase Discovery\n\n```bash\n# Find existing Bitwarden UI components\nfind ui/src/main/kotlin/com/bitwarden/ui/platform/components/ -name \"Bitwarden*.kt\" | sort\n\n# Find all ViewModels\ngrep -rl \"BaseViewModel<\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Find all Navigation files with @Serializable routes\nfind app/src/main/kotlin/ -name \"*Navigation.kt\" | sort\n\n# Find all Hilt modules\nfind app/src/main/kotlin/ -name \"*Module.kt\" -path \"*/di/*\" | sort\n\n# Find all repository interfaces\nfind app/src/main/kotlin/ -name \"*Repository.kt\" -not -name \"*Impl.kt\" -path \"*/repository/*\" | sort\n\n# Find encrypted disk source examples\ngrep -rl \"EncryptedPreferences\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Find Clock injection usage\ngrep -rl \"private val clock: Clock\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Search existing strings before adding new ones\ngrep -n \"search_term\" ui/src/main/res/values/strings.xml\n```\n\n---\n\n## Deployment & Versioning\n\n**Version location**: `gradle/libs.versions.toml`\n```toml\nappVersionCode = \"1\"\nappVersionName = \"2025.11.1\"\n```\nPattern: `YEAR.MONTH.PATCH`\n\n**Publishing channels**:\n- **Play Store**: GitHub Actions workflow with signed AAB\n- **F-Droid**: Dedicated workflow with F-Droid signing keys\n- **Firebase App Distribution**: Beta testing\n",".claude/skills/evaluating-sdk-internal-updates/SKILL.md":"---\nname: evaluating-sdk-internal-updates\ndescription: Evaluates a bitwarden/android \"Update SDK to\" PR against the sdk-internal commit range for compile-time and runtime breaking changes, maps affected symbols to Android call sites, and applies clear in-scope fixes. Use when reviewing an SDK bump PR, a bitwardenSdk version change, or triaging sdk-internal breaking changes.\nallowed-tools:\n  - Bash(gh pr diff:*)\n  - Bash(git -C * log *)\n  - Bash(git -C * show *)\n  - Bash(git add:*)\n  - Bash(git commit:*)\n  - Bash(grep:*)\n  - Bash(./gradlew *)\n  - Read\n  - Grep\n  - Glob\n  - Edit\n  - Write\n  - Skill(implementing-android-code)\n  - Skill(bitwarden-delivery-tools:committing-changes)\n---\n\n# Evaluating sdk-internal Updates\n\n**Identify both compile-time and runtime breaks before fixing anything — fixing the first break found is not finishing.** Steps 3-7 always cover the entire commit range before step 8 starts, no matter how obvious or urgent an early compile-time break looks.\n\n## Identify\n\nBinding surface facts specific to this SDK: `#[uniffi::export]` / `derive(uniffi::...)` annotations are scattered across many crates, not just `crates/bitwarden-uniffi`; `crates/bitwarden-ffi` is unrelated (\"do not use\"). No `.udl` files. UniFFI emits one Kotlin package per crate (`com.bitwarden.core`, `com.bitwarden.vault`, `com.bitwarden.crypto`, etc.) — only the top-level `Client`/`AuthClient`/`GeneratorClients` actually live under `com.bitwarden.sdk`. Both `app` and `authenticator` depend on the SDK; neither is optional to search.\n\nA hunk that only touches a macro invocation (e.g. `state_bridge! { ... }`) doesn't show the binding surface — the expansion lives in the macro's definition, often in a different crate (e.g. `bitwarden-state-bridge-macro`). `#[uniffi::export(with_foreign)]` marks a callback interface: a trait Kotlin must implement, where adding a field/method is never additive-safe for the implementor.\n\n1. Locate the local `bitwarden/sdk-internal` clone (check sibling directories to this repo). If none exists, stop and tell the user it's a required prerequisite for this skill — do not clone it yourself.\n2. `gh pr diff <PR> -R bitwarden/android | grep bitwardenSdk` → old/new `bitwardenSdk` string. Everything after the second `-` is the git ref — a commit SHA or a branch name (`.dev` SDK builds use both); resolve a branch name as `origin/<branch>` in the clone.\n3. Attempt `./gradlew app:compileStandardDebugKotlin authenticator:compileDebugKotlin` at the current checkout before crawling sdk-internal. A failure confirms a compile-time break directly, with a more precise location than any git search — note it and continue to steps 4-7 for the full commit range; do not fix it yet. A clean build only rules out compile-time breaks, not runtime ones.\n4. `git -C <sdk-internal-path> log --oneline OLD..NEW -G'uniffi::export|derive\\(uniffi|#\\[uniffi' -- '*.rs'` → candidate binding-surface commits.\n5. Classify per hunk, not per commit — a commit with one additive headline change can still have a second, unrelated breaking hunk. If a hunk only touches a macro invocation, read the macro's definition before classifying. `git -C <sdk-internal-path> show <sha> -- '*.rs'`.\n6. For every distinct symbol/type touched (every hunk, not just the commit's headline change), grep the whole repo for the bare symbol name to find Android call sites — a fixed module list or a `com.bitwarden.sdk.<Symbol>` import-prefix check both miss real consumers.\n7. Report: compile-time breaks, runtime breaks, safe/no-call-site — each with commit, symbol, and call sites. Cite sdk-internal commits and PRs as `bitwarden/sdk-internal#<NNN>` or a full commit URL/SHA — never a bare `#<NNN>` copied from a commit subject line. A bare reference auto-links within whatever repo the report is posted to (e.g. `bitwarden/android`), tagging an unrelated issue or PR there.\n\n## Resolve\n\nResolve the findings from Step 7 by deciding on a fix (step 8), implementing and committing it directly (step 9), then verifying (step 10).\n\n8. Decide the fix for anything found, compile-time or runtime, whenever the correct behavior is clear and within scope. For a new required method, grep for the underlying concept, not the new method/type name (it won't exist yet) — no existing consumer means stub it: a `// no-op` comment or a null/default return that satisfies the compiler, not a behavioral decision. Never `TODO()` — it throws at runtime, which is a crash, not a stub. A sibling's structure (naming, placement, style) is a template; its behavior (storage, defaulting, error handling, side effects) is not evidence for yours. If unsure, report it instead of guessing, along with anything needing a product decision.\n9. Implement every fix from step 8 directly: invoke `Skill(implementing-android-code)` first if the fix isn't purely mechanical, then commit with `Skill(bitwarden-delivery-tools:committing-changes)`.\n10. Verify with the same compile task used in step 3.\n",".claude/skills/implementing-android-code/SKILL.md":"---\nname: implementing-android-code\nversion: 0.1.4\ndescription: This skill should be used when implementing Android code in Bitwarden. Covers critical patterns, gotchas, and anti-patterns unique to this codebase. Triggered by \"How do I implement a ViewModel?\", \"Create a new screen\", \"Add navigation\", \"Write a repository\", \"BaseViewModel pattern\", \"State-Action-Event\", \"type-safe navigation\", \"@Serializable route\", \"SavedStateHandle persistence\", \"process death recovery\", \"handleAction\", \"sendAction\", \"Hilt module\", \"Repository pattern\", \"implementing a screen\", \"adding a data source\", \"handling navigation\", \"encrypted storage\", \"security patterns\", \"Clock injection\", \"DataState\", or any questions about implementing features, screens, ViewModels, data sources, or navigation in the Bitwarden Android app.\n---\n\n# Implementing Android Code - Bitwarden Quick Reference\n\n**This skill provides tactical guidance for Bitwarden-specific patterns.** For comprehensive architecture decisions and complete code style rules, consult `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n---\n\n## Critical Patterns Reference\n\n### A. ViewModel Implementation (State-Action-Event Pattern)\n\nAll ViewModels follow the **State-Action-Event (SAE)** pattern via `BaseViewModel<State, Event, Action>`.\n\n**Key Requirements:**\n- Annotate with `@HiltViewModel`\n- State class MUST be `@Parcelize data class : Parcelable`\n- Implement `handleAction(action: A)` - MUST be synchronous\n- Post internal actions from coroutines using `sendAction()`\n- Save/restore state via `SavedStateHandle[KEY_STATE]`\n- Private action handlers: `private fun handle*` naming convention\n\n**Template**: See [ViewModel template](templates.md#viewmodel-template-state-action-event-pattern)\n\n**Pattern Summary:**\n```kotlin\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val repository: ExampleRepository,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    initialState = savedStateHandle[KEY_STATE] ?: ExampleState(),\n) {\n    init {\n        stateFlow.onEach { savedStateHandle[KEY_STATE] = it }.launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        // Synchronous dispatch only\n        when (action) {\n            is Action.Click -> handleClick()\n            is Action.Internal.DataReceived -> handleDataReceived(action)\n        }\n    }\n\n    private fun handleClick() {\n        viewModelScope.launch {\n            val result = repository.fetchData()\n            sendAction(Action.Internal.DataReceived(result))  // Post internal action\n        }\n    }\n\n    private fun handleDataReceived(action: Action.Internal.DataReceived) {\n        mutableStateFlow.update { it.copy(data = action.result) }\n    }\n}\n```\n\n**Reference:**\n- `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)\n- `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt` (see class declaration)\n\n**Critical Gotchas:**\n- ❌ **NEVER** update `mutableStateFlow` directly inside coroutines\n- ✅ **ALWAYS** post internal actions from coroutines, update state in `handleAction()`\n- ❌ **NEVER** forget `@IgnoredOnParcel` for sensitive data (causes security leak)\n- ✅ **ALWAYS** use `@Parcelize` on state classes for process death recovery\n- ✅ State restoration happens automatically if properly saved to `SavedStateHandle`\n\n---\n\n### B. Navigation Implementation (Type-Safe)\n\nAll navigation uses **type-safe routes** with kotlinx.serialization.\n\n**Pattern Structure:**\n1. `@Serializable` route data class with parameters\n2. `...Args` helper class for extracting from `SavedStateHandle`\n3. `NavGraphBuilder.{screen}Destination()` extension for adding screen to graph\n4. `NavController.navigateTo{Screen}()` extension for navigation calls\n\n**Template**: See [Navigation template](templates.md#navigation-template-type-safe-routes)\n\n**Pattern Summary:**\n```kotlin\n@Serializable\ndata class ExampleRoute(val userId: String, val isEditMode: Boolean = false)\n\ndata class ExampleArgs(val userId: String, val isEditMode: Boolean)\n\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(userId = route.userId, isEditMode = route.isEditMode)\n}\n\nfun NavController.navigateToExample(\n    userId: String,\n    isEditMode: Boolean = false,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(route = ExampleRoute(userId, isEditMode), navOptions = navOptions)\n}\n\nfun NavGraphBuilder.exampleDestination(onNavigateBack: () -> Unit) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(onNavigateBack = onNavigateBack)\n    }\n}\n```\n\n**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt` (see `LoginRoute` and extensions)\n\n**Key Benefits:**\n- ✅ Type safety: Compile-time errors for missing parameters\n- ✅ No string literals in navigation code\n- ✅ Automatic serialization/deserialization\n- ✅ Clear contract for screen dependencies\n\n---\n\n### C. Screen/Compose Implementation\n\nAll screens follow consistent Compose patterns.\n\n**Template**: See [Screen/Compose template](templates.md#screencompose-template)\n\n**Key Patterns:**\n```kotlin\n@Composable\nfun ExampleScreen(\n    onNavigateBack: () -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            ExampleEvent.NavigateBack -> onNavigateBack()\n        }\n    }\n\n    BitwardenScaffold(\n        topBar = {\n            BitwardenTopAppBar(\n                title = stringResource(R.string.title),\n                navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_back),\n                onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },\n            )\n        },\n    ) {\n        // UI content\n    }\n}\n```\n\n**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt` (see `LoginScreen` composable)\n\n**Essential Requirements:**\n- ✅ Use `hiltViewModel()` for dependency injection\n- ✅ Use `collectAsStateWithLifecycle()` for state (not `collectAsState()`)\n- ✅ Use `EventsEffect(viewModel)` for one-shot events\n- ✅ Use `Bitwarden*` prefixed components from `:ui` module\n\n**State Hoisting Rules:**\n- **ViewModel state**: Data that needs to survive process death or affects business logic\n- **UI-only state**: Temporary UI state (scroll position, text field focus) using `remember` or `rememberSaveable`\n\n---\n\n### D. Data Layer Implementation\n\nThe data layer follows strict patterns for repositories, managers, and data sources.\n\n**Interface + Implementation Separation (ALWAYS)**\n\n**Template**: See [Data Layer template](templates.md#data-layer-template-repository--hilt-module)\n\n**Pattern Summary:**\n```kotlin\n// Interface (injected via Hilt)\ninterface ExampleRepository {\n    suspend fun fetchData(id: String): ExampleResult\n    val dataFlow: StateFlow<DataState<ExampleData>>\n}\n\n// Implementation (NOT directly injected)\nclass ExampleRepositoryImpl(\n    private val exampleDiskSource: ExampleDiskSource,\n    private val exampleService: ExampleService,\n) : ExampleRepository {\n    override suspend fun fetchData(id: String): ExampleResult {\n        // NO exceptions thrown - return Result or sealed class\n        return exampleService.getData(id).fold(\n            onSuccess = { ExampleResult.Success(it.toModel()) },\n            onFailure = { ExampleResult.Error(it.message) },\n        )\n    }\n}\n\n// Sealed result class (domain-specific)\nsealed class ExampleResult {\n    data class Success(val data: ExampleData) : ExampleResult()\n    data class Error(val message: String?) : ExampleResult()\n}\n\n// Hilt Module\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleRepositoryModule {\n    @Provides\n    @Singleton\n    fun provideExampleRepository(\n        exampleDiskSource: ExampleDiskSource,\n        exampleService: ExampleService,\n    ): ExampleRepository = ExampleRepositoryImpl(exampleDiskSource, exampleService)\n}\n```\n\n**Reference:**\n- `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt`\n- `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`\n\n**Three-Layer Data Architecture:**\n1. **Data Sources** - Raw data access (network, disk, SDK). Return `Result<T>`, never throw.\n2. **Managers** - Single responsibility business logic. Wrap OS/external services.\n3. **Repositories** - Aggregate sources/managers. Return domain-specific sealed classes.\n\n**Critical Rules:**\n- ❌ **NEVER** throw exceptions in data layer\n- ✅ **ALWAYS** use interface + `...Impl` pattern\n- ✅ **ALWAYS** inject interfaces, never implementations\n- ✅ Data sources return `Result<T>`, repositories return domain sealed classes\n- ✅ Use `StateFlow` for continuously observed data\n\n**KDoc on Interfaces vs. Implementations**\n\nKDoc on an interface member describes the **contract** — what the caller can rely on. It must not describe **how** the implementation fulfills that contract. Implementation details (which data source is consulted, what is cached, what is logged, which manager is delegated to, ordering of internal calls, etc.) belong on the `...Impl` member — and only when the *why* is non-obvious from the code itself.\n\nThis applies to every interface in the codebase: repositories, managers, data sources, validators, and UI-layer interfaces alike. The rule is the same one CLAUDE.md states for comments in general — describe the contract or the non-obvious *why*, never the *what* a well-named identifier already conveys.\n\n❌ **Wrong** — interface KDoc leaks the implementation:\n```kotlin\ninterface ExampleRepository {\n    /**\n     * Fetches data by reading from [ExampleDiskSource] first, falling back to\n     * [ExampleService] on cache miss and writing the network result back to disk.\n     */\n    suspend fun fetchData(id: String): ExampleResult\n}\n```\n\n✅ **Right** — interface KDoc describes the contract; implementation details (if needed at all) live on the override:\n```kotlin\ninterface ExampleRepository {\n    /** Returns the [ExampleData] for [id], or an error result on failure. */\n    suspend fun fetchData(id: String): ExampleResult\n}\n\nclass ExampleRepositoryImpl(...) : ExampleRepository {\n    // No KDoc needed — the disk-then-network pattern is visible in the code.\n    override suspend fun fetchData(id: String): ExampleResult { ... }\n}\n```\n\nRed flags that an interface KDoc has drifted into implementation territory:\n- Names a concrete collaborator (`...DiskSource`, `...Service`, `...Manager`, `...Impl`)\n- Describes ordering of internal calls (\"first ... then ...\", \"falls back to ...\")\n- Mentions caching, retries, logging, or threading behavior that callers don't depend on\n- Restates the method signature in prose (\"Suspends until X returns Y\")\n\nIf callers genuinely depend on a behavior (e.g., \"this method is safe to call before vault unlock\", \"result is cached for the session\"), that *is* part of the contract and belongs on the interface. The test: would a different valid implementation be free to change this? If yes, it's implementation detail — strip it.\n\n---\n\n### E. UI Components\n\n**Use Existing Components First:**\n\nThe `:ui` module provides reusable `Bitwarden*` prefixed components. Search before creating new ones.\n\n**Common Components:**\n- `BitwardenFilledButton` - Primary action buttons\n- `BitwardenOutlinedButton` - Secondary action buttons\n- `BitwardenTextField` - Text input fields\n- `BitwardenPasswordField` - Password input with show/hide\n- `BitwardenSwitch` - Toggle switches\n- `BitwardenTopAppBar` - Toolbar/app bar\n- `BitwardenScaffold` - Screen container with scaffold\n- `BitwardenBasicDialog` - Simple dialogs\n- `BitwardenLoadingDialog` - Loading indicators\n\n**Component Discovery:**\nSearch `ui/src/main/kotlin/com/bitwarden/ui/platform/components/` for existing `Bitwarden*` components. For build, test, and codebase discovery commands, use the **`build-test-verify`** skill.\n\n**When to Create New Reusable Components:**\n- Component used in 3+ places\n- Component needs consistent theming across app\n- Component has semantic meaning (accessibility)\n- Component has complex state management\n\n**New Component Requirements:**\n- Prefix with `Bitwarden`\n- Accept themed colors/styles from `BitwardenTheme`\n- Include preview composables for testing\n- Support accessibility (content descriptions, semantics)\n\n**String Resources:**\n\nNew strings belong in the `:ui` module: `ui/src/main/res/values/strings.xml`\n\n- Use typographic apostrophes and quotes to avoid escape characters: `you’ll` not `you\\'ll`, `“word”` not `\\\"word\\\"`\n- Reference strings via generated `BitwardenString` resource IDs\n- Do not add strings to other modules unless explicitly instructed\n\n---\n\n### F. Security Patterns\n\n**Encrypted vs Unencrypted Storage:**\n\n**Template**: See [Security templates](templates.md#security-templates)\n\n**Pattern Summary:**\n```kotlin\nclass ExampleDiskSourceImpl(\n    @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,\n    @UnencryptedPreferences sharedPreferences: SharedPreferences,\n) : BaseEncryptedDiskSource(\n    encryptedSharedPreferences = encryptedSharedPreferences,\n    sharedPreferences = sharedPreferences,\n),\n    ExampleDiskSource {\n    fun storeAuthToken(token: String) {\n        putEncryptedString(KEY_TOKEN, token)  // Sensitive — uses base class method\n    }\n\n    fun storeThemePreference(isDark: Boolean) {\n        putBoolean(KEY_THEME, isDark)  // Non-sensitive — uses base class method\n    }\n}\n```\n\n**Android Keystore (Biometric Keys):**\n- User-scoped encryption keys: `BiometricsEncryptionManager`\n- Keys stored in Android Keystore (hardware-backed when available)\n- Integrity validation on biometric state changes\n\n**Input Validation:**\n```kotlin\n// Validation returns boolean, NEVER throws\ninterface RequestValidator {\n    fun validate(request: Request): Boolean\n}\n\n// Sanitization removes dangerous content\nfun String?.sanitizeTotpUri(issuer: String?, username: String?): String? {\n    if (this.isNullOrBlank()) return null\n    // Sanitize and return safe value\n}\n```\n\n**Security Checklist:**\n- ✅ Use `@EncryptedPreferences` for credentials, keys, tokens\n- ✅ Use `@UnencryptedPreferences` for UI state, preferences\n- ✅ Use `@IgnoredOnParcel` for sensitive ViewModel state\n- ❌ **NEVER** log sensitive data (passwords, tokens, vault items)\n- ✅ Validate all user input before processing\n- ✅ Use Timber for non-sensitive logging only\n\n---\n\n### G. Testing Patterns\n\n**ViewModel Testing:**\n\n**Template**: See [Testing templates](templates.md#testing-templates)\n\n**Pattern Summary:**\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n    private val mockRepository: ExampleRepository = mockk()\n\n    @Test\n    fun `ButtonClick should fetch data and update state`() = runTest {\n        val expectedResult = ExampleResult.Success(data = \"test\")\n        coEvery { mockRepository.fetchData(any()) } returns expectedResult\n\n        val viewModel = createViewModel()\n        viewModel.trySendAction(ExampleAction.ButtonClick)\n\n        viewModel.stateFlow.test {\n            assertEquals(EXPECTED_STATE.copy(data = \"test\"), awaitItem())\n        }\n    }\n\n    private fun createViewModel(): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to EXPECTED_STATE)),\n        repository = mockRepository,\n    )\n}\n```\n\n**Reference:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n**Key Testing Patterns:**\n- ✅ Extend `BaseViewModelTest` for proper dispatcher management\n- ✅ Use `runTest` from `kotlinx.coroutines.test`\n- ✅ Use Turbine's `.test { awaitItem() }` for Flow assertions\n- ✅ Use MockK: `coEvery` for suspend functions, `every` for sync\n- ✅ Test both state changes and event emissions\n- ✅ Test both success and failure Result paths\n\n**Flow Testing with Turbine:**\n```kotlin\n// Test state and events simultaneously\nviewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n    viewModel.trySendAction(ExampleAction.Submit)\n    assertEquals(ExpectedState.Loading, stateFlow.awaitItem())\n    assertEquals(ExampleEvent.ShowSuccess, eventFlow.awaitItem())\n}\n```\n\n**MockK Quick Reference:**\n```kotlin\ncoEvery { repository.fetchData(any()) } returns Result.success(\"data\")  // Suspend\nevery { diskSource.getData() } returns \"cached\"  // Sync\ncoVerify { repository.fetchData(\"123\") }  // Verify\n```\n\n---\n\n### H. Clock/Time Handling\n\nAll code needing current time must inject `Clock` for testability.\n\n**Key Requirements:**\n- ✅ Inject `Clock` via Hilt in ViewModels\n- ✅ Pass `Clock` as parameter in extension functions\n- ✅ Use `clock.instant()` to get current time\n- ❌ Never call `Instant.now()` or `DateTime.now()` directly\n- ❌ Never use `mockkStatic` for datetime classes in tests\n\n**Pattern Summary:**\n```kotlin\n// ViewModel with Clock\nclass MyViewModel @Inject constructor(\n    private val clock: Clock,\n) {\n    val timestamp = clock.instant()\n}\n\n// Extension function with Clock parameter\nfun State.getTimestamp(clock: Clock): Instant =\n    existingTime ?: clock.instant()\n\n// Test with fixed clock\nval FIXED_CLOCK = Clock.fixed(\n    Instant.parse(\"2023-10-27T12:00:00Z\"),\n    ZoneOffset.UTC,\n)\n```\n\n**Reference:**\n- `docs/STYLE_AND_BEST_PRACTICES.md` (see Time and Clock Handling section)\n- `core/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt` (see `provideClock` function)\n\n**Critical Gotchas:**\n- ❌ `Instant.now()` creates hidden dependency, non-testable\n- ❌ `mockkStatic(Instant::class)` is fragile, can leak between tests\n- ✅ `Clock.fixed(...)` provides deterministic test behavior\n\n---\n\n### I. Kotlin Style Rules\n\nProject-specific style conventions enforced in code review. These supplement (not replace) `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n**`when` branches with wrapped right-hand side require curly braces.**\n\nWhen a `when` branch's expression is too long to fit on the same line as the arrow and is wrapped to the next line, wrap the body in `{ }`. A bare `->` followed by an indented expression on its own line is rejected in review.\n\n❌ **Wrong** — wrapped body without braces:\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT ->\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    VaultItemCipherType.DRIVERS_LICENSE ->\n        VaultAddEditState.ViewState.Content.ItemType.DriversLicense()\n}\n```\n\n✅ **Right** — wrapped body with braces:\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT -> {\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    }\n    VaultItemCipherType.DRIVERS_LICENSE -> {\n        VaultAddEditState.ViewState.Content.ItemType.DriversLicense()\n    }\n}\n```\n\nSingle-line branches (body fits on the same line as `->`) do **not** need braces.\n\n---\n\n## Bitwarden-Specific Anti-Patterns\n\n**General anti-patterns are documented in CLAUDE.md.** This section covers violations specific to Bitwarden's State-Action-Event, navigation, and data layer patterns:\n\n❌ **NEVER update ViewModel state directly in coroutines**\n- Post internal actions, update state synchronously in `handleAction()`\n\n❌ **NEVER inject `...Impl` classes**\n- Only inject interfaces via Hilt\n\n❌ **NEVER create navigation without `@Serializable` routes**\n- No string-based navigation, always type-safe\n\n❌ **NEVER use raw `Result<T>` in repositories**\n- Use domain-specific sealed classes for better error handling\n\n❌ **NEVER make state classes without `@Parcelize`**\n- All ViewModel state must survive process death\n\n❌ **NEVER skip `SavedStateHandle` persistence for ViewModels**\n- Users lose form progress on process death\n\n❌ **NEVER forget `@IgnoredOnParcel` for passwords/tokens**\n- Causes security vulnerability (sensitive data in parcel)\n\n❌ **NEVER use generic `Exception` catching**\n- Catch specific exceptions only (`RemoteException`, `IOException`)\n\n❌ **NEVER call `Instant.now()` or `DateTime.now()` directly**\n- Inject `Clock` via Hilt, use `clock.instant()` for testability\n\n❌ **NEVER put implementation details in interface KDoc**\n- Interface KDoc describes the contract; concrete collaborators, call ordering, caching, and fallback behavior belong on the `...Impl` override (and only when the *why* is non-obvious). See section D for examples.\n\n---\n\n## Quick Reference\n\nFor build, test, and codebase discovery commands, use the **`build-test-verify`** skill.\n\n**File Reference Format:**\nWhen pointing to specific code, use: `file_path:line_number`\n\nExample: `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)\n",".claude/skills/implementing-android-code/templates.md":"# Code Templates - Bitwarden Android\n\nCopy-pasteable templates derived from actual codebase patterns. Replace `Example` with your feature name.\n\n---\n\n## ViewModel Template (State-Action-Event Pattern)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt`\n\n### State Class\n\n```kotlin\n@Parcelize\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    @IgnoredOnParcel val sensitiveInput: String = \"\", // Sensitive data excluded from parcel\n    val dialogState: DialogState? = null,\n) : Parcelable {\n\n    /**\n     * Dialog states for the Example screen.\n     */\n    sealed class DialogState : Parcelable {\n        @Parcelize\n        data class Error(\n            val title: Text? = null,\n            val message: Text,\n            val error: Throwable? = null,\n        ) : DialogState()\n\n        @Parcelize\n        data class Loading(val message: Text) : DialogState()\n    }\n}\n```\n\n### Event Sealed Class\n\n```kotlin\n/**\n * One-shot UI events for the Example screen.\n */\nsealed class ExampleEvent {\n    data object NavigateBack : ExampleEvent()\n\n    data class ShowToast(val message: Text) : ExampleEvent()\n}\n```\n\n### Action Sealed Class (with Internal)\n\n```kotlin\n/**\n * User and system actions for the Example screen.\n */\nsealed class ExampleAction {\n    data object BackClick : ExampleAction()\n\n    data object SubmitClick : ExampleAction()\n\n    data class InputChanged(val input: String) : ExampleAction()\n\n    data object ErrorDialogDismiss : ExampleAction()\n\n    /**\n     * Internal actions dispatched by the ViewModel from coroutines.\n     */\n    sealed class Internal : ExampleAction() {\n        data class ReceiveDataState(\n            val dataState: DataState<ExampleData>,\n        ) : Internal()\n\n        data class ReceiveDataResult(\n            val result: ExampleResult,\n        ) : Internal()\n    }\n}\n```\n\n### ViewModel\n\n```kotlin\nprivate const val KEY_STATE = \"state\"\n\n/**\n * ViewModel for the Example screen.\n */\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val exampleRepository: ExampleRepository,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    initialState = savedStateHandle[KEY_STATE]\n        ?: run {\n            val args = savedStateHandle.toExampleArgs()\n            ExampleState(\n                data = args.itemId,\n            )\n        },\n) {\n\n    init {\n        // Persist state for process death recovery\n        stateFlow\n            .onEach { savedStateHandle[KEY_STATE] = it }\n            .launchIn(viewModelScope)\n\n        // Collect repository flows as internal actions\n        exampleRepository.dataFlow\n            .map { ExampleAction.Internal.ReceiveDataState(it) }\n            .onEach(::sendAction)\n            .launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        when (action) {\n            ExampleAction.BackClick -> handleBackClick()\n            ExampleAction.SubmitClick -> handleSubmitClick()\n            ExampleAction.ErrorDialogDismiss -> handleErrorDialogDismiss()\n            is ExampleAction.InputChanged -> handleInputChanged(action)\n            is ExampleAction.Internal.ReceiveDataState -> {\n                handleReceiveDataState(action)\n            }\n            is ExampleAction.Internal.ReceiveDataResult -> {\n                handleReceiveDataResult(action)\n            }\n        }\n    }\n\n    private fun handleBackClick() {\n        sendEvent(ExampleEvent.NavigateBack)\n    }\n\n    private fun handleErrorDialogDismiss() {\n        mutableStateFlow.update { it.copy(dialogState = null) }\n    }\n\n    private fun handleSubmitClick() {\n        viewModelScope.launch {\n            val result = exampleRepository.submitData(state.data.orEmpty())\n            sendAction(ExampleAction.Internal.ReceiveDataResult(result))\n        }\n    }\n\n    private fun handleInputChanged(action: ExampleAction.InputChanged) {\n        mutableStateFlow.update { it.copy(sensitiveInput = action.input) }\n    }\n\n    private fun handleReceiveDataState(\n        action: ExampleAction.Internal.ReceiveDataState,\n    ) {\n        when (action.dataState) {\n            is DataState.Loaded -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        data = action.dataState.data.toString(),\n                    )\n                }\n            }\n\n            is DataState.Loading -> {\n                mutableStateFlow.update { it.copy(isLoading = true) }\n            }\n\n            is DataState.Error -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        dialogState = ExampleState.DialogState.Error(\n                            message = BitwardenString.generic_error_message.asText(),\n                            error = action.dataState.error,\n                        ),\n                    )\n                }\n            }\n\n            else -> Unit\n        }\n    }\n\n    private fun handleReceiveDataResult(\n        action: ExampleAction.Internal.ReceiveDataResult,\n    ) {\n        when (val result = action.result) {\n            is ExampleResult.Success -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        data = result.data,\n                    )\n                }\n            }\n\n            is ExampleResult.Error -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        dialogState = ExampleState.DialogState.Error(\n                            message = result.message?.asText()\n                                ?: BitwardenString.generic_error_message.asText(),\n                        ),\n                    )\n                }\n            }\n        }\n    }\n}\n```\n\n---\n\n## Navigation Template (Type-Safe Routes)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt`\n\n```kotlin\n@file:OmitFromCoverage\n\npackage com.x8bit.bitwarden.ui.feature.example\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.navigation.NavController\nimport androidx.navigation.NavGraphBuilder\nimport androidx.navigation.NavOptions\nimport androidx.navigation.toRoute\nimport com.bitwarden.annotation.OmitFromCoverage\nimport com.bitwarden.ui.platform.base.util.composableWithSlideTransitions\nimport kotlinx.serialization.Serializable\n\n/**\n * Route for the Example screen.\n */\n@Serializable\n@OmitFromCoverage\ndata class ExampleRoute(\n    val itemId: String,\n    val isEditMode: Boolean = false,\n)\n\n/**\n * Args extracted from [SavedStateHandle] for the Example screen.\n */\n@OmitFromCoverage\ndata class ExampleArgs(\n    val itemId: String,\n    val isEditMode: Boolean,\n)\n\n/**\n * Extracts [ExampleArgs] from the [SavedStateHandle].\n */\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(\n        itemId = route.itemId,\n        isEditMode = route.isEditMode,\n    )\n}\n\n/**\n * Navigate to the Example screen.\n */\nfun NavController.navigateToExample(\n    itemId: String,\n    isEditMode: Boolean = false,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(\n        route = ExampleRoute(\n            itemId = itemId,\n            isEditMode = isEditMode,\n        ),\n        navOptions = navOptions,\n    )\n}\n\n/**\n * Add the Example screen destination to the navigation graph.\n */\nfun NavGraphBuilder.exampleDestination(\n    onNavigateBack: () -> Unit,\n) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(\n            onNavigateBack = onNavigateBack,\n        )\n    }\n}\n```\n\n---\n\n## Screen/Compose Template\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt`\n\n```kotlin\npackage com.x8bit.bitwarden.ui.feature.example\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.bitwarden.ui.platform.base.util.EventsEffect\nimport com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar\nimport com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold\nimport com.bitwarden.ui.platform.components.util.rememberVectorPainter\nimport com.bitwarden.ui.platform.resource.BitwardenDrawable\nimport com.bitwarden.ui.platform.resource.BitwardenString\n\n/**\n * The Example screen.\n */\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ExampleScreen(\n    onNavigateBack: () -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            ExampleEvent.NavigateBack -> onNavigateBack()\n            is ExampleEvent.ShowToast -> {\n                // Handle toast\n            }\n        }\n    }\n\n    // Dialogs\n    ExampleDialogs(\n        dialogState = state.dialogState,\n        onDismissRequest = { viewModel.trySendAction(ExampleAction.ErrorDialogDismiss) },\n    )\n\n    val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())\n    BitwardenScaffold(\n        modifier = Modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            BitwardenTopAppBar(\n                title = stringResource(id = BitwardenString.example),\n                scrollBehavior = scrollBehavior,\n                navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),\n                onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },\n            )\n        },\n    ) {\n        ExampleScreenContent(\n            state = state,\n            onInputChanged = { viewModel.trySendAction(ExampleAction.InputChanged(it)) },\n            onSubmitClick = { viewModel.trySendAction(ExampleAction.SubmitClick) },\n            modifier = Modifier\n                .fillMaxSize(),\n        )\n    }\n}\n```\n\n---\n\n## Data Layer Template (Repository + Hilt Module)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`\n\n### Interface\n\n```kotlin\n/**\n * Provides data operations for the Example feature.\n */\ninterface ExampleRepository {\n    /**\n     * Submits data and returns a typed result.\n     */\n    suspend fun submitData(input: String): ExampleResult\n\n    /**\n     * Continuously observed data stream.\n     */\n    val dataFlow: StateFlow<DataState<ExampleData>>\n}\n```\n\n### Sealed Result Class\n\n```kotlin\n/**\n * Domain-specific result for Example operations.\n */\nsealed class ExampleResult {\n    data class Success(val data: String) : ExampleResult()\n    data class Error(val message: String?) : ExampleResult()\n}\n```\n\n### Implementation\n\n```kotlin\n/**\n * Default implementation of [ExampleRepository].\n */\nclass ExampleRepositoryImpl(\n    private val exampleDiskSource: ExampleDiskSource,\n    private val exampleService: ExampleService,\n    private val dispatcherManager: DispatcherManager,\n) : ExampleRepository {\n\n    override val dataFlow: StateFlow<DataState<ExampleData>>\n        get() = // ...\n\n    override suspend fun submitData(input: String): ExampleResult {\n        return exampleService\n            .postData(input)\n            .fold(\n                onSuccess = { ExampleResult.Success(it.toModel()) },\n                onFailure = { ExampleResult.Error(it.message) },\n            )\n    }\n}\n```\n\n### Hilt Module\n\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleRepositoryModule {\n\n    @Provides\n    @Singleton\n    fun provideExampleRepository(\n        exampleDiskSource: ExampleDiskSource,\n        exampleService: ExampleService,\n        dispatcherManager: DispatcherManager,\n    ): ExampleRepository = ExampleRepositoryImpl(\n        exampleDiskSource = exampleDiskSource,\n        exampleService = exampleService,\n        dispatcherManager = dispatcherManager,\n    )\n}\n```\n\n---\n\n## Security Templates\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt` and `AuthDiskSourceImpl.kt`\n\n### Encrypted Disk Source (Module)\n\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleDiskModule {\n\n    @Provides\n    @Singleton\n    fun provideExampleDiskSource(\n        @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,\n        @UnencryptedPreferences sharedPreferences: SharedPreferences,\n        json: Json,\n    ): ExampleDiskSource = ExampleDiskSourceImpl(\n        encryptedSharedPreferences = encryptedSharedPreferences,\n        sharedPreferences = sharedPreferences,\n        json = json,\n    )\n}\n```\n\n### Encrypted Disk Source (Implementation)\n\n```kotlin\n/**\n * Disk source for Example data using encrypted and unencrypted storage.\n */\nclass ExampleDiskSourceImpl(\n    encryptedSharedPreferences: SharedPreferences,\n    sharedPreferences: SharedPreferences,\n    private val json: Json,\n) : BaseEncryptedDiskSource(\n    encryptedSharedPreferences = encryptedSharedPreferences,\n    sharedPreferences = sharedPreferences,\n),\n    ExampleDiskSource {\n\n    private companion object {\n        const val ENCRYPTED_TOKEN_KEY = \"exampleToken\"\n        const val UNENCRYPTED_PREF_KEY = \"examplePreference\"\n    }\n\n    override var authToken: String?\n        get() = getEncryptedString(ENCRYPTED_TOKEN_KEY)\n        set(value) { putEncryptedString(ENCRYPTED_TOKEN_KEY, value) }\n\n    override var uiPreference: Boolean\n        get() = getBoolean(UNENCRYPTED_PREF_KEY) ?: false\n        set(value) { putBoolean(UNENCRYPTED_PREF_KEY, value) }\n}\n```\n\n---\n\n## Testing Templates\n\n**Based on**: `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n### ViewModel Test\n\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n\n    // Mock dependencies\n    private val mockRepository = mockk<ExampleRepository>()\n    private val mutableDataFlow = MutableStateFlow<DataState<ExampleData>>(DataState.Loading)\n\n    @BeforeEach\n    fun setup() {\n        every { mockRepository.dataFlow } returns mutableDataFlow\n    }\n\n    @Test\n    fun `initial state should be correct when there is no saved state`() {\n        val viewModel = createViewModel(state = null)\n        assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)\n    }\n\n    @Test\n    fun `initial state should be correct when there is a saved state`() {\n        val savedState = DEFAULT_STATE.copy(data = \"saved\")\n        val viewModel = createViewModel(state = savedState)\n        assertEquals(savedState, viewModel.stateFlow.value)\n    }\n\n    @Test\n    fun `SubmitClick should call repository and update state on success`() = runTest {\n        val expected = ExampleResult.Success(data = \"result\")\n        coEvery { mockRepository.submitData(any()) } returns expected\n\n        val viewModel = createViewModel()\n        viewModel.stateFlow.test {\n            // Initial state\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n\n            // Updated state after result\n            assertEquals(\n                DEFAULT_STATE.copy(data = \"result\", isLoading = false),\n                awaitItem(),\n            )\n        }\n    }\n\n    @Test\n    fun `SubmitClick should show error dialog on failure`() = runTest {\n        val expected = ExampleResult.Error(message = \"Network error\")\n        coEvery { mockRepository.submitData(any()) } returns expected\n\n        val viewModel = createViewModel()\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n\n            val errorState = awaitItem()\n            assertTrue(errorState.dialogState is ExampleState.DialogState.Error)\n        }\n    }\n\n    @Test\n    fun `BackClick should emit NavigateBack event`() = runTest {\n        val viewModel = createViewModel()\n        viewModel.eventFlow.test {\n            viewModel.trySendAction(ExampleAction.BackClick)\n            assertEquals(ExampleEvent.NavigateBack, awaitItem())\n        }\n    }\n\n    // Helper to create ViewModel with optional saved state\n    private fun createViewModel(\n        state: ExampleState? = DEFAULT_STATE,\n    ): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle(\n            mapOf(KEY_STATE to state),\n        ),\n        exampleRepository = mockRepository,\n    )\n\n    companion object {\n        private val DEFAULT_STATE = ExampleState(\n            isLoading = false,\n            data = null,\n        )\n    }\n}\n```\n\n### Flow Testing with stateEventFlow\n\n```kotlin\n@Test\nfun `SubmitClick should update state and emit event`() = runTest {\n    coEvery { mockRepository.submitData(any()) } returns ExampleResult.Success(\"data\")\n\n    val viewModel = createViewModel()\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        viewModel.trySendAction(ExampleAction.SubmitClick)\n\n        // Assert state change\n        assertEquals(\n            DEFAULT_STATE.copy(data = \"data\"),\n            stateFlow.awaitItem(),\n        )\n\n        // Assert event emission\n        assertEquals(\n            ExampleEvent.ShowToast(\"Success\".asText()),\n            eventFlow.awaitItem(),\n        )\n    }\n}\n```\n",".claude/skills/interacting-with-android-device/SKILL.md":"---\nname: interacting-with-android-device\ndescription: Instructions for capturing UI state, comparing with mocks, and interacting with an Android device using MCP tools backed by ADB.\nallowed-tools: mcp__android-device__capture, mcp__android-device__find_element, mcp__android-device__tap_at, mcp__android-device__tap_element, mcp__android-device__navigate, mcp__android-device__input_text, Bash(adb:*), Bash(sleep:*), Bash(./gradlew install*:*), Read, Glob\n---\n\n# Interacting with Android Device\n\n## Quick Start: MCP Tools\n\nThe `android-device` MCP server provides 6 tools for device interaction. These replace the previous shell scripts with proper XML parsing, structured dumpsys parsing, and native obstruction detection.\n\n**Available tools:**\n- `capture` — Capture UI hierarchy XML and/or screenshot. Params: `{ xml?: boolean, screenshot?: boolean }`. Default: both.\n- `find_element` — Find element by `text` or `content-desc`, return coordinates with **obstruction detection**. Params: `{ text: string }`. Returns JSON with coordinates, bounds, and obstruction status.\n- `tap_at` — Tap at specific coordinates, wait, capture screenshot. Params: `{ x, y, waitSeconds? }`.\n- `tap_element` — Find, tap, and capture in one call (recommended). Params: `{ text, waitSeconds? }`. Auto-adjusts coordinates when obstructed.\n- `navigate` — Navigation actions: home, back, app-drawer. Params: `{ action, waitSeconds? }`. Captures screenshot after action.\n- `input_text` — Type text into the focused field. Params: `{ text, clear? }`. Set `clear: true` to erase existing content first.\n\n**Use these MCP tools instead of raw ADB commands** to save tokens, get structured results, and benefit from automatic obstruction detection.\n\n## 1. Capturing Current State\nTo understand what is currently on the device, use the `capture` tool:\n*   It saves `view.xml` (UI hierarchy) and `screen.png` (screenshot) to the working directory\n*   Read `view.xml` to find coordinates (`bounds`) and properties (like `text` or `resource-id`) of UI elements\n*   Use `screen.png` for visual verification against design mocks\n\n## 2. Interacting with the Device\n\n### Using MCP Tools (Recommended)\n\n*   **Find and tap an element by text** — use `tap_element`:\n    This finds the element, detects obstructions, taps (with adjusted coordinates if needed), and captures a screenshot — all in one call.\n\n*   **Tap at specific coordinates** — use `tap_at`:\n    When you already have coordinates from `find_element` or manual inspection.\n\n*   **Navigate (home, back, app-drawer)** — use `navigate`:\n    Performs the action and captures a screenshot.\n\n*   **Find element without tapping** — use `find_element`:\n    Returns coordinates and full element info. Useful when you need to inspect before acting.\n\n*   **Type text into a field** — use `input_text`:\n    Types text into the currently focused field. Set `clear: true` to erase existing content first. Tap the field before calling this if it isn't already focused.\n\n### Raw ADB Commands (When MCP Tools Aren't Sufficient)\n*   **Key Events**:\n    *   Back: `adb shell input keyevent 4`\n    *   Home: `adb shell input keyevent 3`\n    *   Enter: `adb shell input keyevent 66`\n*   **Scrolling/Swiping**: Use `adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms>` where:\n    *   `(x1, y1)` = starting point\n    *   `(x2, y2)` = ending point\n    *   `duration_ms` = duration in milliseconds (1000ms is typical; adjust for speed/distance)\n    *   **Note**: For expanding containers/drawers, use large distances (e.g., 2400->300 for a 2992px tall screen)\n\n## 3. Obstruction Detection\n\nThe `find_element` and `tap_element` tools automatically detect when another element would intercept the tap. This catches:\n*   **System overlays** (Layer 1): TalkBack floating menu, PiP windows, accessibility services — detected via `dumpsys window windows` touchable regions\n*   **In-app elements** (Layer 2): FABs, dialogs, bottom sheets, snackbars — detected by finding the topmost clickable element at the tap point in the UI hierarchy\n\nWhen obstruction is detected:\n*   Coordinates are **auto-adjusted** to the center of the largest unobstructed strip (top/bottom/left/right of the obstructor)\n*   The response includes the obstructor identity, bounds, and visible region info\n*   If fully obscured (no visible region), the original center is returned as best-effort\n*   **Compose parent wrapper** pattern (identical bounds) is recognized as non-obstruction\n\n## 4. Verification Workflow\nFollow these steps for a complete UI test:\n1.  **Build and Install**: Ensure the latest version of the app is running: `./gradlew installDebug`.\n2.  **Inspect**: Use `capture` to dump the UI hierarchy and take a screenshot.\n3.  **Compare**: Check the current UI against any mock image files in the project.\n4.  **Interact**: Use `tap_element` to tap a UI element by text. The tool handles coordinate calculation and obstruction detection automatically.\n5.  **Verify**: Use `capture` again to confirm the UI has updated as expected (e.g., a new screen is shown, or a success message appeared).\n\n## 5. Examples\n\n### Example: Navigate to Settings and Check for Updates\n```\n# Go to home screen\nnavigate({ action: \"home\" })\n\n# Open app drawer\nnavigate({ action: \"app-drawer\" })\n\n# Find and tap through settings\ntap_element({ text: \"Settings\", waitSeconds: 2 })\ntap_element({ text: \"System\", waitSeconds: 2 })\ntap_element({ text: \"Software updates\", waitSeconds: 2 })\ntap_element({ text: \"Check for update\", waitSeconds: 5 })\n```\n\n### Example: Swiping\nFor swipe gestures not covered by the navigate tool, use raw ADB:\n```bash\nadb shell input swipe 672 2800 672 500 1000 && sleep 1 && adb shell screencap -p /sdcard/screen.png && adb pull /sdcard/screen.png .\n```\n\n## 6. Best Practices\n\n### Coordinate Calculation\n*   Prefer `find_element` or `tap_element` over manual coordinate calculation — they handle bounds parsing, center computation, and obstruction detection automatically\n*   When multiple instances of an element exist (e.g., in prediction row and full list), check the `find_element` response to verify you're targeting the correct one\n\n### Navigation and State Evaluation\n*   **Verify after each interaction**: Don't assume an action succeeded — use `capture` after interactions to confirm the UI changed as expected\n*   **Check both visual and structural state**: Use screenshot for visual verification, XML dump for structural confirmation (element presence, text content, state changes)\n*   **Identify navigation failures early**: If a tap opened the wrong screen, use `navigate({ action: \"back\" })` to recover immediately\n\n### Interaction Patterns\n*   **Scrolling before interaction**: When looking for an element, check if it's visible on screen first. If not, scroll using swipe gestures to reveal it\n*   **Use consistent scroll direction**: For vertical scrolling in lists/settings, use downward swipes (higher Y -> lower Y) to scroll down\n*   **Handle app crashes gracefully**: Don't retry the same action — use back button and try an alternative approach\n*   **Check Accessibility**: Use the `content-desc` and `text` properties in the UI hierarchy to ensure the UI is accessible for screen readers\n\n## 7. Troubleshooting\n\n### Device Not Connected\nIf tools report ADB errors:\n*   Check USB connection or emulator status\n*   Enable USB debugging on the device (Settings > Developer Options > USB Debugging)\n*   Accept the RSA key prompt on the device if asked\n*   Restart the device or disconnect/reconnect the USB cable\n*   Run `adb devices` to verify the device is visible\n\n### MCP Server Not Available\nIf tools are not listed in `/mcp`:\n*   Ensure Node.js 18+ is installed\n*   The server auto-builds on first use via `.mcp.json` at the project root\n*   Check `.claude/mcp/android-device-server/` exists with `package.json`\n*   Try manual build: `cd .claude/mcp/android-device-server && npm install && npm run build`\n",".claude/skills/planning-android-implementation/SKILL.md":"---\nname: planning-android-implementation\nversion: 0.1.0\ndescription: Architecture design and phased implementation planning for Bitwarden Android. Use when planning implementation, designing architecture, creating file inventories, or breaking features into phases. Triggered by \"plan implementation\", \"architecture design\", \"implementation plan\", \"break this into phases\", \"what files do I need\", \"design the architecture\".\n---\n\n# Implementation Planning\n\nThis skill takes a refined specification (ideally from the `refining-android-requirements` skill) and produces a phased implementation plan with architecture design, file inventory, and risk assessment.\n\n**Prerequisite**: A clear set of requirements. If requirements are vague or incomplete, invoke the `refining-android-requirements` skill first.\n\n---\n\n## Step 1: Classify Change\n\nDetermine the change type to guide scope and planning depth:\n\n| Type | Description | Typical Scope |\n|------|-------------|---------------|\n| **New Feature** | Entirely new functionality, screens, or flows | New files + modifications, multi-phase |\n| **Enhancement** | Extending existing feature with new capabilities | Mostly modifications, 1-2 phases |\n| **Bug Fix** | Correcting incorrect behavior | Targeted modifications, single phase |\n| **Refactoring** | Restructuring without behavior change | Modifications only, migration-aware |\n| **Infrastructure** | Build, CI, tooling, or dependency changes | Config files, minimal code changes |\n\nState the classification and rationale before proceeding.\n\n---\n\n## Step 2: Codebase Exploration\n\nSearch the codebase to find reference implementations and integration points. Use the discovery commands from the `build-test-verify` skill as needed.\n\n### Find Pattern Anchors\n\nIdentify 2-3 existing files that serve as templates for the planned work:\n\n```\n**Pattern Anchors:**\n1. [file path] — [why this is a good reference]\n2. [file path] — [why this is a good reference]\n3. [file path] — [why this is a good reference]\n```\n\n### Map Integration Points\n\nIdentify files that must be modified to integrate the new work:\n\n- **Navigation**: Nav graph registrations, route definitions\n- **Dependency Injection**: Hilt modules, `@Provides` / `@Binds` functions\n- **Data Layer**: Repository interfaces, data source interfaces, Room DAOs\n- **API Layer**: Retrofit service interfaces, request/response models\n- **Feature Flags**: Feature flag definitions and checks\n- **Managers**: Single-responsibility data layer classes (see `docs/ARCHITECTURE.md` Managers section)\n- **Test Fixtures**: Shared test utilities in `src/testFixtures/` directories\n- **Product Flavor Source Sets**: Code in `src/standard/` vs `src/main/` for Play Services dependencies\n\n### Document Existing Patterns\n\nNote the specific patterns used by the pattern anchors:\n- State class structure (sealed class, data class fields)\n- Action/Event naming conventions\n- Repository method signatures and return types\n- Test structure and assertion patterns\n\n---\n\n## Step 3: Architecture Design\n\nProduce an ASCII diagram showing component relationships for the planned work:\n\n```\n┌─────────────────┐\n│   Screen        │ ← Compose UI\n│  (Composable)   │\n└────────┬────────┘\n         │ State / Action / Event\n┌────────▼────────┐\n│   ViewModel     │ ← Business logic orchestration\n└────────┬────────┘\n         │ Repository calls\n┌────────▼────────┐\n│   Repository    │ ← Data coordination (sealed class results)\n└───┬────┬────┬───┘\n    │    │    │\n┌───▼───┐ │ ┌─▼──────┐\n│Manager│ │ │Manager │ ← Single-responsibility (optional)\n└───┬───┘ │ └─┬──────┘\n    │     │   │\n┌───▼─────▼───▼────┐\n│   Data Sources   │ ← Raw data (Result<T>, never throw)\n└─┬────┬────┬──────┘\n  │    │    │\n Room Retrofit SDK\n```\n\nAdapt the diagram to show the actual components planned. _Consult `docs/ARCHITECTURE.md` for full data layer patterns and conventions._\n\n### Design Decisions\n\nDocument key architectural decisions in a table:\n\n| Decision | Resolution | Rationale |\n|----------|-----------|-----------|\n| [What needed deciding] | [What was chosen] | [Why] |\n\n---\n\n## Step 4: File Inventory\n\n### Files to Create\n\n| File Path | Type | Pattern Reference |\n|-----------|------|-------------------|\n| [full path] | [ViewModel / Screen / Repository / etc.] | [pattern anchor file] |\n\n**Include in file inventory:**\n- `...Navigation.kt` files for new screens\n- `...Module.kt` Hilt module files for new DI bindings\n- Paired test files (`...Test.kt`) for each new class\n\n### Files to Modify\n\n| File Path | Change Description | Risk Level |\n|-----------|-------------------|------------|\n| [full path] | [what changes] | Low / Medium / High |\n\n**Risk levels:**\n- **Low**: Additive changes (new entries in nav graph, new bindings in Hilt module)\n- **Medium**: Modifying existing logic (adding parameters, new branches)\n- **High**: Changing interfaces, data models, or shared utilities\n\n---\n\n## Step 5: Implementation Phases\n\nBreak the work into sequential phases. Each phase should be independently testable and committable.\n\n**Phase ordering principle**: Foundation → SDK/Data → Network → UI (tests accompany each phase)\n\nFor each phase:\n\n```markdown\n### Phase N: [Name]\n\n**Goal**: [What this phase accomplishes]\n\n**Files**:\n- Create: [list]\n- Modify: [list]\n\n**Tasks**:\n1. [Specific implementation task]\n2. [Specific implementation task]\n3. ...\n\n**Verification**:\n- [Test command or manual verification step]\n\n**Skills**: [Which workflow skills apply — e.g., `implementing-android-code`, `testing-android-code`]\n```\n\n### Phase Guidelines\n\n- Each phase should be small enough to be independently testable and committable\n- Tests are written within the same phase as the code they verify (not deferred to a \"testing phase\")\n- UI phases come after their data dependencies are in place\n- If a phase has more than 5 tasks, consider splitting it\n\n---\n\n## Step 6: Risk & Verification\n\n### Risk Assessment\n\n| Risk | Likelihood | Impact | Mitigation |\n|------|-----------|--------|------------|\n| [What could go wrong] | Low/Med/High | Low/Med/High | [How to prevent or handle] |\n\n### Verification Plan\n\n**Automated Verification:**\n- Unit test commands (from `build-test-verify` skill)\n- Lint/detekt commands\n- Build verification\n\n**Manual Verification:**\n- [Specific manual test scenarios]\n- [Edge cases to manually verify]\n- Verify ViewModel state survives process death (test via `SavedStateHandle` persistence and `Don't keep activities` developer option)\n",".claude/skills/refining-android-requirements/SKILL.md":"---\nname: refining-android-requirements\nversion: 0.1.0\ndescription: Requirements gap analysis and structured specification for Bitwarden Android. Use when refining requirements, analyzing specs, identifying gaps, or producing structured specifications from tickets or descriptions. Triggered by \"refine requirements\", \"gap analysis\", \"spec review\", \"requirements analysis\", \"what's missing from this spec\", \"analyze this ticket\".\n---\n\n# Requirements Refinement\n\nThis skill takes raw requirements (from Jira tickets, Confluence pages, or free-text descriptions) and produces a structured, implementation-ready specification through systematic gap analysis.\n\n**Key principle**: This skill identifies gaps and produces specifications. It does NOT propose solutions or architecture — that is the responsibility of the `planning-android-implementation` skill.\n\n---\n\n## Step 1: Source Consolidation\n\nCombine all input sources into a single working document. For each requirement, note its source:\n\n```\n- [Source: PM-12345] User must be able to configure timeout\n- [Source: Confluence] Timeout range is 1-60 minutes\n- [Source: User] Default timeout should be 15 minutes\n```\n\nFlag any contradictions between sources for immediate resolution.\n\n---\n\n## Step 2: Gap Analysis\n\nEvaluate the consolidated requirements against the following 5-category rubric. For each category, check every item and note whether it is **covered**, **partially covered**, or **missing**.\n\n### A. Functional Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| User actions defined? | What specific user actions trigger this feature? |\n| All states covered? (empty, loading, error, success) | What should the user see in [empty/loading/error] state? |\n| Edge cases identified? | What happens when [boundary condition]? |\n| Cancellation/back navigation flows? | Can the user cancel mid-flow? What happens to partial data? |\n| Input validation rules? | What are the valid ranges/formats for [input]? |\n| Success/failure criteria? | How does the user know the operation succeeded or failed? |\n| Offline behavior? | What happens if this is attempted offline? |\n\n### B. Technical Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Module scope identified? (`:app`, `:authenticator`, shared) | Which module(s) does this feature belong to? |\n| SDK dependencies? | Does this require Bitwarden SDK operations? Which ones? |\n| Data storage approach? (Room, DataStore, in-memory) | Where is the data for this feature persisted? |\n| Network API endpoints? | Which API endpoints are involved? Are they existing or new? |\n| Process death handling? | What state needs to survive process death? |\n| Migration requirements? | Does existing data need migration? |\n| Feature flag needed? | Should this be behind a feature flag for staged rollout? |\n| Product flavors (standard vs fdroid)? | Does this feature depend on Google Play Services? Available on F-Droid? |\n| Data layer tier? | Does this need a new Manager (single-responsibility) or only Repository/DataSource? Consult `docs/ARCHITECTURE.md` Data Layer section. |\n| Streaming vs discrete data? | Is data continuously observed (`DataState<T>` + `StateFlow`) or a one-shot operation (custom sealed class)? See `docs/ARCHITECTURE.md` Repositories section. |\n\n### C. Security Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Data sensitivity classified? | What sensitivity level does this data have? (vault-level, account-level, non-sensitive) |\n| Storage encryption required? | Must this data be encrypted at rest? Via SDK or Android Keystore? |\n| Logout cleanup behavior? | What must be cleared when the user logs out? |\n| Auth-gating? | Does accessing this feature require active authentication? |\n| Input sanitization? | Are there URL or credential inputs that need validation? |\n| Sensitive data in ViewModel state? | Will passwords, tokens, or keys appear in state? Must use `@IgnoredOnParcel`. See `implementing-android-code` skill Section F. |\n| SDK crypto context isolation? | Does this use vault encryption? Must use `ScopedVaultSdkSource` for multi-account safety. See CLAUDE.md Security Rules. |\n\n### D. UX/UI Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| UI copy/strings defined? | What text should appear for [label/button/message]? |\n| Error messages specified? | What should the error message say when [failure case]? |\n| Loading states designed? | Should loading show a spinner, skeleton, or shimmer? |\n| Navigation flow clear? | Where does the user go after [action]? Back stack behavior? |\n| Accessibility considerations? | Are there content descriptions or focus order requirements? |\n| Toast/snackbar/dialog for feedback? | What feedback mechanism for [action result]? |\n\n### E. Cross-Cutting Concerns\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Multi-account behavior? | How does this behave with multiple accounts? Per-account or global? |\n| Backwards compatibility? | Does this affect existing users? Migration path? |\n| Feature flag strategy? | Is this behind a server-side or local feature flag? |\n| Analytics/logging? | Are there analytics events to track? |\n| Bitwarden Authenticator impact? | Does this affect the `:authenticator` module? |\n| F-Droid compatibility? | Does this degrade gracefully without Google Play Services (no push notifications, no Play Integrity)? |\n\n---\n\n## Step 3: Present Gaps\n\nOrganize all identified gaps into two categories:\n\n### Blocking Questions\n\nQuestions that **must** be answered before implementation can begin because they change the architecture, data model, or core flow.\n\nFormat each question as:\n\n```\n**G[N]** ([Category]) — [Question text]\n  Context: [Why this matters / what depends on the answer]\n```\n\n### Non-Blocking Questions\n\nQuestions that have **reasonable defaults** and can be resolved during implementation. Note the assumed default.\n\nFormat each question as:\n\n```\n**G[N]** ([Category]) — [Question text]\n  Default assumption: [What we'll assume if not answered]\n  Context: [Why this matters]\n```\n\n---\n\n## Step 4: Produce Specification\n\nAfter the user answers blocking questions (and optionally non-blocking ones), produce a structured specification:\n\n```markdown\n## Overview\n\n[1-2 paragraph summary of the feature, its purpose, and scope]\n\n## Functional Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| FR1 | [requirement] | [source] | [any notes] |\n| FR2 | ... | ... | ... |\n\n## Technical Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| TR1 | [requirement] | [source] | [any notes] |\n| TR2 | ... | ... | ... |\n\n## Security Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| SR1 | [requirement] | [source] | [any notes] |\n\n## UX Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| UX1 | [requirement] | [source] | [any notes] |\n\n## Open Items\n\nNon-blocking items with assumed defaults that may be revisited:\n\n| ID | Question | Assumed Default | Category |\n|----|----------|----------------|----------|\n| G[N] | [question] | [default] | [category] |\n\n## Source Documentation\n\n| Source | Type | Link |\n|--------|------|------|\n| [name] | Jira / Confluence / User-provided | [link if available] |\n```\n\n### Output Guidelines\n\n- Requirements use numbered IDs (FR1, TR1, SR1, UX1) for traceability through implementation\n- Each requirement cites its source (ticket, page, or user-provided)\n- Technical requirements use table format for structured key/value data\n- Interface signatures are included as fenced code blocks when applicable\n- Open items preserve the gap ID (G[N]) for cross-referencing",".claude/skills/reviewing-changes/SKILL.md":"---\nname: reviewing-changes\ndescription: Android-specific code review checklist and MVVM/Compose pattern validation for Bitwarden Android — use this for any review task, even if the user doesn't explicitly ask for a \"checklist\". Detects change type automatically and loads the right review strategy (feature additions, bug fixes, UI refinements, refactoring, dependency updates, infrastructure). Triggered by \"review PR\", \"review changes\", \"review this code\", \"check this code\", \"Android review\", code review requests on Kotlin/ViewModel/Composable/Repository/Gradle files, or any time someone asks to look at a diff, PR, or code changes in bitwarden/android.\n---\n\n# Reviewing Changes - Android Additions\n\nThis skill provides Android-specific workflow additions that complement the base `bitwarden-code-reviewer` agent standards.\n\n## Instructions\n\n**IMPORTANT**: Work systematically through each step before providing feedback. Each checklist file includes structured thinking guidance for its review passes.\n\n### Step 1: Retrieve Additional Details\n\nRetrieve any additional information linked to the pull request using available tools (JIRA MCP, GitHub API).\n\nIf pull request title and message do not provide enough context, request additional details from the reviewer:\n- Link a JIRA ticket\n- Associate a GitHub issue\n- Link to another pull request\n- Add more detail to the PR title or body\n\n**Android metadata checks** — flag as ❓ if any of these are missing:\n- PR includes `*Screen.kt` or Composable changes but has no screenshots\n- PR adds new `ViewModel` or `Repository` but has no test plan or test file changes\n\n### Step 2: Detect Change Type with Android Refinements\n\nUse the base change type detection from the agent, with Android-specific refinements:\n\n**Android-specific patterns:**\n- **Feature Addition**: New `ViewModel`, new `Repository`, new `@Composable` functions, new `*Screen.kt` files\n- **UI Refinement**: Changes only in `*Screen.kt`, `*Composable.kt`, `ui/` package files\n- **Infrastructure**: Changes to `.github/workflows/`, `gradle/`, `build.gradle.kts`, `libs.versions.toml`\n- **Dependency Update**: Changes only to `libs.versions.toml` or `build.gradle.kts` with version bumps\n\n### Step 3: Load Appropriate Checklist\n\nBased on detected type, read the relevant checklist file:\n\n- **Dependency Update** → `checklists/dependency-update.md` (expedited review)\n- **Bug Fix** → `checklists/bug-fix.md` (focused review)\n- **Feature Addition** → `checklists/feature-addition.md` (comprehensive review)\n- **UI Refinement** → `checklists/ui-refinement.md` (design-focused review)\n- **Refactoring** → `checklists/refactoring.md` (pattern-focused review)\n- **Infrastructure** → `checklists/infrastructure.md` (tooling-focused review)\n\nThe checklist provides:\n- Multi-pass review strategy\n- Type-specific focus areas\n- What to check and what to skip\n- Structured thinking guidance\n\n### Step 4: Execute Review Following Checklist\n\nFollow the checklist's multi-pass strategy, thinking through each pass systematically.\n\n### Step 5: Consult Android Reference Materials As Needed\n\nLoad reference files only when needed for specific questions:\n\n- **Re-reviews** → invoke `reviewing-incremental-changes` agent skill; scope to changed lines only, do not flag new issues in unchanged code\n- **Issue prioritization** → `reference/priority-framework.md` (Critical vs Suggested vs Optional)\n- **Phrasing feedback** → `reference/review-psychology.md` (questions vs commands, I-statements)\n- **Architecture questions** → `reference/architectural-patterns.md` (MVVM, Hilt DI, module org, error handling)\n- **Security questions (quick reference)** → `reference/security-patterns.md` (common patterns and anti-patterns)\n- **Security questions (comprehensive)** → `docs/ARCHITECTURE.md#security` (full zero-knowledge architecture)\n- **Testing questions** → `reference/testing-patterns.md` (unit tests, mocking, null safety)\n- **UI questions** → `reference/ui-patterns.md` (Compose patterns, theming)\n- **Style questions (project-specific)** → `reference/style-patterns.md` (Kotlin rules enforced in review)\n- **Style questions (general)** → `docs/STYLE_AND_BEST_PRACTICES.md`\n\n## Core Principles\n\n- **Priority order**: Security → Correctness → Breaking Changes → Performance → Maintainability\n- **Appropriate depth**: Match review rigor to change complexity and risk\n- **Specific references**: Always use `file:line_number` format for precise location\n- **Actionable feedback**: Say what to do and why, not just what's wrong\n- **Efficient reviews**: Use multi-pass strategy, skip what's not relevant\n- **Android patterns**: Validate MVVM, Hilt DI, Compose conventions, Kotlin idioms\n",".claude/skills/reviewing-changes/checklists/bug-fix.md":"# Bug Fix Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Bug\n\n**1. Understand root cause:**\n- What was the broken behavior?\n- What caused it?\n- How does this fix address the root cause?\n\n**2. Assess scope:**\n- How many files changed?\n- Is this a targeted fix or broader refactoring?\n- Does this affect multiple features?\n\n**3. Check for side effects:**\n- Could this break other features?\n- Are there edge cases not considered?\n\n### Second Pass: Verify the Fix\n\n**4. Code changes:**\n- Does the fix make sense?\n- Is it the simplest solution?\n- Any unnecessary changes included?\n\n**5. Testing:**\n- Is there a regression test?\n- Does test verify the bug is fixed?\n- Are edge cases covered?\n\n**6. Related code:**\n- Same pattern in other places that might have same bug?\n- Should other similar code be fixed too?\n\n## What to CHECK\n\n✅ **Root Cause Analysis**\n- Does the fix address the root cause or just symptoms?\n- Is the explanation in PR/commit clear?\n\n✅ **Regression Testing**\n- Is there a new test that would fail without this fix?\n- Does test cover the reported bug scenario?\n- Are related edge cases tested?\n\n✅ **Side Effects**\n- Could this break existing functionality?\n- Are there similar code paths that need checking?\n- Does this change behavior in unexpected ways?\n\n✅ **Fix Scope**\n- Is the fix appropriately scoped (not too broad, not too narrow)?\n- Are all instances of the bug fixed?\n- Any related bugs discovered during investigation?\n\n## What to SKIP\n\n❌ **Full Architecture Review** - Unless fix reveals architectural problems\n❌ **Comprehensive Testing Review** - Focus on regression tests, not entire test suite\n❌ **Major Refactoring Suggestions** - Unless directly related to preventing similar bugs\n\n## Red Flags\n\n🚩 **No test for the bug** - How will we prevent regression?\n🚩 **Fix doesn't match root cause** - Is this fixing symptoms?\n🚩 **Broad changes beyond the bug** - Should this be split into separate PRs?\n🚩 **Similar patterns elsewhere** - Should those be fixed too?\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"Can we add a test that would fail without this fix?\"\n- \"I see this pattern in [other file] - does it have the same issue?\"\n- \"Is this fixing the root cause or masking the symptom?\"\n- \"Could this change affect [related feature]?\"\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n**Overall Assessment:** APPROVE\n\nSee inline comments for suggested improvements.\n```\n\n**Inline comment examples:**\n\n```\n**data/auth/BiometricRepository.kt:120** - SUGGESTED: Extract null handling\n\n<details>\n<summary>Details</summary>\n\nRoot cause analysis: BiometricPrompt result was nullable but code assumed non-null, causing crash on cancellation (PM-12345).\n\nConsider extracting null handling pattern:\n\n\\```kotlin\nprivate fun handleBiometricResult(result: BiometricPrompt.AuthenticationResult?): AuthResult {\n    return result?.let { AuthResult.Success(it) } ?: AuthResult.Cancelled\n}\n\\```\n\nThis pattern could be reused if we add other biometric auth points.\n</details>\n```\n\n```\n**app/auth/BiometricViewModel.kt:89** - SUGGESTED: Add regression test\n\n<details>\n<summary>Details</summary>\n\nAdd test for cancellation scenario to prevent regression:\n\n\\```kotlin\n@Test\nfun `when biometric cancelled then returns cancelled state`() = runTest {\n    coEvery { repository.authenticate() } returns Result.failure(CancelledException())\n    viewModel.onBiometricAuth()\n    assertEquals(AuthState.Cancelled, viewModel.state.value)\n}\n\\```\n\nThis prevents regression of the bug just fixed.\n</details>\n```\n",".claude/skills/reviewing-changes/checklists/dependency-update.md":"# Dependency Update Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Identify and Assess\n\n**1. Identify the change:**\n- Which library? Old version → New version?\n- Major (X.0.0), Minor (0.X.0), or Patch (0.0.X) version change?\n- Single dependency or multiple?\n\n**2. Check compilation safety:**\n- Any imports in codebase that might break?\n- Any deprecated APIs we're currently using?\n- Check if this is a breaking change version\n\n### Second Pass: Deep Analysis\n\n**3. Review release notes** (if available):\n- Breaking changes mentioned?\n- Security fixes included?\n- New features we should know about?\n- Deprecations that affect our usage?\n\n**4. Verify consistency:**\n- If updating androidx library, are related libraries updated consistently?\n- BOM (Bill of Materials) consistency if applicable?\n- Test dependencies updated alongside main dependencies?\n\n## What to CHECK\n\n✅ **Compilation Safety**\n- Look for API deprecations in our codebase\n- Check if import statements still valid\n- Major version bumps require extra scrutiny\n- Beta/alpha versions need stability assessment\n\n✅ **Security Implications** (if applicable)\n- Security-related libraries (crypto, auth, networking)?\n- Check for CVEs addressed in release notes\n- Review security advisories for this library\n\n✅ **Testing Implications**\n- Does this affect test utilities?\n- Are there breaking changes in test APIs?\n- Do existing tests still cover the same scenarios?\n\n✅ **Changelog Review**\n- Read release notes for breaking changes\n- Note any behavioral changes\n- Check migration guides if major version\n\n## What to SKIP\n\n❌ **Full Architecture Review** - No code changed, patterns unchanged\n❌ **Code Style Review** - No code to review\n❌ **New Test Requirements** - Unless API changed significantly\n❌ **Security Deep-Dive** - Unless crypto/auth/networking library\n❌ **Performance Analysis** - Unless release notes mention performance changes\n\n## Red Flags (Escalate to Full Review)\n\n🚩 **Major version bump** (e.g., 1.x → 2.0) - Read `checklists/feature-addition.md`\n🚩 **Security/crypto library** - Read `reference/architectural-patterns.md` and `docs/ARCHITECTURE.md#security`\n🚩 **Breaking changes in release notes** - Read relevant code sections carefully\n🚩 **Multiple dependency updates at once** - Check for interaction risks\n🚩 **Beta/Alpha versions** - Assess stability concerns and rollback plan\n\nIf any red flags present, escalate to more comprehensive review using appropriate checklist.\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Reviews\n\n### Example 1: Simple Patch Version (No Critical Issues)\n\n```markdown\n**Overall Assessment:** APPROVE\n\nSee inline comments for all issue details.\n```\n\n**Inline comment example:**\n```\n**libs.versions.toml:45** - SUGGESTED: Beta version in production\n\n<details>\n<summary>Details</summary>\n\nandroidx.credentials updated from 1.5.0 to 1.6.0-beta03\n\nMonitor for stability issues - beta releases may have unexpected behavior in production.\n\nChangelog: Adds support for additional credential types, internal bug fixes.\n</details>\n```\n\n### Example 2: Major Version with Breaking Changes (With Critical Issues)\n\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Breaking API changes in Retrofit 3.0.0 (network/api/BitwardenApiService.kt)\n- Breaking API changes in Retrofit 3.0.0 (network/api/VaultApiService.kt)\n\nSee inline comments for migration details.\n```\n\n**Inline comment example:**\n```\n**network/api/BitwardenApiService.kt:15** - CRITICAL: Breaking API changes\n\n<details>\n<summary>Details and fix</summary>\n\nRetrofit 3.0.0 removes `Call<T>` return type. Migration required:\n\n\\```kotlin\n// Before\nfun getUser(): Call<UserResponse>\n\n// After\nsuspend fun getUser(): Response<UserResponse>\n\\```\n\nUpdate all API service interfaces to use suspend functions, update call sites to use coroutines instead of enqueue/execute, and update tests accordingly.\n\nConsider creating a separate PR for this migration due to scope.\n\nReference: https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-300\n</details>\n```\n",".claude/skills/reviewing-changes/checklists/feature-addition.md":"# Feature Addition Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: High-Level Assessment\n\n**1. Understand the feature:**\n- Read PR description - what problem does this solve?\n- Identify user-facing changes vs internal changes\n- Note any security implications (auth, encryption, data handling)\n\n**2. Scan file structure:**\n- Which modules affected? (app, data, network, ui, core?)\n- Are files organized correctly per module structure?\n- Any new public APIs introduced?\n\n**3. Initial risk assessment:**\n- Does this touch sensitive data or security-critical paths?\n- Does this affect existing features or only add new ones?\n- Are there obvious compilation or null safety issues?\n\n### Second Pass: Architecture Deep-Dive\n\n**4. MVVM + UDF Pattern Compliance:**\n- ViewModels properly structured?\n- State management using StateFlow?\n- Business logic in correct layer?\n\n**5. Dependency Injection:**\n- Hilt DI used correctly?\n- Dependencies injected, not manually instantiated?\n- Proper scoping applied?\n\n**6. Module Organization:**\n- Code placed in correct modules?\n- No circular dependencies introduced?\n- Proper separation of concerns?\n\n**7. Error Handling:**\n- Using Result types, not exception-based handling?\n- Errors propagated correctly through layers?\n\n### Third Pass: Details and Quality\n\n**8. Testing:**\n- Unit tests for ViewModels and repositories?\n- Test coverage for edge cases and error scenarios?\n- Tests verify behavior, not implementation?\n\n**9. Code Quality:**\n- Null safety handled properly?\n- Public APIs have KDoc documentation?\n- Naming follows project conventions?\n\n**10. Security:**\n- Sensitive data encrypted properly?\n- Authentication/authorization handled correctly?\n- Zero-knowledge architecture preserved?\n\n## Architecture Review\n\nRead `reference/architectural-patterns.md` for full patterns and code examples.\n\n**Check these four areas:**\n- **MVVM/UDF**: ViewModel exposes `StateFlow` (not `MutableStateFlow`), business logic in Repository, UI is stateless\n- **Hilt DI**: `@HiltViewModel` + `@Inject constructor`, inject interfaces not implementations, no manual instantiation\n- **Module placement**: UI in `:ui`/`:app`, data in `:data`, network in `:network`, no circular dependencies\n- **Error handling**: `Result<T>` / `runCatching` throughout — no thrown exceptions from data layer\n\n## Security Review\n\nReference: `docs/ARCHITECTURE.md#security`\n\n**Critical Security Checks:**\n\n- **Sensitive data encrypted**: Passwords, keys, tokens use Android Keystore or EncryptedSharedPreferences\n- **No plaintext secrets**: No passwords/keys in logs, memory dumps, or SharedPreferences\n- **Input validation**: All user-provided data validated and sanitized\n- **Authentication tokens**: Securely stored and transmitted\n- **Zero-knowledge architecture**: Encryption happens client-side, server never sees plaintext\n\n**Red Flags:**\n```kotlin\n// ❌ CRITICAL - Plaintext storage\nsharedPreferences.edit {\n    putString(\"pin\", userPin)  // Must use EncryptedSharedPreferences\n}\n\n// ❌ CRITICAL - Logging sensitive data\nLog.d(\"Auth\", \"Password: $password\")  // Never log sensitive data\n\n// ❌ CRITICAL - Weak encryption\nval cipher = Cipher.getInstance(\"DES\")  // Use AES-256-GCM\n\n// ✅ GOOD - Keystore encryption\nval encryptedData = keystoreManager.encrypt(sensitiveData)\nsecureStorage.store(encryptedData)\n```\n\n**If security concerns found, classify as CRITICAL using `reference/priority-framework.md`**\n\n## Testing Review\n\nReference: `reference/testing-patterns.md`\n\n**Required Test Coverage:**\n\n- **ViewModels**: Unit tests for state transitions, actions, error scenarios\n- **Repositories**: Unit tests for data transformations, error handling\n- **Business logic**: Unit tests for complex algorithms, calculations\n- **Edge cases**: Null inputs, empty states, network failures, concurrent operations\n\n**Test Quality:**\n```kotlin\n// ✅ GOOD - Tests behavior\n@Test\nfun `when login succeeds then state updates to success`() = runTest {\n    val viewModel = LoginViewModel(mockRepository)\n\n    coEvery { mockRepository.login(any(), any()) } returns Result.success(User())\n\n    viewModel.onLoginClicked(\"user\", \"pass\")\n\n    viewModel.state.test {\n        assertEquals(LoginState.Success, awaitItem())\n    }\n}\n\n// ❌ BAD - Tests implementation\n@Test\nfun `repository is called with correct parameters`() {\n    // This is testing internal implementation, not behavior\n}\n```\n\n**Testing Frameworks:**\n- JUnit 5 for test structure\n- MockK for mocking\n- Turbine for Flow testing\n- Kotlinx-coroutines-test for coroutine testing\n\n## Code Quality\n\n### Null Safety\n\n- No `!!` (non-null assertion) without clear safety guarantee\n- Platform types (from Java) handled with explicit nullability\n- Nullable types have proper null checks or use safe operators (`?.`, `?:`)\n\n```kotlin\n// ❌ BAD - Unsafe assertion\nval result = apiService.getData()!!  // Could crash\n\n// ✅ GOOD - Safe handling\nval result = apiService.getData() ?: return State.Error(\"No data\")\n\n// ❌ BAD - Platform type unchecked\nval intent: Intent = getIntent()  // Could be null from Java\nintent.getStringExtra(\"key\")  // Potential NPE\n\n// ✅ GOOD - Explicit nullability\nval intent: Intent? = getIntent()\nintent?.getStringExtra(\"key\")\n```\n\n### Documentation\n\n- **Public APIs**: Have KDoc comments explaining purpose, parameters, return values\n- **Complex algorithms**: Explained in comments\n- **Non-obvious behavior**: Documented with rationale\n\n```kotlin\n// ✅ GOOD - Documented public API\n/**\n * Encrypts the given data using AES-256-GCM with a key from Android Keystore.\n *\n * @param plaintext The data to encrypt\n * @return Result containing encrypted data or encryption error\n */\nsuspend fun encrypt(plaintext: ByteArray): Result<EncryptedData>\n```\n\n### Style Compliance\n\nReference: `docs/STYLE_AND_BEST_PRACTICES.md`\n\nOnly flag style issues if:\n- Not caught by linters (Detekt, ktlint)\n- Have architectural implications\n- Significantly impact readability\n\nSkip minor formatting (spaces, line breaks, etc.) - linters handle this.\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Providing Feedback\n\nUse `reference/review-psychology.md` for phrasing guidance.\n\n**Key principles:**\n- **Ask questions** for design decisions: \"Can we use the existing BitwardenTextField component here?\"\n- **Be prescriptive** for clear violations: \"Change MutableStateFlow to StateFlow (MVVM pattern requirement)\"\n- **Explain rationale**: \"This exposes mutable state, violating unidirectional data flow\"\n- **Use I-statements**: \"It's hard for me to understand this logic without comments\"\n- **Avoid condescension**: Don't use \"just\", \"simply\", \"obviously\"\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n",".claude/skills/reviewing-changes/checklists/infrastructure.md":"# Infrastructure Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Change\n\n**1. Identify the goal:**\n- What problem does this solve?\n- Is this optimization, fix, or new capability?\n- What's the expected impact?\n\n**2. Assess risk:**\n- Does this affect production builds?\n- Could this break CI/CD pipelines?\n- Impact on developer workflow?\n\n**3. Performance implications:**\n- Will builds be faster or slower?\n- CI time impact?\n- Resource usage changes?\n\n### Second Pass: Verify Implementation\n\n**4. Configuration correctness:**\n- Syntax valid?\n- References correct?\n- Secrets/credentials handled securely?\n\n**5. Impact analysis:**\n- What workflows/builds are affected?\n- Rollback plan if this breaks?\n- Documentation for team?\n\n**6. Testing strategy:**\n- How can this be tested before merge?\n- Canary/gradual rollout possible?\n- Monitoring for issues post-merge?\n\n## What to CHECK\n\n✅ **Configuration Correctness**\n- YAML/Groovy syntax valid\n- File references correct\n- Version numbers/tags valid\n- Conditional logic sound\n\n✅ **Security**\n- No hardcoded secrets or credentials\n- GitHub secrets used properly\n- Permissions appropriately scoped\n- No sensitive data in logs\n\n✅ **Performance Impact**\n- Build time implications understood\n- CI queue time impact assessed\n- Resource usage reasonable\n\n✅ **Rollback Plan**\n- Can this be reverted easily?\n- Dependencies on other changes?\n- Gradual rollout possible?\n\n✅ **Documentation**\n- Changes documented for team?\n- README or CONTRIBUTING updated?\n- Breaking changes clearly noted?\n\n## What to SKIP\n\n❌ **Bikeshedding Configuration** - Unless clear performance/maintenance benefit\n❌ **Over-Optimization** - Unless current system has proven problems\n❌ **Suggesting Major Rewrites** - Unless current approach is fundamentally broken\n\n## Red Flags\n\n🚩 **Hardcoded secrets** - Use GitHub secrets or secure storage\n🚩 **No rollback plan** - Critical infrastructure should be revertible\n🚩 **Untested changes** - CI changes should be validated\n🚩 **Breaking changes without notice** - Team needs advance warning\n🚩 **Performance regression** - Builds shouldn't get significantly slower\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"What's the rollback plan if this breaks CI?\"\n- \"Can we test this on a feature branch before main?\"\n- \"Will this impact build times? By how much?\"\n- \"Should this be documented in CONTRIBUTING.md?\"\n\n## Common Infrastructure Patterns\n\n### GitHub Actions\n\n```yaml\n# ✅ GOOD - Secure, clear, tested\nname: Build and Test\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30  # Prevent runaway builds\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run tests\n        env:\n          API_KEY: ${{ secrets.API_KEY }}  # Secure secret usage\n        run: ./gradlew test\n\n# ❌ BAD - Insecure, unclear\nname: Build\non: push  # Too broad, runs on all branches\njobs:\n  build:\n    runs-on: ubuntu-latest\n    # No timeout - could run forever\n    steps:\n      - run: |\n          export API_KEY=\"hardcoded_key_here\"  # Hardcoded secret!\n          ./gradlew test\n```\n\n### Gradle Configuration\n\n```kotlin\n// ✅ GOOD - Clear, maintainable\ndependencies {\n    implementation(libs.androidx.core.ktx)  // Version catalog\n    implementation(libs.hilt.android)\n\n    testImplementation(libs.junit5)\n    testImplementation(libs.mockk)\n}\n\n// ❌ BAD - Hardcoded versions\ndependencies {\n    implementation(\"androidx.core:core-ktx:1.12.0\")  // Hardcoded version\n    implementation(\"com.google.dagger:hilt-android:2.48\")\n}\n```\n\n### Build Optimization\n\n```kotlin\n// ✅ GOOD - Parallel, cached\ntasks.register(\"checkAll\") {\n    dependsOn(\"detekt\", \"ktlintCheck\", \"testStandardDebug\")\n    group = \"verification\"\n    description = \"Run all checks in parallel\"\n\n    // Enable caching for faster builds\n    outputs.upToDateWhen { false }\n}\n\n// ❌ BAD - Sequential, no caching\ntasks.register(\"checkAll\") {\n    doLast {\n        exec { commandLine(\"./gradlew\", \"detekt\") }\n        exec { commandLine(\"./gradlew\", \"ktlintCheck\") }  // Sequential\n        exec { commandLine(\"./gradlew\", \"test\") }\n    }\n}\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n## Summary\nOptimizes CI build by parallelizing test execution and caching dependencies\n\nImpact: Estimated 40% reduction in CI time (12 min → 7 min per build)\n\n## Critical Issues\nNone\n\n## Suggested Improvements\n\n**.github/workflows/build.yml:23** - Add timeout for safety\n```yaml\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30  # Prevent builds from hanging\n    steps:\n      # ...\n```\nThis prevents runaway builds if something goes wrong.\n\n**.github/workflows/build.yml:45** - Consider matrix strategy for module tests\nCan we run module tests in parallel using a matrix strategy?\n```yaml\nstrategy:\n  matrix:\n    module: [app, data, network, ui]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: ./gradlew :${{ matrix.module }}:test\n```\nThis could further reduce CI time.\n\n**build.gradle.kts:12** - Document caching strategy\nCan we add a comment explaining the caching configuration?\nFuture maintainers will appreciate understanding why these specific cache keys are used.\n\n## Rollback Plan\nIf CI breaks:\n- Revert commit: `git revert [commit-hash]`\n- Previous workflow available at: `.github/workflows/build.yml@main^`\n- Monitor CI times at: https://github.com/[org]/[repo]/actions\n```\n",".claude/skills/reviewing-changes/checklists/refactoring.md":"# Refactoring Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Refactoring\n\n**1. Understand the goal:**\n- What pattern is being improved?\n- Why is this refactoring needed?\n- What's the scope of changes?\n\n**2. Assess completeness:**\n- Are all instances refactored or just some?\n- Are there related areas that should also change?\n- Is the migration complete or partial?\n\n**3. Risk assessment:**\n- Does this change behavior?\n- How many files affected?\n- Are tests updated to reflect changes?\n\n### Second Pass: Verify Consistency\n\n**4. Pattern consistency:**\n- Is the new pattern applied consistently throughout?\n- Are there missed instances of the old pattern?\n- Does this match established project patterns?\n\n**5. Migration completeness:**\n- Old pattern fully removed or deprecated?\n- All usages updated?\n- Documentation updated?\n\n**6. Test coverage:**\n- Do tests still pass?\n- Are tests refactored to match?\n- Does behavior remain unchanged?\n\n## What to CHECK\n\n✅ **Pattern Consistency**\n- New pattern applied consistently across all touched code\n- Follows established project patterns (MVVM, DI, error handling)\n- No mix of old and new patterns\n\n✅ **Migration Completeness**\n- All instances of old pattern updated?\n- Deprecated methods removed or marked @Deprecated?\n- Related code also updated (tests, docs)?\n\n✅ **Behavior Preservation**\n- Refactoring doesn't change behavior\n- Tests still pass\n- Edge cases still handled\n\n✅ **Deprecation Strategy** (if applicable)\n- Old APIs marked @Deprecated with migration guidance\n- Replacement clearly documented\n- Timeline for removal specified\n\n## What to SKIP\n\n❌ **Suggesting Additional Refactorings** - Unless directly related to current changes\n❌ **Scope Creep** - Don't request refactoring of untouched code\n❌ **Perfection** - Better code is better than perfect code\n\n## Red Flags\n\n🚩 **Incomplete migration** - Mix of old and new patterns\n🚩 **Behavior changes** - Refactoring shouldn't change behavior\n🚩 **Broken tests** - Tests should be updated to match refactoring\n🚩 **Undocumented pattern** - New pattern should be clear to team\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"I see the old pattern still used in [file:line] - should that be updated too?\"\n- \"Can we add @Deprecated to the old method with migration guidance?\"\n- \"How do we ensure this behavior remains the same?\"\n- \"Should this pattern be documented in ARCHITECTURE.md?\"\n\n## Common Refactoring Patterns\n\n### Extract Interface/Repository\n\n```kotlin\n// ✅ GOOD - Complete migration\ninterface FeatureRepository {\n    suspend fun getData(): Result<Data>\n}\n\nclass FeatureRepositoryImpl @Inject constructor(\n    private val apiService: FeatureApiService\n) : FeatureRepository {\n    override suspend fun getData(): Result<Data> = runCatching {\n        apiService.fetchData()\n    }\n}\n\n// All usages updated to inject interface\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository  // Interface\n) : ViewModel()\n\n// ❌ BAD - Incomplete migration\n// Some files still inject FeatureRepositoryImpl directly\n```\n\n### Modernize Error Handling\n\n```kotlin\n// ✅ GOOD - Complete migration\n// Old exception-based removed\nsuspend fun fetchData(): Result<Data> = runCatching {\n    apiService.getData()\n}\n\n// All call sites updated\nrepository.fetchData().fold(\n    onSuccess = { /* handle */ },\n    onFailure = { /* handle */ }\n)\n\n// ❌ BAD - Mixed patterns\n// Some functions use Result, others still throw exceptions\n```\n\n### Extract Reusable Component\n\n```kotlin\n// ✅ GOOD - Complete extraction\n// Component moved to :ui module\n@Composable\nfun BitwardenButton(\n    text: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier\n)\n\n// All usages updated to use new component\n// Old inline button implementations removed\n\n// ❌ BAD - Incomplete extraction\n// Some screens use new component, others still have inline implementation\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Reviews\n\n### Example 1: Refactoring with Incomplete Migration\n\n**Context**: Refactoring authentication to Repository pattern, but one ViewModel still uses old pattern\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Incomplete migration (app/vault/VaultViewModel.kt:89)\n\nSee inline comments for details.\n```\n\n**Inline Comment 1** (on `app/vault/VaultViewModel.kt:89`):\n```markdown\n**IMPORTANT**: Incomplete migration\n\n<details>\n<summary>Details and fix</summary>\n\nThis ViewModel still injects AuthManager directly. Should it use AuthRepository like the other 11 ViewModels?\n\n\\```kotlin\n// Current (old pattern)\nclass VaultViewModel @Inject constructor(\n    private val authManager: AuthManager\n)\n\n// Should be (new pattern)\nclass VaultViewModel @Inject constructor(\n    private val authRepository: AuthRepository\n)\n\\```\n\nThis is the only ViewModel still using the old pattern.\n</details>\n```\n\n**Inline Comment 2** (on `data/auth/AuthManager.kt:1`):\n```markdown\n**SUGGESTED**: Add deprecation notice\n\n<details>\n<summary>Details</summary>\n\nCan we add @Deprecated to AuthManager to guide future development?\n\n\\```kotlin\n@Deprecated(\n    message = \"Use AuthRepository interface instead\",\n    replaceWith = ReplaceWith(\"AuthRepository\"),\n    level = DeprecationLevel.WARNING\n)\nclass AuthManager @Inject constructor(...)\n\\```\n\nThis helps prevent new code from using the old pattern.\n</details>\n```\n\n---\n\n### Example 2: Clean Refactoring (No Issues)\n\n**Context**: Refactoring with complete migration, all patterns followed correctly, tests passing\n\n**Review Comment:**\n```markdown\n**Overall Assessment:** APPROVE\n\nClean refactoring moving ExitManager to :ui module. Follows established patterns, eliminates duplication, tests updated correctly.\n```\n\n**Token count:** ~30 tokens (vs ~800 for verbose format)\n\n**Why this works:**\n- 3 lines total\n- Clear approval decision\n- Briefly notes what was done\n- No elaborate sections, checkmarks, or excessive praise\n- Author gets immediate green light to merge\n\n**What NOT to do for clean refactorings:**\n```markdown\n❌ DO NOT create these sections:\n\n## Summary\nThis PR successfully refactors ExitManager into shared code...\n\n## Key Strengths\n- ✅ Follows established module organization patterns\n- ✅ Removes code duplication between apps\n- ✅ Improves test coverage\n- ✅ Maintains consistent behavior\n[...20 more checkmarks...]\n\n## Code Quality & Architecture\n**Architectural Compliance:** ✅\n- Correctly places manager in :ui module\n- Follows established pattern for UI-layer managers\n[...detailed analysis...]\n\n## Changes\n- ✅ Moved ExitManager interface from app → ui module\n- ✅ Moved ExitManagerImpl from app → ui module\n[...listing every file...]\n```\n\nThis is excessive. **For clean PRs: 2-3 lines maximum.**\n",".claude/skills/reviewing-changes/checklists/ui-refinement.md":"# UI Refinement Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Visual Changes\n\n**1. Understand the changes:**\n- What visual/UX problem is being solved?\n- Are there designs or screenshots to reference?\n- Is this a bug fix or enhancement?\n\n**2. Component usage:**\n- Using existing components from `:ui` module?\n- Any new custom components created?\n- Could existing components be reused?\n\n### Second Pass: Implementation Review\n\n**3. Compose best practices:**\n- Composables properly structured?\n- State hoisted correctly?\n- Preview composables included?\n\n**4. Accessibility:**\n- Content descriptions for images/icons?\n- Semantic properties for screen readers?\n- Touch targets meet minimum size (48dp)?\n\n**5. Design consistency:**\n- Using theme colors, spacing, typography?\n- Consistent with other screens?\n- Responsive to different screen sizes?\n\n## What to CHECK\n\n✅ **Compose Best Practices**\n- Composables are stateless where possible\n- State hoisting follows patterns\n- Side effects (LaunchedEffect, DisposableEffect) used correctly\n- Preview composables provided for development\n\n✅ **Component Reuse**\n- Using existing BitwardenButton, BitwardenTextField, etc.?\n- Could custom UI be replaced with existing components?\n- New reusable components placed in `:ui` module?\n\n✅ **Accessibility**\n- `contentDescription` for icons and images\n- `semantics` for custom interactions\n- Sufficient contrast ratios\n- Touch targets ≥ 48dp minimum\n\n✅ **Design Consistency**\n- Using `BitwardenTheme` colors (not hardcoded)\n- Using `BitwardenTheme` spacing (16.dp, 8.dp, etc.)\n- Using `BitwardenTheme` typography styles\n- Consistent with existing screen patterns\n\n✅ **Responsive Design**\n- Handles different screen sizes?\n- Scrollable content where appropriate?\n- Landscape orientation considered?\n\n## What to SKIP\n\n❌ **Deep Architecture Review** - Unless ViewModel changes are substantial\n❌ **Business Logic Review** - Focus is on presentation, not logic\n❌ **Security Review** - Unless UI exposes sensitive data improperly\n\n## Red Flags\n\n🚩 **Duplicating existing components** - Should reuse from `:ui` module\n🚩 **Hardcoded colors/dimensions** - Should use theme\n🚩 **Missing accessibility properties** - Critical for screen readers\n🚩 **State management in UI** - Should be hoisted to ViewModel\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"Can we use BitwardenButton here instead of this custom button?\"\n- \"Should this color come from BitwardenTheme instead of being hardcoded?\"\n- \"How will this look on a small screen?\"\n- \"Is there a contentDescription for this icon?\"\n\n## Common Patterns\n\n### Composable Structure\n\n```kotlin\n// ✅ GOOD - Stateless, hoisted state\n@Composable\nfun FeatureScreen(\n    state: FeatureState,\n    onActionClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    // UI rendering only\n}\n\n// ❌ BAD - Business state in composable\n@Composable\nfun FeatureScreen() {\n    var userData by remember { mutableStateOf<User?>(null) }  // Business state should be in ViewModel\n    var isLoading by remember { mutableStateOf(false) }  // App state should be in ViewModel\n    // ...\n}\n\n// ✅ OK - UI-local state in composable\n@Composable\nfun LoginForm(onSubmit: (String, String) -> Unit) {\n    var username by remember { mutableStateOf(\"\") }  // UI-local input state is fine\n    var password by remember { mutableStateOf(\"\") }\n    // Hoist only as high as needed\n}\n```\n\n### Theme Usage\n\n```kotlin\n// ✅ GOOD - Using theme\nText(\n    text = \"Title\",\n    style = BitwardenTheme.typography.titleLarge,\n    color = BitwardenTheme.colorScheme.primary\n)\n\n// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)\nSpacer(modifier = Modifier.height(16.dp))\n\n// ❌ BAD - Hardcoded\nText(\n    text = \"Title\",\n    style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold),  // Should use theme\n    color = Color(0xFF0000FF)  // Should use theme color\n)\n\nSpacer(modifier = Modifier.height(17.dp))  // Non-standard spacing\n```\n\n### Accessibility\n\n```kotlin\n// ✅ GOOD - Interactive element with description\nIcon(\n    painter = painterResource(R.drawable.ic_password),\n    contentDescription = \"Password visibility toggle\",\n    modifier = Modifier.clickable { onToggle() }\n)\n\n// ✅ GOOD - Decorative icon with explicit null\nIcon(\n    painter = painterResource(R.drawable.ic_check),\n    contentDescription = null,  // Decorative icon next to descriptive text\n    tint = BitwardenTheme.colorScheme.success\n)\n\n// ❌ BAD - Interactive element missing description\nIcon(\n    painter = painterResource(R.drawable.ic_delete),\n    contentDescription = null,  // Interactive elements need descriptions\n    modifier = Modifier.clickable { onDelete() }\n)\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n## Summary\nUpdates login screen layout for improved visual hierarchy and touch targets\n\n## Critical Issues\nNone\n\n## Suggested Improvements\n\n**app/auth/LoginScreen.kt:67** - Can we use BitwardenTextField?\nThis custom text field looks very similar to `ui/components/BitwardenTextField.kt:89`.\nWould using the existing component maintain consistency?\n\n**app/auth/LoginScreen.kt:123** - Add contentDescription\n```kotlin\nIcon(\n    painter = painterResource(R.drawable.ic_visibility),\n    contentDescription = \"Show password\",  // Add for accessibility\n    modifier = Modifier.clickable { onToggleVisibility() }\n)\n```\n\n**app/auth/LoginScreen.kt:145** - Use design system spacing\n```kotlin\n// Current\nSpacer(modifier = Modifier.height(17.dp))\n\n// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)\nSpacer(modifier = Modifier.height(16.dp))\n```\n```\n",".claude/skills/reviewing-changes/examples/review-outputs.md":"# Review Output Examples\n\nWell-structured code reviews demonstrating appropriate depth, tone, and formatting for different change types.\n\n## Table of Contents\n\n**Format Reference:**\n- [Quick Format Reference](#quick-format-reference)\n  - [Inline Comment Format](#inline-comment-format-required)\n  - [Summary Comment Format](#summary-comment-format)\n\n**Examples:**\n- [Example 1: Clean PR (No Issues)](#example-1-clean-pr-no-issues)\n- [Example 2: Dependency Update with Breaking Changes](#example-2-dependency-update-with-breaking-changes)\n- [Example 3: Feature Addition with Critical Issues](#example-3-feature-addition-with-critical-issues)\n\n**Anti-Patterns:**\n- [❌ Anti-Patterns to Avoid](#-anti-patterns-to-avoid)\n  - [Problem: Verbose Summary with Multiple Sections](#problem-verbose-summary-with-multiple-sections)\n  - [Problem: Praise-Only Inline Comments](#problem-praise-only-inline-comments)\n  - [Problem: Missing `<details>` Tags](#problem-missing-details-tags)\n\n**Summary:**\n- [Summary](#summary)\n\n---\n\n## Quick Format Reference\n\n### Inline Comment Format (REQUIRED)\n\n**MUST use `<details>` tags.** Only severity + description visible; all other content collapsed.\n\n```\n[emoji] **[SEVERITY]**: [One-line issue description]\n\n<details>\n<summary>Details and fix</summary>\n\n[Code example or specific fix]\n\n[Rationale explaining why]\n\nReference: [docs link if applicable]\n</details>\n```\n\n**Severity Levels:**\n- ❌ **CRITICAL** - Blocking, must fix (security, crashes, architecture violations)\n- ⚠️ **IMPORTANT** - Should fix (missing tests, quality issues)\n- ♻️ **DEBT** - Technical debt (duplication, convention violations, future rework needed)\n- 🎨 **SUGGESTED** - Nice to have (refactoring, improvements)\n- ❓ **QUESTION** - Seeking clarification (requirements, design decisions)\n\n### Summary Comment Format\n\nUses the agent's `posting-review-summary` skill format. Surface ❌ CRITICAL issues at the top level for immediate visibility, wrap the full findings list in `<details>` for scannability.\n\n```\n**Overall Assessment:** APPROVE / REQUEST CHANGES\n\n[1-2 neutral sentences describing what was reviewed]\n\n**Critical Issues** (if any):\n- ❌ [One-line summary with file:line]\n\n<details>\n<summary>All findings</summary>\n\n- ❌ **CRITICAL**: [description] (`file:line`)\n- ⚠️ **IMPORTANT**: [description] (`file:line`)\n- ♻️ **DEBT**: [description] (`file:line`)\n- 🎨 **SUGGESTED**: [description] (`file:line`)\n- ❓ **QUESTION**: [description] (`file:line`)\n</details>\n```\n\nFor clean PRs with no findings, omit both sections entirely — verdict + 1-2 sentences is sufficient.\n\n**GitHub pitfall**: Never use `#` followed by a number in comment text (e.g., `#42`, `#PR123`). GitHub autolinks these to issues/PRs. Use `Finding 1:` or `item 42` instead.\n\n---\n\n## Example 1: Clean PR (No Issues)\n\n**Context**: Moving shared code to common module, complete migration, all patterns followed\n\n**Review Comment:**\n```markdown\n**Overall Assessment:** APPROVE\n\nClean refactoring that moves ExitManager to :ui module, eliminating duplication between apps.\n```\n\n**Why this works:**\n- Immediate approval visible (2-3 lines)\n- One sentence acknowledging the work\n- No unnecessary sections or elaborate praise\n- Author gets quick feedback and can proceed\n\n---\n\n## Example 2: Dependency Update with Breaking Changes\n\n**Context**: Major version update requiring code migration\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- API migration required for Retrofit 3.0 breaking changes (network/api/BitwardenApiService.kt:34)\n\nSee inline comments for migration details.\n```\n\n**Inline Comment 1** (on `network/api/BitwardenApiService.kt:34`):\n```markdown\n❌ **CRITICAL**: API migration required for Retrofit 3.0\n\n<details>\n<summary>Details and fix</summary>\n\nRetrofit 3.0 removes the `Call<T>` return type. All 12 API methods in this file need migration:\n\n```kotlin\n// Current (deprecated in Retrofit 3.0)\n@GET(\"api/accounts/profile\")\nfun getProfile(): Call<ProfileResponse>\n\n// Must migrate to\n@GET(\"api/accounts/profile\")\nsuspend fun getProfile(): Response<ProfileResponse>\n```\n\nBreaking API change affects:\n- 12 methods in BitwardenApiService\n- 8 methods in VaultApiService\n- All call sites using enqueue/execute\n- Test utilities\n\nConsider creating separate PR for this migration given the scope.\n\nReference: [Retrofit 3.0 migration guide](https://square.github.io/retrofit/changelogs/changelog-3.x/)\n</details>\n```\n\n**Key Features:**\n- Minimal summary (2-3 lines)\n- Full details in collapsed inline comment\n- Specific file:line references\n- Code examples in <details>\n- Migration guidance and scope assessment\n\n---\n\n## Example 3: Feature Addition with Critical Issues\n\n**Context**: Implements PIN unlock for vault access\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Exposes mutable state violating MVVM (UnlockViewModel.kt:78)\n- PIN stored without encryption - SECURITY ISSUE (UnlockRepository.kt:145)\n\nSee inline comments for all issues and suggestions.\n```\n\n**Inline Comment 1** (on `app/vault/unlock/UnlockViewModel.kt:78`):\n```markdown\n❌ **CRITICAL**: Exposes mutable state\n\n<details>\n<summary>Details and fix</summary>\n\nChange `MutableStateFlow<State>` to `StateFlow<State>`:\n\n```kotlin\n// Current (problematic)\nval unlockState: MutableStateFlow<UnlockState>\n\n// Should be\nprivate val _unlockState = MutableStateFlow<UnlockState>()\nval unlockState: StateFlow<UnlockState> = _unlockState.asStateFlow()\n```\n\nExposing MutableStateFlow allows external mutation, violating MVVM unidirectional data flow.\n\nReference: docs/ARCHITECTURE.md#mvvm-pattern\n</details>\n```\n\n**Inline Comment 2** (on `data/vault/UnlockRepository.kt:145`):\n```markdown\n❌ **CRITICAL**: PIN stored without encryption - SECURITY ISSUE\n\n<details>\n<summary>Details and fix</summary>\n\nStoring PIN in plaintext SharedPreferences exposes it to backup systems and rooted devices.\n\n```kotlin\n// Current (CRITICAL SECURITY ISSUE)\nsharedPreferences.edit {\n    putString(KEY_PIN, pin)\n}\n\n// Must use Android Keystore encryption\nsuspend fun storePin(pin: String): Result<Unit> = runCatching {\n    val encrypted = keystoreManager.encrypt(pin.toByteArray())\n    encryptedPrefs.putBytes(KEY_PIN, encrypted)\n}\n```\n\nUse Android Keystore encryption or EncryptedSharedPreferences per security architecture.\n\nReference: docs/ARCHITECTURE.md#security\n</details>\n```\n\n**Inline Comment 3** (on `app/vault/unlock/UnlockViewModel.kt:92`):\n```markdown\n⚠️ **IMPORTANT**: Missing error handling test\n\n<details>\n<summary>Details and fix</summary>\n\nAdd test to prevent regression if error handling changes:\n\n```kotlin\n@Test\nfun `when incorrect PIN entered then returns error state`() = runTest {\n    val viewModel = UnlockViewModel(mockRepository)\n    coEvery { mockRepository.validatePin(\"1234\") }\n        returns Result.failure(InvalidPinException())\n\n    viewModel.onPinEntered(\"1234\")\n\n    assertEquals(UnlockState.Error(\"Invalid PIN\"), viewModel.state.value)\n}\n```\n\nEnsures error flow remains robust across refactorings.\n</details>\n```\n\n**Inline Comment 4** (on `app/vault/unlock/UnlockViewModel.kt:105`):\n```markdown\n🎨 **SUGGESTED**: Consider rate limiting for PIN attempts\n\n<details>\n<summary>Details and fix</summary>\n\nCurrently allows unlimited attempts, which could enable brute force attacks.\n\n```kotlin\nprivate var attemptCount = 0\nprivate var lockoutUntil: Instant? = null\n\nfun onPinEntered(pin: String) {\n    if (isLockedOut()) {\n        _state.value = UnlockState.LockedOut(lockoutUntil!!)\n        return\n    }\n    // ... validate PIN ...\n    if (invalid) {\n        attemptCount++\n        if (attemptCount >= MAX_ATTEMPTS) {\n            lockoutUntil = clock.millis() + 15.minutes\n        }\n    }\n}\n```\n\nWould add security layer against brute force. Consider discussing threat model with security team.\n</details>\n```\n\n**Inline Comment 5** (on `app/vault/unlock/UnlockScreen.kt:134`):\n```markdown\n❓ **QUESTION**: Can we use BitwardenTextField?\n\n<details>\n<summary>Details</summary>\n\nThis custom PIN input field looks similar to `ui/components/BitwardenTextField.kt:67`.\n\nWould using the existing component maintain consistency and reduce custom UI code?\n</details>\n```\n\n**Key Features:**\n- Minimal summary (3-4 lines) with critical issues only\n- Each issue gets separate inline comment with `<details>` tag\n- Multiple severity levels demonstrated (CRITICAL, IMPORTANT, SUGGESTED, QUESTION)\n- Mix of prescriptive fixes and collaborative questions\n- Code examples collapsed in <details>\n- No \"Good Practices\" or \"Action Items\" sections\n\n---\n\n## ❌ Anti-Patterns to Avoid\n\n### Problem: Verbose Summary with Multiple Sections\n\n**What NOT to do:**\n```markdown\n### Review Complete ✅\n\n## Summary\n[Lengthy description of what the PR does]\n\n### Strengths 👍\n1. **Excellent documentation** - KDoc comments are comprehensive\n2. **Proper fail-closed design** - Security defaults to rejection\n3. **Defense in depth** - Multiple validation layers\n[7 total items with elaboration]\n\n### Critical Issues ⚠️\n- Missing test coverage for security-critical code (with full details)\n- [More issues with full explanations]\n\n### Recommendations 🎨\n- [Multiple recommendations]\n\n### Test Coverage Status 📊\n- [Analysis]\n\n### Architecture Compliance ✅\n- [Analysis]\n\n## Recommendation\n**Conditional approval** with follow-up...\n```\n\n**Why this is wrong:**\n- 800+ tokens for a summary comment\n- Multiple sections (Strengths, Recommendations, Test Coverage, Architecture)\n- Elaborates on positive aspects (\"Excellent documentation...\")\n- Duplicates critical issues (summary has details + inline comments have same details)\n- Creates visual clutter in PR conversation\n\n**Correct approach:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Missing test coverage for security-critical code (PasswordManagerSignatureVerifierImpl.kt:47)\n\nSee inline comments for details.\n```\n\n**Key differences:**\n- 3-5 lines vs 800+ tokens\n- Verdict + critical issues only\n- All details belong in inline comments\n- No positive commentary sections\n- Scales with PR complexity, not analysis thoroughness\n\n### Problem: Praise-Only Inline Comments\n\n**What NOT to do:**\n\nCreating inline comment on `AuthenticatorBridgeManagerImpl.kt:73`:\n```markdown\n👍 **Excellent integration of signature verification**\n\nThe signature verification is properly integrated into the connection flow:\n- Checked during initialization (line 73)\n- Checked before binding (line 134)\n- Ensures only validated apps can connect\n\nThis is exactly the right approach for fail-safe security.\n```\n\n**Why this is wrong:**\n- Entire comment is positive feedback with no actionable issue\n- Takes up space in PR conversation\n- Distracts from actual issues\n- Violates \"focus on actionable feedback\" principle\n\n**Correct approach:**\n- Do not create this comment at all\n- Reserve inline comments exclusively for issues requiring attention\n\n### Problem: Missing `<details>` Tags\n\n**What NOT to do:**\n\n```markdown\n❌ **CRITICAL**: Missing test coverage for security-critical code\n\nThe `@OmitFromCoverage` annotation excludes this entire class from test coverage.\n\n**Problems:**\n1. No validation that certificate hashes match actual Bitwarden certificates\n2. No verification of fail-closed behavior on edge cases\n3. No tests for multiple signer rejection logic\n4. Certificate hash typos would go undetected until production\n\n**Recommendation:**\nReplace `@OmitFromCoverage` with proper unit tests.\n\nExample test structure:\n[long code block]\n\nSecurity-critical code should have the highest test coverage, not be omitted.\n```\n\n**Why this is wrong:**\n- All content visible immediately (code examples, problems list, rationale)\n- Creates visual clutter in PR conversation\n- Makes it hard to scan multiple issues quickly\n\n**Correct approach:**\n```markdown\n❌ **CRITICAL**: Missing test coverage for security-critical code\n\n<details>\n<summary>Details and fix</summary>\n\nThe `@OmitFromCoverage` annotation excludes this entire class from test coverage.\n\n**Problems:**\n1. No validation that certificate hashes match actual Bitwarden certificates\n2. No verification of fail-closed behavior on edge cases\n3. No tests for multiple signer rejection logic\n4. Certificate hash typos would go undetected until production\n\n**Recommendation:**\nReplace `@OmitFromCoverage` with proper unit tests.\n\nExample test structure:\n[code block]\n\nSecurity-critical code should have the highest test coverage, not be omitted.\n</details>\n```\n\n**Key difference:** Only severity + one-line description visible. All details collapsed.\n\n---\n\n## Summary\n\n**Always use:**\n- Minimal summary (verdict + critical issues)\n- Separate inline comments with `<details>` tags\n- Hybrid emoji + text severity prefixes\n- Focus exclusively on actionable feedback\n\n**Never use:**\n- Multiple summary sections (Strengths, Recommendations, etc.)\n- Praise-only inline comments\n- Duplication between summary and inline comments\n- Verbose analysis in summary (belongs in inline comments)\n",".claude/skills/reviewing-changes/reference/architectural-patterns.md":"# Architectural Patterns Quick Reference\n\nQuick reference for Bitwarden Android architectural patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## Table of Contents\n\n**Core Patterns:**\n- [MVVM + UDF Pattern](#mvvm--udf-pattern)\n  - [ViewModel Structure](#viewmodel-structure)\n  - [UI Layer (Compose)](#ui-layer-compose)\n- [Hilt Dependency Injection](#hilt-dependency-injection)\n  - [ViewModels](#viewmodels)\n  - [Repositories and Managers](#repositories-and-managers)\n  - [Clock/Time Handling](#clocktime-handling)\n- [Module Organization](#module-organization)\n- [Error Handling](#error-handling)\n  - [Use Result Types, Not Exceptions](#use-result-types-not-exceptions)\n- [Quick Checklist](#quick-checklist)\n\n---\n\n## MVVM + UDF Pattern\n\n### ViewModel Structure\n\n**✅ GOOD - Proper state encapsulation**:\n```kotlin\n@HiltViewModel\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository\n) : ViewModel() {\n    // Private mutable state\n    private val _state = MutableStateFlow<FeatureState>(FeatureState.Initial)\n\n    // Public immutable state\n    val state: StateFlow<FeatureState> = _state.asStateFlow()\n\n    // Actions as functions, state updated via internal action\n    fun onActionClicked() {\n        viewModelScope.launch {\n            val result = repository.performAction()\n            sendAction(FeatureAction.Internal.ActionComplete(result))\n        }\n    }\n\n    // The ViewModel has a handler that processes the internal action\n    private fun handleInternalAction(action: FeatureAction.Internal) {\n        when (action) {\n            is FeatureAction.Internal.ActionComplete -> {\n                // The action handler evaluates the result and updates state\n                action.result.fold(\n                    onSuccess = { _state.value = State.Success },\n                    onFailure = { _state.value = State.Error(it) }\n                )\n            }\n        }\n    }\n}\n```\n\n**❌ BAD - Common violations**:\n```kotlin\nclass FeatureViewModel : ViewModel() {\n    // ❌ Exposes mutable state\n    val state: MutableStateFlow<FeatureState>\n\n    // ❌ Business logic in ViewModel\n    fun onSubmit() {\n        val encrypted = encryptionManager.encrypt(data)  // Should be in Repository\n        _state.value = FeatureState.Success\n    }\n\n    // ❌ Direct Android framework dependency\n    fun onCreate(context: Context) {  // ViewModels shouldn't depend on Context\n        // ...\n    }\n}\n```\n\n**Key Rules**:\n- Expose `StateFlow<T>`, never `MutableStateFlow<T>`\n- Delegate business logic to Repository/Manager\n- No direct Android framework dependencies (except ViewModel, SavedStateHandle)\n- Use `viewModelScope` for coroutines\n\nReference: `docs/ARCHITECTURE.md#mvvm-pattern`\n\n---\n\n### UI Layer (Compose)\n\n**✅ GOOD - Stateless, observes only**:\n```kotlin\n@Composable\nfun FeatureScreen(\n    state: FeatureState,\n    onActionClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    Column(modifier = modifier) {\n        when (state) {\n            is FeatureState.Loading -> LoadingIndicator()\n            is FeatureState.Success -> SuccessContent(state.data)\n            is FeatureState.Error -> ErrorMessage(state.error)\n        }\n\n        BitwardenButton(\n            text = \"Action\",\n            onClick = onActionClick  // Sends event to ViewModel\n        )\n    }\n}\n```\n\n**❌ BAD - Stateful, modifies state**:\n```kotlin\n@Composable\nfun FeatureScreen(viewModel: FeatureViewModel) {\n    var localState by remember { mutableStateOf(...) }  // ❌ State in UI\n\n    Button(onClick = {\n        viewModel._state.value = FeatureState.Loading  // ❌ Directly modifying ViewModel state\n    })\n}\n```\n\n**Key Rules**:\n- Compose screens observe state, never modify\n- User actions passed as events/callbacks to ViewModel\n- No business logic in UI layer\n- Use existing components from `:ui` module\n\n---\n\n## Hilt Dependency Injection\n\n### ViewModels\n\n**✅ GOOD - Interface injection**:\n```kotlin\n@HiltViewModel\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository,  // Interface, not implementation\n    private val authManager: AuthManager,\n    savedStateHandle: SavedStateHandle\n) : ViewModel()\n```\n\n**❌ BAD - Common violations**:\n```kotlin\n// ❌ No @HiltViewModel annotation\nclass FeatureViewModel @Inject constructor(...)\n\n// ❌ Injecting implementation instead of interface\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepositoryImpl  // Should inject interface\n)\n\n// ❌ Manual instantiation\nclass FeatureViewModel : ViewModel() {\n    private val repository = FeatureRepositoryImpl()  // Should use @Inject\n}\n```\n\n**Key Rules**:\n- Annotate with `@HiltViewModel`\n- Use `@Inject constructor`\n- Inject interfaces, not implementations\n- Use `SavedStateHandle` for process death survival\n\nReference: `docs/ARCHITECTURE.md#dependency-injection`\n\n---\n\n### Repositories and Managers\n\n**✅ GOOD - Implementation with @Inject**:\n```kotlin\ninterface FeatureRepository {\n    suspend fun fetchData(): Result<Data>\n}\n\nclass FeatureRepositoryImpl @Inject constructor(\n    private val apiService: FeatureApiService,\n    private val database: FeatureDao\n) : FeatureRepository {\n    override suspend fun fetchData(): Result<Data> = runCatching {\n        apiService.getData()\n    }\n}\n```\n\n**Module provides interface**:\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nabstract class DataModule {\n    @Binds\n    @Singleton\n    abstract fun bindFeatureRepository(\n        impl: FeatureRepositoryImpl\n    ): FeatureRepository\n}\n```\n\n**Key Rules**:\n- Define interface for abstraction\n- Implementation uses `@Inject constructor`\n- Module binds implementation to interface\n- Appropriate scoping (`@Singleton`, `@ViewModelScoped`)\n\n---\n\n### Clock/Time Handling\n\nTime-dependent code must use injected `Clock` rather than direct `Instant.now()` or `DateTime.now()` calls. This follows the same DI principle as other dependencies.\n\n**✅ GOOD - Injected Clock**:\n```kotlin\n// ViewModel with Clock injection\nclass MyViewModel @Inject constructor(\n    private val clock: Clock,\n) {\n    fun save() {\n        val timestamp = clock.instant()\n    }\n}\n\n// Extension function with Clock parameter\nfun State.getTimestamp(clock: Clock): Instant =\n    existingTime ?: clock.instant()\n```\n\n**❌ BAD - Static/direct calls**:\n```kotlin\n// Hidden dependency, non-testable\nval timestamp = Instant.now()\nval dateTime = DateTime.now()\n```\n\n**Key Rules**:\n- Inject `Clock` via Hilt constructor (like other dependencies)\n- Pass `Clock` as parameter to extension functions\n- `Clock` is provided via `CoreModule` as singleton\n- Enables deterministic testing with `Clock.fixed(...)`\n\nReference: `docs/STYLE_AND_BEST_PRACTICES.md#best-practices--time-and-clock-handling`\n\n---\n\n## Module Organization\n\n```\nandroid/\n├── core/           # Shared utilities (cryptography, analytics, logging)\n├── data/           # Repositories, database, domain models\n├── network/        # API clients, network utilities\n├── ui/             # Reusable Compose components, theme\n├── app/            # Application, feature screens, ViewModels\n└── authenticator/  # Authenticator app (separate from password manager)\n```\n\n**Correct Placement**:\n- UI screens and ViewModels → `:app`\n- Reusable Compose components → `:ui`\n- Data models and Repositories → `:data`\n- API services → `:network`\n- Cryptography, logging → `:core`\n\n**Check for**:\n- No circular dependencies\n- Correct module placement\n- Proper visibility (internal vs public)\n\nReference: `docs/ARCHITECTURE.md#module-structure`\n\n---\n\n## Error Handling\n\n### Use Result Types, Not Exceptions\n\n**✅ GOOD - Result-based**:\n```kotlin\n// Repository\nsuspend fun fetchData(): Result<Data> = runCatching {\n    apiService.getData()\n}\n\n// ViewModel\nfun onFetch() {\n    viewModelScope.launch {\n        val result = repository.fetchData()\n        sendAction(FeatureAction.Internal.FetchComplete(result))\n    }\n}\n```\n\n**❌ BAD - Exception-based in business logic**:\n```kotlin\n// ❌ Don't throw in business logic\nsuspend fun fetchData(): Data {\n    try {\n        return apiService.getData()\n    } catch (e: Exception) {\n        throw FeatureException(e)  // Don't throw in repositories\n    }\n}\n\n// ❌ Try-catch in ViewModel\nfun onFetch() {\n    viewModelScope.launch {\n        try {\n            val data = repository.fetchData()\n            sendAction(FeatureAction.Internal.FetchComplete(data))\n        } catch (e: Exception) {\n            sendAction(FeatureAction.Internal.FetchComplete(e))\n        }\n    }\n}\n```\n\n**Key Rules**:\n- Use `Result<T>` return types in repositories\n- Use `runCatching { }` to wrap API calls\n- Handle results with `.fold()` in ViewModels\n- Don't throw exceptions in business logic\n\nReference: `docs/ARCHITECTURE.md#error-handling`\n\n---\n\n## Quick Checklist\n\n### Architecture\n- [ ] ViewModels expose StateFlow, not MutableStateFlow?\n- [ ] Business logic in Repository, not ViewModel?\n- [ ] Using Hilt DI (@HiltViewModel, @Inject constructor)?\n- [ ] Injecting interfaces, not implementations?\n- [ ] Time-dependent code uses injected `Clock` (not `Instant.now()`)?\n- [ ] Correct module placement?\n\n### Error Handling\n- [ ] Using Result types, not exceptions in business logic?\n- [ ] Errors handled with .fold() in ViewModels?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n",".claude/skills/reviewing-changes/reference/priority-framework.md":"# Finding Priority Framework\n\nUse this framework to classify findings during code review. Clear prioritization helps authors triage and address issues effectively.\n\n## Table of Contents\n\n**Severity Categories:**\n- [❌ CRITICAL (Blocker - Must Fix Before Merge)](#critical-blocker---must-fix-before-merge)\n- [⚠️ IMPORTANT (Should Fix)](#important-should-fix)\n- [♻️ DEBT (Technical Debt)](#debt-technical-debt)\n- [🎨 SUGGESTED (Nice to Have)](#suggested-nice-to-have)\n- [❓ QUESTION (Seeking Clarification)](#question-seeking-clarification)\n- [Optional (Acknowledge But Don't Require)](#optional-acknowledge-but-dont-require)\n\n**Guidelines:**\n- [Classification Guidelines](#classification-guidelines)\n  - [When Something is Between Categories](#when-something-is-between-categories)\n  - [Context Matters](#context-matters)\n- [Examples by Change Type](#examples-by-change-type)\n- [Special Cases](#special-cases)\n- [Summary](#summary)\n\n---\n\n## ❌ **CRITICAL** (Blocker - Must Fix Before Merge)\n\nThese issues **must** be addressed before the PR can be merged. They pose immediate risks to security, stability, or architecture integrity.\n\n### Security\n- Data leaks or plaintext sensitive data (passwords, keys, tokens)\n- Weak encryption or insecure key storage\n- Missing authentication or authorization checks\n- Input injection vulnerabilities (SQL, XSS, command injection)\n- Sensitive data in logs or error messages\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption\nPIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.\nReference: docs/ARCHITECTURE.md#security\n```\n\n### Stability\n- Compilation errors or warnings\n- Null pointer exceptions in production paths\n- Resource leaks (file handles, network connections, memory)\n- Crashes or unhandled exceptions in critical paths\n- Thread safety violations\n\n**Example**:\n```\n**app/auth/BiometricRepository.kt:120** - CRITICAL: Missing null safety check\nbiometricPrompt result can be null. Add explicit null check to prevent crash.\n```\n\n### Architecture\n- Mutable state exposure in ViewModels (violates MVVM)\n- Exception-based error handling in business logic (should use Result)\n- Circular dependencies between modules\n- Violation of zero-knowledge principles\n- Direct dependency instantiation (should use DI)\n\n**Example**:\n```\n**app/login/LoginViewModel.kt:45** - CRITICAL: Exposes mutable state\nChange MutableStateFlow to StateFlow in public API to prevent external state mutation.\nThis violates MVVM encapsulation pattern.\n```\n\n---\n\n## ⚠️ **IMPORTANT** (Should Fix)\n\nThese issues should be addressed but don't block merge if there's a compelling reason. They improve code quality, maintainability, or robustness.\n\n### Testing\n- Missing tests for critical paths (authentication, encryption, data sync)\n- Missing tests for new public APIs\n- Tests that don't verify actual behavior (test implementation, not behavior)\n- Missing test coverage for error scenarios\n\n**Example**:\n```\n**data/auth/BiometricRepository.kt** - IMPORTANT: Missing test for cancellation\nAdd test for user cancellation scenario to prevent regression.\n```\n\n### Architecture\n- Inconsistent patterns within PR (mixing error handling approaches)\n- Poor separation of concerns\n- Tight coupling between components\n- Not following established project patterns\n\n**Example**:\n```\n**app/vault/VaultViewModel.kt:89** - IMPORTANT: Business logic in ViewModel\nEncryption logic should be in Repository, not ViewModel.\nReference: docs/ARCHITECTURE.md#mvvm-pattern\n```\n\n### Documentation\n- Undocumented public APIs (missing KDoc)\n- Missing documentation for complex algorithms\n- Unclear naming or confusing interfaces\n\n**Example**:\n```\n**core/crypto/EncryptionManager.kt:34** - IMPORTANT: Missing KDoc\nPublic encryption method should document parameters, return value, and exceptions.\n```\n\n### Performance\n- Inefficient algorithms in hot paths (with evidence from profiling)\n- Blocking main thread with I/O operations\n- Memory inefficient data structures (with evidence)\n\n**Example**:\n```\n**app/vault/VaultListViewModel.kt:78** - IMPORTANT: N+1 query pattern\nFetching items one-by-one in loop. Consider batch fetch to reduce database queries.\n```\n\n---\n\n## ♻️ **DEBT** (Technical Debt)\n\nCode that duplicates existing patterns, violates established conventions, or will require rework within 6 months. Introduces technical debt that should be tracked for future cleanup.\n\n### Duplication\n- Copy-pasted code blocks across files\n- Repeated validation or business logic\n- Multiple implementations of same pattern\n- Data transformation duplicated in multiple places\n\n**Example**:\n```\n**app/vault/VaultListViewModel.kt:156** - DEBT: Duplicates encryption logic\nSame encryption pattern exists in VaultRepository.kt:234 and SyncManager.kt:89.\nExtract to shared EncryptionUtil to reduce maintenance burden.\n```\n\n### Convention Violations\n- Inconsistent error handling approaches within same module\n- Mixing architectural patterns (MVVM + MVC)\n- Not following established DI patterns\n- Deviating from project code style significantly\n\n**Example**:\n```\n**data/auth/AuthRepository.kt:78** - DEBT: Exception-based error handling\nProject standard is Result<T> for error handling. This uses try-catch with throws.\nCreates inconsistency and makes testing harder.\nReference: docs/ARCHITECTURE.md#error-handling\n```\n\n### Future Rework Required\n- Hardcoded values that should be configurable\n- Temporary workarounds without TODO/FIXME\n- Code that will need changes when planned features arrive\n- Tight coupling that prevents future extensibility\n\n**Example**:\n```\n**app/settings/SettingsViewModel.kt:45** - DEBT: Hardcoded feature flags\nFeature flags should come from remote config for A/B testing.\nWill require rework when experimentation framework launches.\n```\n\n---\n\n## 🎨 **SUGGESTED** (Nice to Have)\n\nImprovements with measurable value only. A finding qualifies as SUGGESTED if it provides: security gain, cyclomatic complexity reduction, bug class prevention, or elimination of an O(n²) pattern. Subjective style preferences, vague simplifications, and naming nitpicks do not qualify — leave those out entirely or raise in conversation.\n\n### Code Quality\n- Extractable duplicated logic that reduces measurable complexity or improves testability\n- Patterns that would prevent a recurring bug class in this module\n- Architecture improvements that eliminate tight coupling with measurable impact\n\n**Example**:\n```\n**app/vault/VaultScreen.kt:145** - SUGGESTED: Consider extracting helper function\nThis 20-line block appears in 3 places. Consider extracting to reduce duplication.\n```\n\n### Testing\n- Additional test coverage for edge cases (beyond critical paths)\n- More comprehensive integration tests\n- Performance tests for non-critical paths\n\n**Example**:\n```\n**app/login/LoginViewModelTest.kt** - SUGGESTED: Add test for concurrent login attempts\nNot critical, but would increase confidence in edge case handling.\n```\n\n### Refactoring\n- Extracting reusable patterns\n- Modernizing old patterns (if touching related code)\n- Improving testability\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:200** - SUGGESTED: Consider extracting validation logic\nCould be extracted to separate validator class for reusability and testing.\n```\n\n---\n\n## ❓ **QUESTION** (Seeking Clarification)\n\nQuestions about requirements, unclear intent, or potential conflicts that require human knowledge to answer. Open inquiries that cannot be resolved through code inspection alone.\n\n### Requirements Clarification\n- Ambiguous acceptance criteria\n- Multiple valid implementation approaches\n- Unclear business rules or edge case handling\n- Conflicting requirements between specs and implementation\n\n**Example**:\n```\n**app/vault/ItemListViewModel.kt:67** - QUESTION: Expected sort behavior for equal timestamps?\nWhen items have identical timestamps, should secondary sort be by:\n- Name (alphabetical)\n- Creation order\n- Item type priority\n\nSpec doesn't specify tie-breaking logic.\n```\n\n### Design Decisions\n- Architecture choices that could go multiple ways\n- Trade-offs between approaches without clear winner\n- Feature flag strategy or rollout approach\n- API design with multiple valid patterns\n\n**Example**:\n```\n**data/sync/SyncManager.kt:134** - QUESTION: Should sync failures retry automatically?\nCurrent implementation fails immediately. Options:\n- Exponential backoff (3 retries)\n- User-triggered retry only\n- Background retry on network restore\n\nWhat's the expected UX?\n```\n\n### System Integration\n- Unclear contracts with external systems\n- Potential conflicts with other features/modules\n- Assumptions about third-party API behavior\n- Cross-team coordination needs\n\n**Example**:\n```\n**app/auth/BiometricPrompt.kt:89** - QUESTION: Compatibility with pending device credentials PR?\nPR #1234 is refactoring device credentials. Should this:\n- Merge first and adapt later\n- Wait for #1234 to land\n- Coordinate with that author\n\nTiming unclear.\n```\n\n### Testing Strategy\n- Uncertainty about test scope or approach\n- Questions about mocking external dependencies\n- Edge cases that need product input\n- Performance testing requirements\n\n**Example**:\n```\n**data/vault/EncryptionTest.kt:45** - QUESTION: Should we test against real Keystore?\nCurrently using mocked Keystore. Real Keystore testing would:\n+ Catch hardware-specific issues\n- Slow down CI significantly\n- Require API 23+ emulators\n\nWhat's the priority?\n```\n\n---\n\n## Optional (Acknowledge But Don't Require)\n\nNote good practices to reinforce positive patterns. Keep these **brief** - list only, no elaboration.\n\n### Good Practices\n\n**Format**: Simple bullet list, no explanation\n\n```markdown\n## Good Practices\n- Proper Hilt DI usage throughout\n- Comprehensive unit test coverage\n- Clear separation of concerns\n- Well-documented public APIs\n```\n\n**Don't do this** (too verbose):\n```markdown\n## Good Practices\n- Proper Hilt DI usage throughout: Great job using @Inject constructor and injecting interfaces! This follows our established patterns perfectly and makes the code very testable. Really excellent work here! 👍\n```\n\n---\n\n## Classification Guidelines\n\n### When Something is Between Categories\n\n**If unsure between Critical and Important**:\n- Ask: \"Could this cause production incidents, data loss, or security breaches?\"\n- If yes → Critical\n- If no → Important\n\n**If unsure between Important and Debt**:\n- Ask: \"Is this a bug/defect or just duplication/inconsistency?\"\n- If bug/defect → Important\n- If duplication/inconsistency → Debt\n\n**If unsure between Important and Suggested**:\n- Ask: \"Would I block merge over this?\"\n- If yes → Important\n- If no → Suggested\n\n**If unsure between Debt and Suggested**:\n- Ask: \"Will this require rework within 6 months?\"\n- If yes → Debt\n- If no → Suggested\n\n**If unsure between Suggested and Question**:\n- Ask: \"Am I requesting a change or asking for clarification?\"\n- If requesting change → Suggested\n- If seeking clarification → Question\n\n**If unsure between Suggested and Optional**:\n- Ask: \"Is this actionable feedback or just acknowledgment?\"\n- If actionable → Suggested\n- If acknowledgment → Optional\n\n### Context Matters\n\n**Same issue, different contexts**:\n\n```\n// Critical for production code\nMissing null safety check in auth flow → CRITICAL\n\n// Suggested for internal test utility\nMissing null safety check in test helper → SUGGESTED\n```\n\n**Same pattern, different risk levels**:\n\n```\n// Critical for new feature\nMissing tests for new auth method → CRITICAL\n\n// Important for bug fix\nMissing regression test → IMPORTANT\n\n// Suggested for refactoring\nMissing tests for refactored helper → SUGGESTED\n```\n\n---\n\n## Examples by Change Type\n\n### Dependency Update\n- **Critical**: Known CVEs in old version not addressed\n- **Important**: Breaking changes that need migration\n- **Suggested**: Beta/alpha version stability concerns\n\n### Bug Fix\n- **Critical**: Fix doesn't address root cause\n- **Important**: Missing regression test\n- **Suggested**: Similar bugs in related code\n\n### Feature Addition\n- **Critical**: Security vulnerabilities, architecture violations\n- **Important**: Missing tests for critical paths\n- **Suggested**: Additional test coverage, minor refactoring\n\n### UI Refinement\n- **Critical**: Missing accessibility for key actions\n- **Important**: Not using theme (hardcoded colors)\n- **Suggested**: Minor spacing/alignment improvements\n\n### Refactoring\n- **Critical**: Changes behavior (should be behavior-preserving)\n- **Important**: Incomplete migration (mix of old/new patterns)\n- **Suggested**: Additional instances that could be refactored\n\n### Infrastructure\n- **Critical**: Hardcoded secrets, no rollback plan\n- **Important**: Performance regression in build times\n- **Suggested**: Further optimization opportunities\n\n---\n\n## Special Cases\n\n### Technical Debt\n- Acknowledge existing tech debt but don't require fixing in unrelated PR\n- Exception: If change makes tech debt worse, it's Important to address\n\n### Scope Creep\n- Don't request changes outside PR scope\n- Can note as \"Future consideration\" but not required for this PR\n\n### Linter-Catchable Issues\n- Don't flag issues that automated tools handle\n- Exception: If linter is misconfigured and missing real issues\n\n### Personal Preferences\n- Don't flag unless grounded in project standards or architectural principles\n- Use \"I-statements\" if suggesting alternative approaches\n\n---\n\n## Summary\n\n**Critical**: Block merge, must fix (security, stability, architecture)\n**Important**: Should fix before merge (testing, quality, performance)\n**Debt**: Technical debt introduced, track for future cleanup\n**Suggested**: Nice to have, consider effort vs benefit\n**Question**: Seeking clarification on requirements or design\n**Optional**: Acknowledge good practices, keep brief\n",".claude/skills/reviewing-changes/reference/review-psychology.md":"# Review Psychology: Constructive Feedback Phrasing\n\nEffective code review feedback is clear, actionable, and constructive. This guide provides phrasing patterns for inline comments.\n\n## Table of Contents\n\n**Guidelines:**\n- [Phrasing Templates](#phrasing-templates)\n  - [Critical Issues (Prescriptive)](#critical-issues-prescriptive)\n  - [Suggested Improvements (Exploratory)](#suggested-improvements-exploratory)\n  - [Questions (Collaborative)](#questions-collaborative)\n  - [Test Suggestions](#test-suggestions)\n- [When to Be Prescriptive vs Ask Questions](#when-to-be-prescriptive-vs-ask-questions)\n- [Special Cases](#special-cases)\n\n---\n\n## Phrasing Templates\n\n### Critical Issues (Prescriptive)\n\n**Pattern**: State problem + Provide solution + Explain why\n\n```\n**[file:line]** - CRITICAL: [Issue description]\n\n[Specific fix with code example if applicable]\n\n[Rationale explaining why this is critical]\n\nReference: [docs link if applicable]\n```\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption\n\nPIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.\nPlaintext storage exposes the PIN to backup systems and rooted devices.\n\nReference: docs/ARCHITECTURE.md#security\n```\n\n---\n\n### Suggested Improvements (Exploratory)\n\n**Pattern**: Observe + Suggest + Explain benefit\n\n```\n**[file:line]** - Consider [alternative approach]\n\n[Current observation]\nCan we [specific suggestion]?\n\n[Benefit or rationale]\n```\n\n**Example**:\n```\n**app/login/LoginScreen.kt:89** - Consider using existing BitwardenButton\n\nThis custom button implementation looks similar to `ui/components/BitwardenButton.kt:45`.\nCan we use the existing component to maintain consistency across the app?\n```\n\n---\n\n### Questions (Collaborative)\n\n**Pattern**: Ask + Provide context (optional)\n\n```\n**[file:line]** - [Question about intent or approach]?\n\n[Optional context or observation]\n```\n\n**Example**:\n```\n**data/sync/SyncManager.kt:234** - How does this handle concurrent sync attempts?\n\nIt looks like multiple coroutines could call `startSync()` simultaneously.\nIs there a mechanism to prevent race conditions, or is that handled elsewhere?\n```\n\n---\n\n### Test Suggestions\n\n**Pattern**: Observe gap + Suggest specific test + Provide skeleton\n\n```\n**[file:line]** - Consider adding test for [scenario]\n\n[Rationale]\n\n```kotlin\n@Test\nfun `test description`() = runTest {\n    // Test skeleton\n}\n```\n```\n\n**Example**:\n```\n**data/auth/BiometricRepository.kt** - Consider adding test for cancellation scenario\n\nThis would prevent regression of the bug you just fixed:\n\n```kotlin\n@Test\nfun `when biometric cancelled then returns cancelled state`() = runTest {\n    coEvery { biometricPrompt.authenticate() } returns null\n\n    val result = repository.authenticate()\n\n    assertEquals(AuthResult.Cancelled, result)\n}\n```\n```\n\n---\n\n## When to Be Prescriptive vs Ask Questions\n\n**Be Prescriptive** (Tell them what to do):\n- Security issues\n- Architecture pattern violations\n- Null safety problems\n- Compilation errors\n- Documented project standards\n\n**Ask Questions** (Seek explanation):\n- Design decisions with multiple valid approaches\n- Performance trade-offs without data\n- Unclear intent or reasoning\n- Scope decisions (this PR vs future work)\n- Patterns not documented in project guidelines\n\n---\n\n## Special Cases\n\n**Nitpicks** - For truly minor suggestions, use \"Nit:\" prefix:\n```\n**Nit**: Extra blank line at line 145\n```\n\n**Uncertainty** - If unsure, acknowledge it:\n```\nI'm not certain, but this might be called frequently.\nHas this been profiled?\n```\n\n**Positive Feedback** - Brief list only, no elaboration:\n```\n## Good Practices\n- Proper Hilt DI usage throughout\n- Comprehensive unit test coverage\n- Clear separation of concerns\n```\n",".claude/skills/reviewing-changes/reference/security-patterns.md":"# Security Patterns Quick Reference\n\nQuick reference for Bitwarden Android security patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md#security`.\n\n## Encryption and Key Storage\n\n**✅ GOOD - Android Keystore**:\n```kotlin\n// Sensitive data encrypted with Keystore\nclass SecureStorage @Inject constructor(\n    private val keystoreManager: KeystoreManager\n) {\n    suspend fun storePin(pin: String): Result<Unit> = runCatching {\n        val encrypted = keystoreManager.encrypt(pin.toByteArray())\n        securePreferences.putBytes(KEY_PIN, encrypted)\n    }\n}\n\n// Or use EncryptedSharedPreferences\nval encryptedPrefs = EncryptedSharedPreferences.create(\n    context,\n    \"secure_prefs\",\n    masterKey,\n    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n)\n```\n\n**❌ BAD - Plaintext or weak encryption**:\n```kotlin\n// ❌ CRITICAL - Plaintext storage\nsharedPreferences.edit {\n    putString(\"pin\", userPin)  // Never store sensitive data in plaintext\n}\n\n// ❌ CRITICAL - Weak encryption\nval cipher = Cipher.getInstance(\"DES\")  // Use AES-256-GCM\n\n// ❌ CRITICAL - Hardcoded keys\nval key = \"my_secret_key_123\"  // Use Android Keystore\n```\n\n**Key Rules**:\n- Use Android Keystore for encryption keys\n- Use EncryptedSharedPreferences for simple key-value storage\n- Use AES-256-GCM for encryption\n- Never store sensitive data in plaintext\n- Never hardcode encryption keys\n\nReference: `docs/ARCHITECTURE.md#security`\n\n---\n\n## Logging Sensitive Data\n\n**✅ GOOD - No sensitive data**:\n```kotlin\nLog.d(TAG, \"Authentication attempt for user\")\nLog.d(TAG, \"Vault sync completed with ${items.size} items\")\n```\n\n**❌ BAD - Logs sensitive data**:\n```kotlin\n// ❌ CRITICAL\nLog.d(TAG, \"Password: $password\")\nLog.d(TAG, \"Auth token: $token\")\nLog.d(TAG, \"PIN: $pin\")\nLog.d(TAG, \"Encryption key: ${key.encoded}\")\n```\n\n**Key Rules**:\n- Never log passwords, PINs, tokens, keys\n- Never log encryption keys or sensitive data\n- Be careful with error messages (don't include sensitive context)\n\n---\n\n## Quick Checklist\n\n### Security\n- [ ] Sensitive data encrypted with Keystore?\n- [ ] No plaintext passwords/keys?\n- [ ] No sensitive data in logs?\n- [ ] Using AES-256-GCM for encryption?\n- [ ] No hardcoded encryption keys?\n\n---\n\nFor comprehensive security details, always refer to:\n- `docs/ARCHITECTURE.md#security` - Complete security architecture and zero-knowledge principles\n",".claude/skills/reviewing-changes/reference/style-patterns.md":"# Style Patterns Quick Reference\n\nProject-specific Kotlin style rules to catch during code review. These supplement (not replace) `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## `when` branches with wrapped right-hand side require curly braces\n\nWhen a `when` branch's expression is too long to fit on the same line as `->` and is wrapped to its own line, the body must be wrapped in `{ }`. A bare `->` followed by an indented expression on the next line should be flagged.\n\n**Flag this:**\n\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT ->\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n}\n```\n\n**Accept this:**\n\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT -> {\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    }\n}\n```\n\nSingle-line branches (body fits alongside `->`) do **not** require braces.\n\n**Suggested classification:** SUGGESTED (style consistency, not correctness).\n",".claude/skills/reviewing-changes/reference/testing-patterns.md":"# Testing Patterns Quick Reference\n\nQuick reference for Bitwarden Android testing patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## ViewModel Tests\n\n**✅ GOOD - Tests behavior**:\n```kotlin\n@Test\nfun `when login succeeds then state updates to success`() = runTest {\n    // Arrange\n    val viewModel = LoginViewModel(mockRepository)\n    coEvery { mockRepository.login(any(), any()) } returns Result.success(User())\n\n    // Act\n    viewModel.onLoginClicked(\"user@example.com\", \"password\")\n\n    // Assert\n    viewModel.state.test {\n        assertEquals(LoginState.Loading, awaitItem())\n        assertEquals(LoginState.Success, awaitItem())\n    }\n}\n```\n\n**❌ BAD - Tests implementation**:\n```kotlin\n@Test\nfun `repository is called with correct parameters`() {\n    // ❌ This tests implementation details, not behavior\n    viewModel.onLoginClicked(\"user\", \"pass\")\n    coVerify { mockRepository.login(\"user\", \"pass\") }\n}\n```\n\n**Key Rules**:\n- Test behavior, not implementation\n- Use `runTest` for coroutine tests\n- Use Turbine for Flow testing\n- Use MockK for mocking\n\n---\n\n## Repository Tests\n\n**✅ GOOD - Tests data transformations**:\n```kotlin\n@Test\nfun `fetchItems maps API response to domain model`() = runTest {\n    // Arrange\n    val apiResponse = listOf(ApiItem(id = \"1\", name = \"Test\"))\n    coEvery { apiService.getItems() } returns apiResponse\n\n    // Act\n    val result = repository.fetchItems()\n\n    // Assert\n    assertTrue(result.isSuccess)\n    assertEquals(\n        listOf(DomainItem(id = \"1\", name = \"Test\")),\n        result.getOrThrow()\n    )\n}\n```\n\n**Key Rules**:\n- Test data transformations\n- Test error handling (network failures, API errors)\n- Test caching behavior if applicable\n- Mock API services and databases\n\nReference: Project uses JUnit 5, MockK, Turbine, kotlinx-coroutines-test\n\n---\n\n## Null Safety\n\n**✅ GOOD - Safe handling**:\n```kotlin\n// Safe call with elvis operator\nval result = apiService.getData() ?: return State.Error(\"No data\")\n\n// Let with safe call\nintent?.getStringExtra(\"key\")?.let { value ->\n    processValue(value)\n}\n\n// Require with message\nval data = requireNotNull(response.data) { \"Response data must not be null\" }\n```\n\n**❌ BAD - Unsafe assertions**:\n```kotlin\n// ❌ Unsafe - can crash\nval result = apiService.getData()!!\n\n// ❌ Platform type unchecked\nval intent: Intent = getIntent()  // Could be null from Java\nval value = intent.getStringExtra(\"key\")  // Potential NPE\n```\n\n**Key Rules**:\n- Avoid `!!` unless safety is guaranteed (rare)\n- Handle platform types with explicit nullability\n- Use safe calls (`?.`), elvis operator (`?:`), or explicit checks\n- Use `requireNotNull` with descriptive message if crash is acceptable\n\n---\n\n## Quick Checklist\n\n### Testing\n- [ ] ViewModels have unit tests?\n- [ ] Tests verify behavior, not implementation?\n- [ ] Edge cases covered?\n- [ ] Error scenarios tested?\n\n### Code Quality\n- [ ] Null safety handled properly (no `!!` without guarantee)?\n- [ ] Public APIs have KDoc?\n- [ ] Following naming conventions?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n",".claude/skills/reviewing-changes/reference/ui-patterns.md":"# Compose UI Patterns Quick Reference\n\nQuick reference for Bitwarden Android Compose UI patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## Component Reuse\n\n**✅ GOOD - Uses existing components**:\n```kotlin\nBitwardenButton(\n    text = \"Submit\",\n    onClick = onSubmit\n)\n\nBitwardenTextField(\n    value = text,\n    onValueChange = onTextChange,\n    label = \"Email\"\n)\n```\n\n**❌ BAD - Duplicates existing components**:\n```kotlin\n// ❌ Recreating BitwardenButton\nButton(\n    onClick = onSubmit,\n    colors = ButtonDefaults.buttonColors(\n        containerColor = BitwardenTheme.colorScheme.primary\n    )\n) {\n    Text(\"Submit\")\n}\n```\n\n**Key Rules**:\n- Check `:ui` module for existing components before creating custom ones\n- Use BitwardenButton, BitwardenTextField, etc. for consistency\n- Place new reusable components in `:ui` module\n\n---\n\n## Theme Usage\n\n**✅ GOOD - Uses theme**:\n```kotlin\nText(\n    text = \"Title\",\n    style = BitwardenTheme.typography.titleLarge,\n    color = BitwardenTheme.colorScheme.primary\n)\n\nSpacer(modifier = Modifier.height(16.dp))  // Standard spacing\n```\n\n**❌ BAD - Hardcoded values**:\n```kotlin\nText(\n    text = \"Title\",\n    style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold),  // Use theme\n    color = Color(0xFF0066FF)  // Use theme color\n)\n\nSpacer(modifier = Modifier.height(17.dp))  // Non-standard spacing\n```\n\n**Key Rules**:\n- Use `BitwardenTheme.colorScheme` for colors\n- Use `BitwardenTheme.typography` for text styles\n- Use standard spacing (4.dp, 8.dp, 16.dp, 24.dp)\n\n---\n\n## Quick Checklist\n\n### UI Patterns\n- [ ] Using existing Bitwarden components from `:ui` module?\n- [ ] Using BitwardenTheme for colors and typography?\n- [ ] Using standard spacing values (4, 8, 16, 24 dp)?\n- [ ] No hardcoded colors or text styles?\n- [ ] UI is stateless (observes state, doesn't modify)?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n",".claude/skills/testing-android-code/SKILL.md":"---\nname: testing-android-code\ndescription: This skill should be used when writing or reviewing tests for Android code in Bitwarden. Triggered by \"BaseViewModelTest\", \"BitwardenComposeTest\", \"BaseServiceTest\", \"stateEventFlow\", \"bufferedMutableSharedFlow\", \"FakeDispatcherManager\", \"expectNoEvents\", \"assertCoroutineThrows\", \"createMockCipher\", \"createMockSend\", \"asSuccess\", \"Why is my Bitwarden test failing?\", or testing questions about ViewModels, repositories, Compose screens, or data sources in Bitwarden.\nversion: 1.0.0\n---\n\n# Testing Android Code - Bitwarden Testing Patterns\n\n**This skill provides tactical testing guidance for Bitwarden-specific patterns.** For comprehensive architecture and testing philosophy, consult `docs/ARCHITECTURE.md`.\n\n## Test Framework Configuration\n\n**Required Dependencies:**\n- **JUnit 5** (jupiter), **MockK**, **Turbine** (app.cash.turbine)\n- **kotlinx.coroutines.test**, **Robolectric**, **Compose Test**\n\n**Critical Note:** Tests run with en-US locale for consistency. Don't assume other locales.\n\n---\n\n## A. ViewModel Testing Patterns\n\n### Base Class: BaseViewModelTest\n\n**Always extend `BaseViewModelTest` for ViewModel tests.**\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`\n\n**Benefits:**\n- Automatically registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`\n- Provides `stateEventFlow()` helper for simultaneous StateFlow/EventFlow testing\n\n**Pattern:**\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n    private val mockRepository: ExampleRepository = mockk()\n    private val savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to INITIAL_STATE))\n\n    @Test\n    fun `ButtonClick should fetch data and update state`() = runTest {\n        coEvery { mockRepository.fetchData(any()) } returns Result.success(\"data\")\n\n        val viewModel = ExampleViewModel(savedStateHandle, mockRepository)\n\n        viewModel.stateFlow.test {\n            assertEquals(INITIAL_STATE, awaitItem())\n            viewModel.trySendAction(ExampleAction.ButtonClick)\n            assertEquals(INITIAL_STATE.copy(data = \"data\"), awaitItem())\n        }\n\n        coVerify { mockRepository.fetchData(any()) }\n    }\n}\n```\n\n**For complete examples:** See `references/test-base-classes.md`\n\n### StateFlow vs EventFlow (Critical Distinction)\n\n| Flow Type | Replay | First Action | Pattern |\n|-----------|--------|--------------|---------|\n| StateFlow | Yes (1) | `awaitItem()` gets current state | Expect initial → trigger → expect new |\n| EventFlow | No | `expectNoEvents()` first | expectNoEvents → trigger → expect event |\n\n**For detailed patterns:** See `references/flow-testing-patterns.md`\n\n---\n\n## B. Compose UI Testing Patterns\n\n### Base Class: BitwardenComposeTest\n\n**Always extend `BitwardenComposeTest` for Compose screen tests.**\n\n**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`\n\n**Benefits:**\n- Pre-configures all Bitwarden managers (FeatureFlags, AuthTab, Biometrics, etc.)\n- Wraps content in `BitwardenTheme` and `LocalManagerProvider`\n- Provides fixed Clock for deterministic time-based tests\n\n**Pattern:**\n```kotlin\nclass ExampleScreenTest : BitwardenComposeTest() {\n    private var haveCalledNavigateBack = false\n    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        setContent {\n            ExampleScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                viewModel = viewModel,\n            )\n        }\n    }\n\n    @Test\n    fun `on back click should send BackClick action`() {\n        composeTestRule.onNodeWithContentDescription(\"Back\").performClick()\n        verify { viewModel.trySendAction(ExampleAction.BackClick) }\n    }\n}\n```\n\n**Note:** Use `bufferedMutableSharedFlow` for event testing in Compose tests. Default replay is 0; pass `replay = 1` if needed.\n\n**For complete base class details:** See `references/test-base-classes.md`\n\n---\n\n## C. Repository and Service Testing\n\n### Service Testing with MockWebServer\n\n**Base Class:** `BaseServiceTest` (`network/src/testFixtures/`)\n\n```kotlin\nclass ExampleServiceTest : BaseServiceTest() {\n    private val api: ExampleApi = retrofit.create()\n    private val service = ExampleServiceImpl(api)\n\n    @Test\n    fun `getConfig should return success when API succeeds`() = runTest {\n        server.enqueue(MockResponse().setBody(EXPECTED_JSON))\n        val result = service.getConfig()\n        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)\n    }\n}\n```\n\n### Repository Testing Pattern\n\n```kotlin\nclass ExampleRepositoryTest {\n    private val fixedClock: Clock = Clock.fixed(\n        Instant.parse(\"2023-10-27T12:00:00Z\"),\n        ZoneOffset.UTC,\n    )\n    private val dispatcherManager = FakeDispatcherManager()\n    private val mockDiskSource: ExampleDiskSource = mockk()\n    private val mockService: ExampleService = mockk()\n\n    private val repository = ExampleRepositoryImpl(\n        clock = fixedClock,\n        exampleDiskSource = mockDiskSource,\n        exampleService = mockService,\n        dispatcherManager = dispatcherManager,\n    )\n\n    @Test\n    fun `fetchData should return success when service succeeds`() = runTest {\n        coEvery { mockService.getData(any()) } returns expectedData.asSuccess()\n        val result = repository.fetchData(userId)\n        assertTrue(result.isSuccess)\n    }\n}\n```\n\n**Key patterns:** Use `FakeDispatcherManager`, fixed Clock, and `.asSuccess()` helpers.\n\n---\n\n## D. Test Data Builders\n\n### Builder Pattern with Number Parameter\n\n**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/model/`\n\n```kotlin\nfun createMockCipher(\n    number: Int,\n    id: String = \"mockId-$number\",\n    name: String? = \"mockName-$number\",\n    // ... more parameters with defaults\n): SyncResponseJson.Cipher\n\n// Usage:\nval cipher1 = createMockCipher(number = 1)  // mockId-1, mockName-1\nval cipher2 = createMockCipher(number = 2)  // mockId-2, mockName-2\nval custom = createMockCipher(number = 3, name = \"Custom\")\n```\n\n**Available Builders (35+):**\n- **Cipher:** `createMockCipher()`, `createMockLogin()`, `createMockCard()`, `createMockIdentity()`, `createMockSecureNote()`, `createMockSshKey()`, `createMockField()`, `createMockUri()`, `createMockFido2Credential()`, `createMockPasswordHistory()`, `createMockCipherPermissions()`\n- **Sync:** `createMockSyncResponse()`, `createMockFolder()`, `createMockCollection()`, `createMockPolicy()`, `createMockDomains()`\n- **Send:** `createMockSend()`, `createMockFile()`, `createMockText()`, `createMockSendJsonRequest()`\n- **Profile:** `createMockProfile()`, `createMockOrganization()`, `createMockProvider()`, `createMockPermissions()`\n- **Attachments:** `createMockAttachment()`, `createMockAttachmentJsonRequest()`, `createMockAttachmentResponse()`\n\nSee `network/src/testFixtures/kotlin/com/bitwarden/network/model/` for full list.\n\n---\n\n## E. Result Type Testing\n\n**Locations:**\n- `.asSuccess()`, `.asFailure()`: `core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.kt`\n- `assertCoroutineThrows`: `core/src/testFixtures/kotlin/com/bitwarden/core/data/util/TestHelpers.kt`\n\n```kotlin\n// Create results\n\"data\".asSuccess()              // Result.success(\"data\")\nthrowable.asFailure()           // Result.failure<T>(throwable)\n\n// Assertions\nassertTrue(result.isSuccess)\nassertEquals(expectedValue, result.getOrNull())\n```\n\n---\n\n## F. Test Utilities and Helpers\n\n### Fake Implementations\n\n| Fake | Location | Purpose |\n|------|----------|---------|\n| `FakeDispatcherManager` | `core/src/testFixtures/` | Deterministic coroutine execution |\n| `FakeConfigDiskSource` | `data/src/testFixtures/` | In-memory config storage |\n| `FakeSharedPreferences` | `data/src/testFixtures/` | Memory-backed SharedPreferences |\n\n### Exception Testing (CRITICAL)\n\n```kotlin\n// CORRECT - Call directly, NOT inside runTest\n@Test\nfun `test exception`() {\n    assertCoroutineThrows<IllegalStateException> {\n        repository.throwingFunction()\n    }\n}\n```\n\n**Why:** `runTest` catches exceptions and rethrows them, breaking the assertion pattern.\n\n---\n\n## G. Critical Gotchas\n\nCommon testing mistakes in Bitwarden. **For complete details and examples:** See `references/critical-gotchas.md`\n\n**Core Patterns:**\n- **assertCoroutineThrows + runTest** - Never wrap in `runTest`; call directly\n- **Static mock cleanup** - Always `unmockkStatic()` in `@After`\n- **StateFlow vs EventFlow** - StateFlow: `awaitItem()` first; EventFlow: `expectNoEvents()` first\n- **FakeDispatcherManager** - Always use instead of real `DispatcherManagerImpl`\n- **Coroutine test wrapper** - Use `runTest { }` for all Flow/coroutine tests\n\n**Assertion Patterns:**\n- **Complete state assertions** - Assert entire state objects, not individual fields\n- **JUnit over Kotlin** - Use `assertTrue()`, not Kotlin's `assert()`\n- **Use Result extensions** - Use `asSuccess()` and `asFailure()` for Result type assertions\n\n**Test Design:**\n- **Fake vs Mock strategy** - Use Fakes for happy paths, Mocks for error paths\n- **DI over static mocking** - Extract interfaces (like UuidManager) instead of mockkStatic\n- **Null stream testing** - Test null returns from ContentResolver operations\n- **bufferedMutableSharedFlow** - Use with `.onSubscription { emit(state) }` in Fakes\n- **Test factory methods** - Accept domain state types, not SavedStateHandle\n\n---\n\n## H. Test File Organization\n\n### Directory Structure\n\n```\nmodule/src/test/kotlin/com/bitwarden/.../\n├── ui/*ScreenTest.kt, *ViewModelTest.kt\n├── data/repository/*RepositoryTest.kt\n└── network/service/*ServiceTest.kt\n\nmodule/src/testFixtures/kotlin/com/bitwarden/.../\n├── util/TestHelpers.kt\n├── base/Base*Test.kt\n└── model/*Util.kt\n```\n\n### Test Constants Placement\n\nDeclare test constants as top-level `private const val` at the **bottom** of the file, after the class closing brace. Do NOT use `companion object` for test constants.\n\n### Test Naming\n\n- Classes: `*Test.kt`, `*ScreenTest.kt`, `*ViewModelTest.kt`\n- Functions: `` `given state when action should result` ``\n\n---\n\n## Summary\n\nKey Bitwarden-specific testing patterns:\n\n1. **BaseViewModelTest** - Automatic dispatcher setup with `stateEventFlow()` helper\n2. **BitwardenComposeTest** - Pre-configured with all managers and theme\n3. **BaseServiceTest** - MockWebServer setup for network testing\n4. **Turbine Flow Testing** - StateFlow (replay) vs EventFlow (no replay)\n5. **Test Data Builders** - Consistent `number: Int` parameter pattern\n6. **Fake Implementations** - FakeDispatcherManager, FakeConfigDiskSource\n7. **Result Type Testing** - `.asSuccess()`, `.asFailure()`\n\n**Always consult:** `docs/ARCHITECTURE.md` and existing test files for reference implementations.\n\n---\n\n## Reference Documentation\n\nFor detailed information, see:\n\n- `references/test-base-classes.md` - Detailed base class documentation and usage patterns\n- `references/flow-testing-patterns.md` - Complete Turbine patterns for StateFlow/EventFlow\n- `references/critical-gotchas.md` - Full anti-pattern reference and debugging tips\n\n**Complete Examples:**\n- `examples/viewmodel-test-example.md` - Full ViewModel test with StateFlow/EventFlow\n- `examples/compose-screen-test-example.md` - Full Compose screen test\n- `examples/repository-test-example.md` - Full repository test with mocks and fakes\n",".claude/skills/testing-android-code/examples/compose-screen-test-example.md":"/**\n * Complete Compose Screen Test Example\n *\n * Key patterns demonstrated:\n * - Extending BitwardenComposeTest\n * - Mocking ViewModel with flows\n * - Testing UI interactions\n * - Testing navigation callbacks\n * - Using bufferedMutableSharedFlow for events\n * - Testing dialogs with isDialog() and hasAnyAncestor()\n */\npackage com.bitwarden.example.feature\n\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.filterToOne\nimport androidx.compose.ui.test.hasAnyAncestor\nimport androidx.compose.ui.test.isDialog\nimport androidx.compose.ui.test.onAllNodesWithText\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow\nimport com.bitwarden.ui.util.assertNoDialogExists\nimport com.bitwarden.ui.util.isProgressBar\nimport com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.verify\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport junit.framework.TestCase.assertTrue\nimport org.junit.Before\nimport org.junit.Test\n\nclass ExampleScreenTest : BitwardenComposeTest() {\n\n    // Track navigation callbacks\n    private var haveCalledNavigateBack = false\n    private var haveCalledNavigateToNext = false\n\n    // Use bufferedMutableSharedFlow for events (default replay = 0)\n    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n\n    // Mock ViewModel with relaxed = true\n    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        haveCalledNavigateBack = false\n        haveCalledNavigateToNext = false\n\n        setContent {\n            ExampleScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                onNavigateToNext = { haveCalledNavigateToNext = true },\n                viewModel = viewModel,\n            )\n        }\n    }\n\n    /**\n     * Test: Back button sends action to ViewModel\n     */\n    @Test\n    fun `on back click should send BackClick action`() {\n        composeTestRule\n            .onNodeWithContentDescription(\"Back\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.BackClick) }\n    }\n\n    /**\n     * Test: Submit button sends action to ViewModel\n     */\n    @Test\n    fun `on submit click should send SubmitClick action`() {\n        composeTestRule\n            .onNodeWithText(\"Submit\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.SubmitClick) }\n    }\n\n    /**\n     * Test: Loading state shows progress indicator\n     */\n    @Test\n    fun `loading state should display progress indicator`() {\n        mutableStateFlow.update { it.copy(isLoading = true) }\n\n        composeTestRule\n            .onNode(isProgressBar)\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Data state shows content\n     */\n    @Test\n    fun `data state should display content`() {\n        mutableStateFlow.update { it.copy(data = \"Test Data\") }\n\n        composeTestRule\n            .onNodeWithText(\"Test Data\")\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Error state shows error message\n     */\n    @Test\n    fun `error state should display error message`() {\n        mutableStateFlow.update { it.copy(errorMessage = \"Something went wrong\") }\n\n        composeTestRule\n            .onNodeWithText(\"Something went wrong\")\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: NavigateBack event triggers navigation callback\n     */\n    @Test\n    fun `NavigateBack event should call onNavigateBack`() {\n        mutableEventFlow.tryEmit(ExampleEvent.NavigateBack)\n\n        assertTrue(haveCalledNavigateBack)\n    }\n\n    /**\n     * Test: NavigateToNext event triggers navigation callback\n     */\n    @Test\n    fun `NavigateToNext event should call onNavigateToNext`() {\n        mutableEventFlow.tryEmit(ExampleEvent.NavigateToNext)\n\n        assertTrue(haveCalledNavigateToNext)\n    }\n\n    /**\n     * Test: Item in list can be clicked\n     */\n    @Test\n    fun `on item click should send ItemClick action`() {\n        val itemId = \"item-123\"\n        mutableStateFlow.update {\n            it.copy(items = listOf(ExampleItem(id = itemId, name = \"Test Item\")))\n        }\n\n        composeTestRule\n            .onNodeWithText(\"Test Item\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.ItemClick(itemId)) }\n    }\n\n    // ==================== DIALOG TESTS ====================\n\n    /**\n     * Test: No dialog exists when dialogState is null\n     */\n    @Test\n    fun `no dialog should exist when dialogState is null`() {\n        mutableStateFlow.update { it.copy(dialogState = null) }\n\n        composeTestRule.assertNoDialogExists()\n    }\n\n    /**\n     * Test: Loading dialog displays when state updates\n     * PATTERN: Use isDialog() to check dialog exists\n     */\n    @Test\n    fun `loading dialog should display when dialogState is Loading`() {\n        mutableStateFlow.update {\n            it.copy(dialogState = ExampleState.DialogState.Loading(\"Please wait...\"))\n        }\n\n        composeTestRule\n            .onNode(isDialog())\n            .assertIsDisplayed()\n\n        // Verify loading text within dialog using hasAnyAncestor(isDialog())\n        composeTestRule\n            .onAllNodesWithText(\"Please wait...\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Error dialog displays title and message\n     * PATTERN: Use filterToOne(hasAnyAncestor(isDialog())) to find text within dialogs\n     */\n    @Test\n    fun `error dialog should display title and message`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Error(\n                    title = \"An error has occurred\",\n                    message = \"Something went wrong. Please try again.\",\n                ),\n            )\n        }\n\n        // Verify dialog exists\n        composeTestRule\n            .onNode(isDialog())\n            .assertIsDisplayed()\n\n        // Verify title within dialog\n        composeTestRule\n            .onAllNodesWithText(\"An error has occurred\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n\n        // Verify message within dialog\n        composeTestRule\n            .onAllNodesWithText(\"Something went wrong. Please try again.\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Dialog button click sends action\n     * PATTERN: Find button with hasAnyAncestor(isDialog()) then performClick()\n     */\n    @Test\n    fun `error dialog dismiss button should send DismissDialog action`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Error(\n                    title = \"Error\",\n                    message = \"An error occurred\",\n                ),\n            )\n        }\n\n        // Click dismiss button within dialog\n        composeTestRule\n            .onAllNodesWithText(\"Ok\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.DismissDialog) }\n    }\n\n    /**\n     * Test: Confirmation dialog with multiple buttons\n     * PATTERN: Test both confirm and cancel actions\n     */\n    @Test\n    fun `confirmation dialog confirm button should send ConfirmAction`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Confirmation(\n                    title = \"Confirm Action\",\n                    message = \"Are you sure you want to proceed?\",\n                ),\n            )\n        }\n\n        // Click confirm button\n        composeTestRule\n            .onAllNodesWithText(\"Confirm\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.ConfirmAction) }\n    }\n\n    @Test\n    fun `confirmation dialog cancel button should send DismissDialog action`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Confirmation(\n                    title = \"Confirm Action\",\n                    message = \"Are you sure?\",\n                ),\n            )\n        }\n\n        // Click cancel button\n        composeTestRule\n            .onAllNodesWithText(\"Cancel\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.DismissDialog) }\n    }\n}\n\nprivate val DEFAULT_STATE = ExampleState(\n    isLoading = false,\n    data = null,\n    errorMessage = null,\n    items = emptyList(),\n    dialogState = null,\n)\n\n// Example types (normally in separate files)\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    val errorMessage: String? = null,\n    val items: List<ExampleItem> = emptyList(),\n    val dialogState: DialogState? = null,\n) {\n    /**\n     * PATTERN: Nested sealed class for dialog states.\n     * Common dialog types: Loading, Error, Confirmation\n     */\n    sealed class DialogState {\n        data class Loading(val message: String) : DialogState()\n        data class Error(val title: String, val message: String) : DialogState()\n        data class Confirmation(val title: String, val message: String) : DialogState()\n    }\n}\n\ndata class ExampleItem(val id: String, val name: String)\n\nsealed class ExampleAction {\n    data object BackClick : ExampleAction()\n    data object SubmitClick : ExampleAction()\n    data class ItemClick(val itemId: String) : ExampleAction()\n    data object DismissDialog : ExampleAction()\n    data object ConfirmAction : ExampleAction()\n}\n\nsealed class ExampleEvent {\n    data object NavigateBack : ExampleEvent()\n    data object NavigateToNext : ExampleEvent()\n}\n",".claude/skills/testing-android-code/examples/repository-test-example.md":"/**\n * Complete Repository Test Example\n *\n * Key patterns demonstrated:\n * - Fake for disk sources, Mock for network services\n * - Using FakeDispatcherManager for deterministic coroutines\n * - Using fixed Clock for deterministic time\n * - Testing Result types with .asSuccess() / .asFailure()\n * - Asserting actual objects (not isSuccess/isFailure) for better diagnostics\n * - Testing Flow emissions with Turbine\n */\npackage com.bitwarden.example.data.repository\n\nimport app.cash.turbine.test\nimport com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager\nimport com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow\nimport com.bitwarden.core.data.util.asFailure\nimport com.bitwarden.core.data.util.asSuccess\nimport io.mockk.coEvery\nimport io.mockk.coVerify\nimport io.mockk.mockk\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.onSubscription\nimport kotlinx.coroutines.test.runTest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNull\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport java.time.Clock\nimport java.time.Instant\nimport java.time.ZoneOffset\n\nclass ExampleRepositoryTest {\n\n    // Fixed clock for deterministic time-based tests\n    private val fixedClock: Clock = Clock.fixed(\n        Instant.parse(\"2023-10-27T12:00:00Z\"),\n        ZoneOffset.UTC,\n    )\n\n    // Use FakeDispatcherManager for deterministic coroutine execution\n    private val dispatcherManager = FakeDispatcherManager()\n\n    // Mock service (network layer is always mocked)\n    private val mockService: ExampleService = mockk()\n\n    /**\n     * PATTERN: Use Fake for disk source in happy path tests.\n     * This is the Bitwarden convention for repository testing.\n     */\n    private val fakeDiskSource = FakeExampleDiskSource()\n\n    private lateinit var repository: ExampleRepositoryImpl\n\n    @BeforeEach\n    fun setup() {\n        repository = ExampleRepositoryImpl(\n            clock = fixedClock,\n            service = mockService,\n            diskSource = fakeDiskSource,\n            dispatcherManager = dispatcherManager,\n        )\n    }\n\n    // ==================== HAPPY PATH TESTS (use Fake) ====================\n\n    /**\n     * Test: Successful fetch returns data and saves to disk\n     */\n    @Test\n    fun `fetchData should return success and save to disk when service succeeds`() = runTest {\n        val expectedData = ExampleData(id = \"1\", name = \"Test\", updatedAt = fixedClock.instant())\n        coEvery { mockService.getData() } returns expectedData.asSuccess()\n\n        val result = repository.fetchData()\n\n        assertEquals(expectedData, result.getOrThrow())\n        // Fake automatically stores the data - verify it's there\n        assertEquals(expectedData, fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Service failure returns failure without saving\n     */\n    @Test\n    fun `fetchData should return failure when service fails`() = runTest {\n        val exception = Exception(\"Network error\")\n        coEvery { mockService.getData() } returns exception.asFailure()\n\n        val result = repository.fetchData()\n\n        assertEquals(exception, result.exceptionOrNull())\n        // Fake was not updated\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Repository flow emits when disk source updates\n     */\n    @Test\n    fun `dataFlow should emit when disk source updates`() = runTest {\n        val data1 = ExampleData(id = \"1\", name = \"First\", updatedAt = fixedClock.instant())\n        val data2 = ExampleData(id = \"2\", name = \"Second\", updatedAt = fixedClock.instant())\n\n        repository.dataFlow.test {\n            // Initial null value from Fake\n            assertNull(awaitItem())\n\n            // Update via Fake property setter (triggers emission)\n            fakeDiskSource.storedData = data1\n            assertEquals(data1, awaitItem())\n\n            // Another update\n            fakeDiskSource.storedData = data2\n            assertEquals(data2, awaitItem())\n        }\n    }\n\n    /**\n     * Test: Refresh fetches and saves new data\n     */\n    @Test\n    fun `refresh should fetch new data and update disk source`() = runTest {\n        val newData = ExampleData(id = \"new\", name = \"Fresh\", updatedAt = fixedClock.instant())\n        coEvery { mockService.getData() } returns newData.asSuccess()\n\n        val result = repository.refresh()\n\n        assertEquals(Unit, result.getOrThrow())\n        coVerify { mockService.getData() }\n        assertEquals(newData, fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Delete clears data from disk\n     */\n    @Test\n    fun `deleteData should clear disk source`() = runTest {\n        // Pre-populate the fake\n        fakeDiskSource.storedData = ExampleData(id = \"1\", name = \"Test\", updatedAt = fixedClock.instant())\n\n        repository.deleteData()\n\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Cached data returns from disk when available\n     */\n    @Test\n    fun `getCachedData should return disk data without network call`() = runTest {\n        val cachedData = ExampleData(\n            id = \"cached\",\n            name = \"Cached\",\n            updatedAt = fixedClock.instant(),\n        )\n        fakeDiskSource.storedData = cachedData\n\n        val result = repository.getCachedData()\n\n        assertEquals(cachedData, result)\n        coVerify(exactly = 0) { mockService.getData() }\n    }\n\n    // ==================== ERROR PATH TESTS ====================\n\n    /**\n     * PATTERN: For error paths, reconfigure the class-level mock per-test.\n     * Use coEvery to change mock behavior for each specific test case.\n     */\n    @Test\n    fun `fetchData should return failure when service returns error`() = runTest {\n        val exception = Exception(\"Server unavailable\")\n        coEvery { mockService.getData() } returns exception.asFailure()\n\n        val result = repository.fetchData()\n\n        assertEquals(exception, result.exceptionOrNull())\n        // Fake state unchanged on failure\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    @Test\n    fun `refresh should return failure and preserve cached data when service fails`() = runTest {\n        // Pre-populate cache via Fake\n        val cachedData = ExampleData(id = \"cached\", name = \"Old\", updatedAt = fixedClock.instant())\n        fakeDiskSource.storedData = cachedData\n\n        // Reconfigure mock to return failure\n        coEvery { mockService.getData() } returns Exception(\"Network error\").asFailure()\n\n        val result = repository.refresh()\n\n        assertTrue(result.isFailure)\n        // Cached data preserved on failure\n        assertEquals(cachedData, fakeDiskSource.storedData)\n    }\n}\n\n// Example types (normally in separate files)\ndata class ExampleData(\n    val id: String,\n    val name: String,\n    val updatedAt: Instant,\n)\n\ninterface ExampleService {\n    suspend fun getData(): Result<ExampleData>\n}\n\ninterface ExampleDiskSource {\n    val dataFlow: kotlinx.coroutines.flow.Flow<ExampleData?>\n    fun getData(): ExampleData?\n    fun saveData(data: ExampleData)\n    fun clearData()\n}\n\n/**\n * PATTERN: Fake implementation for happy path testing.\n *\n * Key characteristics:\n * - Uses bufferedMutableSharedFlow(replay = 1) for proper replay behavior\n * - Uses .onSubscription { emit(state) } for immediate state emission\n * - Private storage with override property setter that emits to flow\n * - Test assertions done via the override property getter\n */\nclass FakeExampleDiskSource : ExampleDiskSource {\n    private var storedDataValue: ExampleData? = null\n    private val mutableDataFlow = bufferedMutableSharedFlow<ExampleData?>(replay = 1)\n\n    /**\n     * Override property with getter/setter. Setter emits to flow automatically.\n     * Tests can read this property for assertions and write to trigger emissions.\n     */\n    var storedData: ExampleData?\n        get() = storedDataValue\n        set(value) {\n            storedDataValue = value\n            mutableDataFlow.tryEmit(value)\n        }\n\n    override val dataFlow: Flow<ExampleData?>\n        get() = mutableDataFlow.onSubscription { emit(storedData) }\n\n    override fun getData(): ExampleData? = storedData\n\n    override fun saveData(data: ExampleData) {\n        storedData = data\n    }\n\n    override fun clearData() {\n        storedData = null\n    }\n}\n",".claude/skills/testing-android-code/examples/viewmodel-test-example.md":"/**\n * Complete ViewModel Test Example\n *\n * Key patterns demonstrated:\n * - Extending BaseViewModelTest\n * - Testing StateFlow with Turbine\n * - Testing EventFlow with Turbine\n * - Using stateEventFlow() for simultaneous testing\n * - MockK mocking patterns\n * - Test factory method design (accepts domain state, not SavedStateHandle)\n * - Complete state assertions (assert entire state objects)\n */\npackage com.bitwarden.example.feature\n\nimport androidx.lifecycle.SavedStateHandle\nimport app.cash.turbine.test\nimport com.bitwarden.ui.platform.base.BaseViewModelTest\nimport io.mockk.coEvery\nimport io.mockk.coVerify\nimport io.mockk.every\nimport io.mockk.mockk\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.test.runTest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Test\n\nclass ExampleViewModelTest : BaseViewModelTest() {\n\n    // Mock dependencies\n    private val mockRepository: ExampleRepository = mockk()\n    private val mockAuthDiskSource: AuthDiskSource = mockk {\n        every { userStateFlow } returns MutableStateFlow(null)\n    }\n\n    /**\n     * StateFlow has replay=1, so first awaitItem() returns current state\n     */\n    @Test\n    fun `initial state should be default state`() = runTest {\n        val viewModel = createViewModel()\n\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n        }\n    }\n\n    /**\n     * Test state transitions: initial -> loading -> success\n     */\n    @Test\n    fun `LoadData action should update state from idle to loading to success`() = runTest {\n        val expectedData = \"loaded data\"\n        coEvery { mockRepository.fetchData(any()) } returns Result.success(expectedData)\n\n        val viewModel = createViewModel()\n\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.LoadData)\n\n            assertEquals(DEFAULT_STATE.copy(isLoading = true), awaitItem())\n            assertEquals(DEFAULT_STATE.copy(isLoading = false, data = expectedData), awaitItem())\n        }\n\n        coVerify { mockRepository.fetchData(any()) }\n    }\n\n    /**\n     * EventFlow has no replay - MUST call expectNoEvents() first\n     */\n    @Test\n    fun `SubmitClick action should emit NavigateToNext event`() = runTest {\n        coEvery { mockRepository.submitData(any()) } returns Result.success(Unit)\n\n        val viewModel = createViewModel()\n\n        viewModel.eventFlow.test {\n            expectNoEvents() // CRITICAL for EventFlow\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n            assertEquals(ExampleEvent.NavigateToNext, awaitItem())\n        }\n    }\n\n    /**\n     * Use stateEventFlow() helper for simultaneous testing\n     */\n    @Test\n    fun `complex action should update state and emit event`() = runTest {\n        coEvery { mockRepository.complexOperation(any()) } returns Result.success(\"result\")\n\n        val viewModel = createViewModel()\n\n        viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n            assertEquals(DEFAULT_STATE, stateFlow.awaitItem())\n            eventFlow.expectNoEvents()\n\n            viewModel.trySendAction(ExampleAction.ComplexAction)\n\n            assertEquals(DEFAULT_STATE.copy(isLoading = true), stateFlow.awaitItem())\n            assertEquals(DEFAULT_STATE.copy(data = \"result\"), stateFlow.awaitItem())\n            assertEquals(ExampleEvent.ShowToast(\"Success!\"), eventFlow.awaitItem())\n        }\n    }\n\n    /**\n     * Test state restoration from saved state.\n     * Note: Use initialState parameter, NOT SavedStateHandle directly.\n     */\n    @Test\n    fun `initial state from saved state should be preserved`() = runTest {\n        // Build complete expected state - always assert full objects\n        val savedState = ExampleState(\n            isLoading = false,\n            data = \"restored data\",\n            errorMessage = null,\n        )\n\n        val viewModel = createViewModel(initialState = savedState)\n\n        viewModel.stateFlow.test {\n            assertEquals(savedState, awaitItem())\n        }\n    }\n\n    /**\n     * Factory method accepts domain state, NOT SavedStateHandle.\n     * This hides Android framework details from test logic.\n     */\n    private fun createViewModel(\n        initialState: ExampleState? = null,\n    ): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle().apply { set(\"state\", initialState) },\n        repository = mockRepository,\n        authDiskSource = mockAuthDiskSource,\n    )\n}\n\nprivate val DEFAULT_STATE = ExampleState(\n    isLoading = false,\n    data = null,\n    errorMessage = null,\n)\n\n// Example types (normally in separate files)\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    val errorMessage: String? = null,\n)\n\nsealed class ExampleAction {\n    data object LoadData : ExampleAction()\n    data object SubmitClick : ExampleAction()\n    data object ComplexAction : ExampleAction()\n}\n\nsealed class ExampleEvent {\n    data object NavigateToNext : ExampleEvent()\n    data class ShowToast(val message: String) : ExampleEvent()\n}\n",".claude/skills/testing-android-code/references/critical-gotchas.md":"# Critical Gotchas and Anti-Patterns\n\nCommon mistakes and pitfalls when writing tests in the Bitwarden Android codebase.\n\n## ❌ NEVER wrap assertCoroutineThrows in runTest\n\n### The Problem\n\n`runTest` catches exceptions and rethrows them, which breaks the `assertCoroutineThrows` assertion pattern.\n\n### Wrong\n\n```kotlin\n@Test\nfun `test exception`() = runTest {\n    assertCoroutineThrows<Exception> {\n        repository.throwingFunction()\n    }  // Won't work - exception is caught by runTest!\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `test exception`() {\n    assertCoroutineThrows<Exception> {\n        repository.throwingFunction()\n    }  // Works correctly\n}\n```\n\n### Why This Happens\n\n`runTest` provides a coroutine scope and catches exceptions to provide better error messages. However, `assertCoroutineThrows` needs to catch the exception itself to verify it was thrown. When wrapped in `runTest`, the exception is caught twice, breaking the assertion.\n\n## ❌ ALWAYS unmock static functions\n\n### The Problem\n\nMockK's static mocking persists across tests. Forgetting to clean up causes mysterious failures in subsequent tests.\n\n### Wrong\n\n```kotlin\n@Before\nfun setup() {\n    mockkStatic(::isBuildVersionAtLeast)\n    every { isBuildVersionAtLeast(any()) } returns true\n}\n\n// Forgot @After - subsequent tests will fail mysteriously!\n```\n\n### Correct\n\n```kotlin\n@Before\nfun setup() {\n    mockkStatic(::isBuildVersionAtLeast)\n    every { isBuildVersionAtLeast(any()) } returns true\n}\n\n@After\nfun tearDown() {\n    unmockkStatic(::isBuildVersionAtLeast)  // CRITICAL\n}\n```\n\n### Common Static Functions to Watch\n\n```kotlin\n// Platform version checks\nmockkStatic(::isBuildVersionAtLeast)\nunmockkStatic(::isBuildVersionAtLeast)\n\n// URI parsing\nmockkStatic(Uri::class)\nunmockkStatic(Uri::class)\n\n// Static utility functions\nmockkStatic(MyUtilClass::class)\nunmockkStatic(MyUtilClass::class)\n```\n\n### Debugging Tip\n\nIf tests pass individually but fail when run together, suspect static mocking cleanup issues.\n\n## ❌ Don't confuse StateFlow and EventFlow testing\n\n### StateFlow (replay = 1)\n\n```kotlin\n// CORRECT - StateFlow always has current value\nviewModel.stateFlow.test {\n    val initial = awaitItem()  // Gets current state immediately\n    viewModel.trySendAction(action)\n    val updated = awaitItem()  // Gets new state\n}\n```\n\n### EventFlow (no replay)\n\n```kotlin\n// CORRECT - EventFlow has no initial value\nviewModel.eventFlow.test {\n    expectNoEvents()  // MUST do this first\n    viewModel.trySendAction(action)\n    val event = awaitItem()  // Gets emitted event\n}\n```\n\n### Common Mistake\n\n```kotlin\n// WRONG - Forgetting expectNoEvents() on EventFlow\nviewModel.eventFlow.test {\n    viewModel.trySendAction(action)  // May cause flaky tests\n    assertEquals(event, awaitItem())\n}\n```\n\n## ❌ Don't mix real and test dispatchers\n\n### Wrong\n\n```kotlin\nprivate val repository = ExampleRepositoryImpl(\n    dispatcherManager = DispatcherManagerImpl(),  // Real dispatcher!\n)\n\n@Test\nfun `test repository`() = runTest {\n    // Test will have timing issues - real dispatcher != test dispatcher\n}\n```\n\n### Correct\n\n```kotlin\nprivate val repository = ExampleRepositoryImpl(\n    dispatcherManager = FakeDispatcherManager(),  // Test dispatcher\n)\n\n@Test\nfun `test repository`() = runTest {\n    // Test runs deterministically\n}\n```\n\n### Why This Matters\n\nReal dispatchers use actual thread pools and delays. Test dispatchers (UnconfinedTestDispatcher) execute immediately and deterministically. Mixing them causes:\n- Non-deterministic test failures\n- Real delays in tests (slow test suite)\n- Race conditions\n\n### Always Use\n\n- `FakeDispatcherManager()` for repositories\n- `UnconfinedTestDispatcher()` when manually creating dispatchers\n- `runTest` for coroutine tests (provides TestDispatcher automatically)\n\n## ❌ Don't forget to use runTest for coroutine tests\n\n### Wrong\n\n```kotlin\n@Test\nfun `test coroutine`() {\n    viewModel.stateFlow.test { /* ... */ }  // Missing runTest!\n}\n```\n\nThis causes:\n- Test completes before coroutines finish\n- False positives (test passes but assertions never run)\n- Mysterious failures\n\n### Correct\n\n```kotlin\n@Test\nfun `test coroutine`() = runTest {\n    viewModel.stateFlow.test { /* ... */ }\n}\n```\n\n### When runTest is Required\n\n- Testing ViewModels (they use `viewModelScope`)\n- Testing Flows with Turbine `.test {}`\n- Testing repositories with suspend functions\n- Any test calling suspend functions\n\n### Exception: assertCoroutineThrows\n\nAs noted above, `assertCoroutineThrows` should NOT be wrapped in `runTest`.\n\n## ❌ Don't forget relaxed = true for complex mocks\n\n### Without relaxed\n\n```kotlin\nprivate val viewModel = mockk<ExampleViewModel>()  // Must mock every method!\n\n// Error: \"no answer found for: stateFlow\"\n```\n\n### With relaxed\n\n```kotlin\nprivate val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n    // Only mock what you care about\n    every { stateFlow } returns mutableStateFlow\n    every { eventFlow } returns mutableEventFlow\n}\n```\n\n### When to Use relaxed\n\n- Mocking ViewModels in Compose tests\n- Mocking complex objects with many methods\n- When you only care about specific method calls\n\n### When NOT to Use relaxed\n\n- Mocking repository interfaces (be explicit about behavior)\n- When you want to verify NO unexpected calls\n- Testing error paths (want test to fail if unexpected method called)\n\n## ❌ Don't assert individual fields when complete state is available\n\n### The Problem\n\nAsserting individual state fields can miss unintended side effects on other fields.\n\n### Wrong\n\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    viewModel.trySendAction(SomeAction.DoThing)\n\n    val state = viewModel.stateFlow.value\n    assertEquals(null, state.dialog)  // Only checks one field!\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    viewModel.trySendAction(SomeAction.DoThing)\n\n    val expected = SomeState(\n        isLoading = false,\n        data = \"result\",\n        dialog = null,\n    )\n    assertEquals(expected, viewModel.stateFlow.value)  // Checks all fields\n}\n```\n\n### Why This Matters\n\n- Catches unintended mutations to other state fields\n- Makes expected state explicit and readable\n- Prevents silent regressions when state structure changes\n\n---\n\n## ❌ Don't use Kotlin assert() for boolean checks\n\n### The Problem\n\nKotlin's `assert()` doesn't follow JUnit conventions and provides poor failure messages.\n\n### Wrong\n\n```kotlin\n@Test\nfun `event should trigger callback`() {\n    mutableEventFlow.tryEmit(SomeEvent.Navigate)\n\n    assert(onNavigateCalled)  // Kotlin assert - bad failure messages\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `event should trigger callback`() {\n    mutableEventFlow.tryEmit(SomeEvent.Navigate)\n\n    assertTrue(onNavigateCalled)  // JUnit assertTrue - proper assertion\n}\n```\n\n### Always Use JUnit Assertions\n\n- `assertTrue()` / `assertFalse()` for booleans\n- `assertEquals()` for value comparisons\n- `assertNotNull()` / `assertNull()` for nullability\n- `assertThrows<T>()` for exceptions\n\n---\n\n## ❌ Don't pass SavedStateHandle to test factory methods\n\n### The Problem\n\nExposing `SavedStateHandle` in test factory methods leaks Android framework details into test logic.\n\n### Wrong\n\n```kotlin\nprivate fun createViewModel(\n    savedStateHandle: SavedStateHandle = SavedStateHandle(),  // Framework type exposed\n): MyViewModel = MyViewModel(\n    savedStateHandle = savedStateHandle,\n    repository = mockRepository,\n)\n\n@Test\nfun `initial state from saved state`() = runTest {\n    val savedState = MyState(isLoading = true)\n    val savedStateHandle = SavedStateHandle(mapOf(\"state\" to savedState))\n\n    val viewModel = createViewModel(savedStateHandle = savedStateHandle)\n    // ...\n}\n```\n\n### Correct\n\n```kotlin\nprivate fun createViewModel(\n    initialState: MyState? = null,  // Domain type only\n): MyViewModel = MyViewModel(\n    savedStateHandle = SavedStateHandle().apply { set(\"state\", initialState) },\n    repository = mockRepository,\n)\n\n@Test\nfun `initial state from saved state`() = runTest {\n    val savedState = MyState(isLoading = true)\n\n    val viewModel = createViewModel(initialState = savedState)\n    // ...\n}\n```\n\n### Why This Matters\n\n- Cleaner, more intuitive test code\n- Hides SavedStateHandle implementation details\n- Follows Bitwarden conventions\n\n---\n\n## ❌ Don't test SavedStateHandle persistence in unit tests\n\n### The Problem\n\nTesting whether state persists to SavedStateHandle is testing Android framework behavior, not your business logic.\n\n### Wrong\n\n```kotlin\n@Test\nfun `state should persist to SavedStateHandle`() = runTest {\n    val savedStateHandle = SavedStateHandle()\n    val viewModel = createViewModel(savedStateHandle = savedStateHandle)\n\n    viewModel.trySendAction(SomeAction)\n\n    val savedState = savedStateHandle.get<MyState>(\"state\")\n    assertEquals(expectedState, savedState)  // Testing framework, not logic!\n}\n```\n\n### Correct\n\nFocus on testing business logic and state transformations:\n\n```kotlin\n@Test\nfun `action should update state correctly`() = runTest {\n    val viewModel = createViewModel()\n\n    viewModel.trySendAction(SomeAction)\n\n    assertEquals(expectedState, viewModel.stateFlow.value)  // Test observable state\n}\n```\n\n---\n\n## ❌ Don't use static mocking when DI pattern is available\n\n### The Problem\n\nStatic mocking (`mockkStatic`) is harder to maintain and less testable than dependency injection.\n\n### Wrong\n\n```kotlin\nclass ParserTest {\n    @BeforeEach\n    fun setup() {\n        mockkStatic(UUID::class)\n        every { UUID.randomUUID() } returns mockk {\n            every { toString() } returns \"fixed-uuid\"\n        }\n    }\n\n    @AfterEach\n    fun tearDown() {\n        unmockkStatic(UUID::class)\n    }\n}\n```\n\n### Correct\n\nExtract an interface and inject it:\n\n```kotlin\n// Production code\ninterface UuidManager {\n    fun generateUuid(): String\n}\n\nclass UuidManagerImpl : UuidManager {\n    override fun generateUuid(): String = UUID.randomUUID().toString()\n}\n\nclass Parser(private val uuidManager: UuidManager) { ... }\n\n// Test code\nclass ParserTest {\n    private val mockUuidManager = mockk<UuidManager>()\n\n    @BeforeEach\n    fun setup() {\n        every { mockUuidManager.generateUuid() } returns \"fixed-uuid\"\n    }\n\n    // No tearDown needed - no static mocking!\n}\n```\n\n### When to Use This Pattern\n\n- UUID generation\n- Timestamp/Clock operations\n- System property access\n- Any static function that needs deterministic testing\n\n---\n\n## ❌ Don't forget to test null stream returns from Android APIs\n\n### The Problem\n\nAndroid's `ContentResolver.openOutputStream()` and `openInputStream()` can return null, not just throw exceptions.\n\n### Wrong\n\n```kotlin\nclass FileManagerTest {\n    @Test\n    fun `stringToUri with exception should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } throws IOException()\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n\n        assertFalse(result)\n    }\n    // Missing: test for null return!\n}\n```\n\n### Correct\n\n```kotlin\nclass FileManagerTest {\n    @Test\n    fun `stringToUri with exception should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } throws IOException()\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n        assertFalse(result)\n    }\n\n    @Test\n    fun `stringToUri with null stream should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } returns null\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n        assertFalse(result)  // CRITICAL: must handle null!\n    }\n}\n```\n\n### Common Android APIs That Return Null\n\n- `ContentResolver.openOutputStream()` / `openInputStream()`\n- `Context.getExternalFilesDir()`\n- `PackageManager.getApplicationInfo()` (can throw)\n\n---\n\n## Bitwarden Mocking Guidelines\n\n**Mock at architectural boundaries:**\n- Repository → ViewModel (mock repository)\n- Service → Repository (mock service)\n- API → Service (use MockWebServer, not mocks)\n- DiskSource → Repository (mock disk source)\n\n**Fake vs Mock Strategy (IMPORTANT):**\n- **Happy paths**: Use Fake implementations (`FakeAuthenticatorDiskSource`, `FakeVaultDiskSource`)\n- **Error paths**: Use MockK with isolated repository instances\n\n```kotlin\n// Happy path - use Fake\nprivate val fakeDiskSource = FakeAuthenticatorDiskSource()\n\n@Test\nfun `createItem should return Success`() = runTest {\n    val result = repository.createItem(mockItem)\n    assertEquals(CreateItemResult.Success, result)\n}\n\n// Error path - use isolated Mock\n@Test\nfun `createItem with exception should return Error`() = runTest {\n    val mockDiskSource = mockk<AuthenticatorDiskSource> {\n        coEvery { saveItem(any()) } throws RuntimeException()\n    }\n    val repository = RepositoryImpl(diskSource = mockDiskSource)\n\n    val result = repository.createItem(mockItem)\n    assertEquals(CreateItemResult.Error, result)\n}\n```\n\n**Use Fakes for:**\n- `FakeDispatcherManager` - deterministic coroutines\n- `FakeConfigDiskSource` - in-memory config storage\n- `FakeSharedPreferences` - memory-backed preferences\n- `FakeAuthenticatorDiskSource` - in-memory authenticator storage\n\n**Create real instances for:**\n- Data classes, value objects (User, Config, CipherView)\n- Test data builders (`createMockCipher(number = 1)`)\n\n## ❌ Don't forget bufferedMutableSharedFlow with onSubscription for Fakes\n\n### The Problem\n\nFake data sources using `MutableSharedFlow` won't emit cached state to new subscribers without explicit handling.\n\n### Wrong\n\n```kotlin\nclass FakeDataSource : DataSource {\n    private val mutableFlow = MutableSharedFlow<List<Item>>()\n    private val storedItems = mutableListOf<Item>()\n\n    override fun getItems(): Flow<List<Item>> = mutableFlow\n\n    override suspend fun saveItem(item: Item) {\n        storedItems.add(item)\n        mutableFlow.emit(storedItems)\n    }\n}\n\n// Test: Initial collection gets nothing!\nrepository.dataFlow.test {\n    // Hangs or fails - no initial emission\n}\n```\n\n### Correct\n\n```kotlin\nclass FakeDataSource : DataSource {\n    private val mutableFlow = bufferedMutableSharedFlow<List<Item>>()\n    private val storedItems = mutableListOf<Item>()\n\n    override fun getItems(): Flow<List<Item>> = mutableFlow\n        .onSubscription { emit(storedItems.toList()) }\n\n    override suspend fun saveItem(item: Item) {\n        storedItems.add(item)\n        mutableFlow.emit(storedItems.toList())\n    }\n}\n\n// Test: Initial collection receives current state\nrepository.dataFlow.test {\n    assertEquals(emptyList(), awaitItem())  // Works!\n}\n```\n\n### Key Points\n\n- Use `bufferedMutableSharedFlow()` from `core/data/repository/util/`\n- Add `.onSubscription { emit(currentState) }` for immediate state emission\n- This ensures new collectors receive the current cached state\n\n---\n\n## ✅ Use Result extension functions for assertions\n\n### The Pattern\n\nUse `asSuccess()` and `asFailure()` extensions from `com.bitwarden.core.data.util` for cleaner Result assertions.\n\n### Success Path\n\n```kotlin\n@Test\nfun `getData should return success`() = runTest {\n    val result = repository.getData()\n    val expected = expectedData.asSuccess()\n\n    assertEquals(expected.getOrNull(), result.getOrNull())\n}\n```\n\n### Failure Path\n\n```kotlin\n@Test\nfun `getData with error should return failure`() = runTest {\n    val exception = IOException(\"Network error\")\n    coEvery { mockService.getData() } returns exception.asFailure()\n\n    val result = repository.getData()\n\n    assertTrue(result.isFailure)\n    assertEquals(exception, result.exceptionOrNull())\n}\n```\n\n### Avoid Redundant Assertions\n\n```kotlin\n// WRONG - redundant success checks\nassertTrue(result.isSuccess)\nassertTrue(expected.isSuccess)\nassertArrayEquals(expected.getOrNull(), result.getOrNull())\n\n// CORRECT - final assertion is sufficient\nassertArrayEquals(expected.getOrNull(), result.getOrNull())\n```\n\n---\n\n## Summary Checklist\n\nBefore submitting tests, verify:\n\n**Core Patterns:**\n- [ ] No `assertCoroutineThrows` inside `runTest`\n- [ ] All static mocks have `unmockk` in `@After`\n- [ ] EventFlow tests start with `expectNoEvents()`\n- [ ] Using FakeDispatcherManager, not real dispatchers\n- [ ] All coroutine tests use `runTest`\n\n**Assertion Patterns:**\n- [ ] Assert complete state objects, not individual fields\n- [ ] Use JUnit `assertTrue()`, not Kotlin `assert()`\n- [ ] Use `asSuccess()` for Result type assertions\n- [ ] Avoid redundant assertion patterns\n\n**Test Design:**\n- [ ] Test factory methods accept domain types, not SavedStateHandle\n- [ ] Use Fakes for happy paths, Mocks for error paths\n- [ ] Prefer DI patterns over static mocking\n- [ ] Test null returns from Android APIs (streams, files)\n- [ ] Fakes use `bufferedMutableSharedFlow()` with `.onSubscription`\n\n**General:**\n- [ ] Tests don't depend on execution order\n- [ ] Complex mocks use `relaxed = true`\n- [ ] Test data is created fresh for each test\n- [ ] Mocking behavior, not value objects\n- [ ] Testing observable behavior, not implementation\n\nWhen tests fail mysteriously, check these gotchas first.\n",".claude/skills/testing-android-code/references/flow-testing-patterns.md":"# Flow Testing with Turbine\n\nBitwarden Android uses Turbine for testing Kotlin Flows, including the critical distinction between StateFlow and EventFlow patterns.\n\n## StateFlow vs EventFlow\n\n### StateFlow (Replayed)\n\n**Characteristics:**\n- `replay = 1` - Always emits current value to new collectors\n- First `awaitItem()` returns the current/initial state\n- Survives configuration changes\n- Used for UI state that needs to be immediately available\n\n**Test Pattern:**\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateFlow.test {\n        // First awaitItem() gets CURRENT state\n        assertEquals(INITIAL_STATE, awaitItem())\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.LoadData)\n\n        // Next awaitItem() gets UPDATED state\n        assertEquals(LOADING_STATE, awaitItem())\n        assertEquals(SUCCESS_STATE, awaitItem())\n    }\n}\n```\n\n### EventFlow (No Replay)\n\n**Characteristics:**\n- `replay = 0` - Only emits new events after subscription\n- No initial value emission\n- One-time events (navigation, toasts, dialogs)\n- Does not survive configuration changes\n\n**Test Pattern:**\n```kotlin\n@Test\nfun `action should emit event`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.eventFlow.test {\n        // MUST call expectNoEvents() first - nothing emitted yet\n        expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.Submit)\n\n        // Now expect the event\n        assertEquals(MyEvent.NavigateToNext, awaitItem())\n    }\n}\n```\n\n**Critical:** Always call `expectNoEvents()` before triggering actions on EventFlow. Forgetting this causes flaky tests.\n\n## Testing State and Events Simultaneously\n\nUse the `stateEventFlow()` helper from `BaseViewModelTest`:\n\n```kotlin\n@Test\nfun `complex action should update state and emit event`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        // Initial state\n        assertEquals(INITIAL_STATE, stateFlow.awaitItem())\n\n        // No events yet\n        eventFlow.expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.ComplexAction)\n\n        // Verify state progression\n        assertEquals(LOADING_STATE, stateFlow.awaitItem())\n        assertEquals(SUCCESS_STATE, stateFlow.awaitItem())\n\n        // Verify event emission\n        assertEquals(MyEvent.ShowToast, eventFlow.awaitItem())\n    }\n}\n```\n\n## Repository Flow Testing\n\n### Testing Database Flows\n\n```kotlin\n@Test\nfun `dataFlow should emit when database updates`() = runTest {\n    val dataFlow = MutableStateFlow(initialData)\n    every { mockDiskSource.dataFlow } returns dataFlow\n\n    repository.dataFlow.test {\n        // Initial value\n        assertEquals(initialData, awaitItem())\n\n        // Update disk source\n        dataFlow.value = updatedData\n\n        // Verify emission\n        assertEquals(updatedData, awaitItem())\n    }\n}\n```\n\n### Testing Transformed Flows\n\n```kotlin\n@Test\nfun `flow transformation should map correctly`() = runTest {\n    val sourceFlow = MutableStateFlow(UserEntity(id = \"1\", name = \"John\"))\n    every { mockDao.observeUser() } returns sourceFlow\n\n    // Repository transforms entity to domain model\n    repository.userFlow.test {\n        val expectedUser = User(id = \"1\", name = \"John\")\n        assertEquals(expectedUser, awaitItem())\n    }\n}\n```\n\n## Common Patterns\n\n### Pattern 1: Testing Initial State + Action\n\n```kotlin\n@Test\nfun `load data should update from idle to loading to success`() = runTest {\n    coEvery { repository.getData() } returns \"data\".asSuccess()\n\n    viewModel.stateFlow.test {\n        assertEquals(DEFAULT_STATE, awaitItem())\n\n        viewModel.loadData()\n\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Success), awaitItem())\n    }\n}\n```\n\n### Pattern 2: Testing Error States\n\n```kotlin\n@Test\nfun `load data with error should emit failure state`() = runTest {\n    val error = Exception(\"Network error\")\n    coEvery { repository.getData() } returns error.asFailure()\n\n    viewModel.stateFlow.test {\n        assertEquals(DEFAULT_STATE, awaitItem())\n\n        viewModel.loadData()\n\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())\n        assertEquals(\n            DEFAULT_STATE.copy(loadingState = LoadingState.Error(\"Network error\")),\n            awaitItem(),\n        )\n    }\n}\n```\n\n### Pattern 3: Testing Event Sequences\n\n```kotlin\n@Test\nfun `submit should emit validation then navigation events`() = runTest {\n    viewModel.eventFlow.test {\n        expectNoEvents()\n\n        viewModel.trySendAction(MyAction.Submit)\n\n        assertEquals(MyEvent.ShowValidation, awaitItem())\n        assertEquals(MyEvent.NavigateToNext, awaitItem())\n    }\n}\n```\n\n### Pattern 4: Testing Cancellation\n\n```kotlin\n@Test\nfun `cancelling collection should stop emissions`() = runTest {\n    val flow = flow {\n        repeat(100) {\n            emit(it)\n            delay(100)\n        }\n    }\n\n    flow.test {\n        assertEquals(0, awaitItem())\n        assertEquals(1, awaitItem())\n\n        // Cancel after 2 items\n        cancel()\n\n        // No more items received\n    }\n}\n```\n\n## Anti-Patterns\n\n### ❌ Forgetting expectNoEvents() on EventFlow\n\n```kotlin\n// WRONG\nviewModel.eventFlow.test {\n    viewModel.trySendAction(action)  // May fail - no initial expectNoEvents\n    assertEquals(event, awaitItem())\n}\n\n// CORRECT\nviewModel.eventFlow.test {\n    expectNoEvents()  // ALWAYS do this first\n    viewModel.trySendAction(action)\n    assertEquals(event, awaitItem())\n}\n```\n\n### ❌ Not Using runTest\n\n```kotlin\n// WRONG - Missing runTest\n@Test\nfun `test flow`() {\n    flow.test { /* ... */ }\n}\n\n// CORRECT\n@Test\nfun `test flow`() = runTest {\n    flow.test { /* ... */ }\n}\n```\n\n### ❌ Mixing StateFlow and EventFlow Patterns\n\n```kotlin\n// WRONG - Treating StateFlow like EventFlow\nstateFlow.test {\n    expectNoEvents()  // Unnecessary - StateFlow always has value\n    /* ... */\n}\n\n// WRONG - Treating EventFlow like StateFlow\neventFlow.test {\n    val item = awaitItem()  // Will hang - no initial value!\n    /* ... */\n}\n```\n\n## Reference Implementations\n\n**ViewModel with StateFlow and EventFlow:**\n`app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n**Repository Flow Testing:**\n`data/src/test/kotlin/com/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt`\n\n**Complex Flow Transformations:**\n`data/src/test/kotlin/com/bitwarden/data/vault/repository/VaultRepositoryTest.kt`\n",".claude/skills/testing-android-code/references/test-base-classes.md":"# Test Base Classes Reference\n\nBitwarden Android provides specialized base classes that configure test environments and provide helper utilities.\n\n## BaseViewModelTest\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`\n\n### Purpose\nProvides essential setup for testing ViewModels with proper coroutine dispatcher configuration and Flow testing helpers.\n\n### Automatic Configuration\n- Registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`\n- Ensures deterministic coroutine execution in tests\n- All coroutines complete immediately without real delays\n\n### Key Feature: stateEventFlow() Helper\n\n**Use Case:** When you need to test both StateFlow and EventFlow simultaneously.\n\n```kotlin\n@Test\nfun `complex action should update state and emit event`() = runTest {\n    val viewModel = ExampleViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        // Verify initial state\n        assertEquals(INITIAL_STATE, stateFlow.awaitItem())\n\n        // No events yet\n        eventFlow.expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(ExampleAction.ComplexAction)\n\n        // Verify state updated\n        assertEquals(LOADING_STATE, stateFlow.awaitItem())\n\n        // Verify event emitted\n        assertEquals(ExampleEvent.ShowToast, eventFlow.awaitItem())\n    }\n}\n```\n\n### Usage Pattern\n\n```kotlin\nclass MyViewModelTest : BaseViewModelTest() {\n    private val mockRepository: MyRepository = mockk()\n    private val savedStateHandle = SavedStateHandle(\n        mapOf(KEY_STATE to INITIAL_STATE)\n    )\n\n    @Test\n    fun `test action`() = runTest {\n        val viewModel = MyViewModel(\n            savedStateHandle = savedStateHandle,\n            repository = mockRepository\n        )\n\n        // Test with automatic dispatcher setup\n        viewModel.stateFlow.test {\n            assertEquals(INITIAL_STATE, awaitItem())\n        }\n    }\n}\n```\n\n## BitwardenComposeTest\n\n**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`\n\n### Purpose\nPre-configured test class for Compose UI tests with all Bitwarden managers and theme setup.\n\n### Automatic Configuration\n- All Bitwarden managers pre-configured (FeatureFlags, AuthTab, Biometrics, etc.)\n- Wraps content in `BitwardenTheme` and `LocalManagerProvider`\n- Provides fixed `Clock` for deterministic time-based tests\n- Extends `BaseComposeTest` for Robolectric and dispatcher setup\n\n### Key Features\n\n**Pre-configured Managers:**\n- `FeatureFlagManager` - Controls feature flag behavior\n- `AuthTabManager` - Manages auth tab state\n- `BiometricsManager` - Handles biometric authentication\n- `ClipboardManager` - Clipboard operations\n- `NotificationManager` - Notification display\n\n**Fixed Clock:**\nAll tests use a fixed clock for deterministic time-based testing:\n```kotlin\n// Tests use consistent time: 2023-10-27T12:00:00Z\nval fixedClock: Clock\n```\n\n### Usage Pattern\n\n```kotlin\nclass MyScreenTest : BitwardenComposeTest() {\n    private var haveCalledNavigateBack = false\n    private val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n    private val viewModel = mockk<MyViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        setContent {\n            MyScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                viewModel = viewModel\n            )\n        }\n    }\n\n    @Test\n    fun `on back click should send action`() {\n        composeTestRule.onNodeWithContentDescription(\"Back\").performClick()\n        verify { viewModel.trySendAction(MyAction.BackClick) }\n    }\n\n    @Test\n    fun `loading state should show progress`() {\n        mutableStateFlow.value = DEFAULT_STATE.copy(isLoading = true)\n        composeTestRule.onNode(isProgressBar).assertIsDisplayed()\n    }\n}\n```\n\n### Important: bufferedMutableSharedFlow for Events\n\nIn Compose tests, use `bufferedMutableSharedFlow` instead of regular `MutableSharedFlow` (default replay is 0):\n\n```kotlin\n// Correct for Compose tests\nprivate val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()\n\n// This allows triggering events and having the UI react\nmutableEventFlow.tryEmit(MyEvent.NavigateBack)\n```\n\n## BaseServiceTest\n\n**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt`\n\n### Purpose\nProvides MockWebServer setup for testing API service implementations.\n\n### Automatic Configuration\n- `server: MockWebServer` - Auto-started before each test, stopped after\n- `retrofit: Retrofit` - Pre-configured with:\n  - JSON converter (kotlinx.serialization)\n  - NetworkResultCallAdapter for Result<T> responses\n  - Base URL pointing to MockWebServer\n- `json: Json` - kotlinx.serialization JSON instance\n\n### Usage Pattern\n\n```kotlin\nclass MyServiceTest : BaseServiceTest() {\n    private val api: MyApi = retrofit.create()\n    private val service = MyServiceImpl(api)\n\n    @Test\n    fun `getConfig should return success when API succeeds`() = runTest {\n        // Enqueue mock response\n        server.enqueue(MockResponse().setBody(EXPECTED_JSON))\n\n        // Call service\n        val result = service.getConfig()\n\n        // Verify result\n        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)\n    }\n\n    @Test\n    fun `getConfig should return failure when API fails`() = runTest {\n        // Enqueue error response\n        server.enqueue(MockResponse().setResponseCode(500))\n\n        // Call service\n        val result = service.getConfig()\n\n        // Verify failure\n        assertTrue(result.isFailure)\n    }\n}\n```\n\n### MockWebServer Patterns\n\n**Enqueue successful response:**\n```kotlin\nserver.enqueue(MockResponse().setBody(\"\"\"{\"key\": \"value\"}\"\"\"))\n```\n\n**Enqueue error response:**\n```kotlin\nserver.enqueue(MockResponse().setResponseCode(404))\nserver.enqueue(MockResponse().setResponseCode(500))\n```\n\n**Enqueue delayed response:**\n```kotlin\nserver.enqueue(\n    MockResponse()\n        .setBody(\"\"\"{\"key\": \"value\"}\"\"\")\n        .setBodyDelay(1000, TimeUnit.MILLISECONDS)\n)\n```\n\n**Verify request details:**\n```kotlin\nval request = server.takeRequest()\nassertEquals(\"/api/config\", request.path)\nassertEquals(\"GET\", request.method)\nassertEquals(\"Bearer token\", request.getHeader(\"Authorization\"))\n```\n\n## BaseComposeTest\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseComposeTest.kt`\n\n### Purpose\nBase class for Compose tests that extends `BaseRobolectricTest` and provides `setTestContent()` helper.\n\n### Features\n- Robolectric configuration for Compose\n- Proper dispatcher setup\n- `composeTestRule` for UI testing\n- `setTestContent()` helper wraps content in theme\n\n### Usage\nTypically you'll extend `BitwardenComposeTest` which extends this class. Use `BaseComposeTest` directly only for tests that don't need Bitwarden-specific manager configuration.\n\n## When to Use Each Base Class\n\n| Test Type | Base Class | Use When |\n|-----------|------------|----------|\n| ViewModel tests | `BaseViewModelTest` | Testing ViewModel state and events |\n| Compose screen tests | `BitwardenComposeTest` | Testing Compose UI with Bitwarden components |\n| API service tests | `BaseServiceTest` | Testing network layer with MockWebServer |\n| Repository tests | None (manual setup) | Testing repository logic with mocked dependencies |\n| Utility/helper tests | None (manual setup) | Testing pure functions or utilities |\n\n## Complete Examples\n\n**ViewModel Test:**\n`../examples/viewmodel-test-example.md`\n\n**Compose Screen Test:**\n`../examples/compose-screen-test-example.md`\n\n**Repository Test:**\n`../examples/repository-test-example.md`\n","docs/ARCHITECTURE.md":"# Architecture\n\n- [Overview](#overview)\n- [Application Modules](#application-modules)\n  - [Core Library Modules](#core-library-modules)\n  - [Specialized Library Modules](#specialized-library-modules)\n  - [Supporting Modules (Source Sets and Test Fixtures)](#supporting-modules-source-sets-and-test-fixtures)\n- [Data Layer](#data-layer)\n  - [Data Sources](#data-sources)\n  - [Managers](#managers)\n  - [Repositories](#repositories)\n  - [Note on Dependency Injection](#note-on-dependency-injection)\n- [UI Layer](#ui-layer)\n  - [ViewModels / MVVM](#viewmodels--mvvm)\n    - [Example](#example)\n  - [Screens / Compose](#screens--compose)\n    - [State Hoisting](#state-hoisting)\n    - [Example](#example-1)\n  - [Navigation](#navigation)\n    - [State-based Navigation](#state-based-navigation)\n    - [Event-based Navigation](#event-based-navigation)\n    - [Navigation Implementation](#navigation-implementation)\n    - [Example](#example-2)\n\n## Overview\n\nThe Bitwarden Android project employs a multi-module architecture to enhance separation of concerns, maintainability, and build efficiency. This architecture distinguishes between application modules, which are user-facing, and library modules, which provide shared functionalities.\n\n### Application modules\nThese modules represent the entry points and primary user interfaces for the applications. They are structured with standard Android source sets to organize code for different build types and testing:\n\n* **`Bitwarden.app`**: This module constitutes the Bitwarden Password Manager application. It integrates various library modules to deliver password management and secure data storage features. It is responsible for the application's top-level setup, primary navigation graph, and dependency injection rooting for password manager functionalities. The `app` module utilizes product flavors to generate different versions of the application:\n    * `src/standard/`: Contains code and resources specific to the 'standard' product flavor. Builds produced from this flavor are intended for distribution via the Google Play Store and include functionality dependent on Google Play Services.\n    * `src/fdroid/`: Contains code and resources specific to the 'fdroid' product flavor. Builds produced from this flavor are intended for distribution via F-Droid. This flavor is stripped of all Google Play Services libraries and, as such, does not contain features like push notifications for real-time syncing.\n    * `src/debug/`: Contains code and resources specific to the 'debug' build type (e.g., for enabling debug-specific features, configurations, or alternative API endpoints), which can be combined with any product flavor.\n    * `src/release/`: Contains code and resources specific to the 'release' build type (e.g., for ProGuard/R8 configurations, release-specific API keys, or analytics configurations), which can be combined with any product flavor.\n* **`Bitwarden.authenticator`**: This module implements the Bitwarden Authenticator application. Its purpose is to store and generate two-factor authentication (2FA) codes, such as Time-based One-Time Passwords (TOTP), directly on the user's device. It provides the user interface and logic for managing authenticator accounts and displaying codes, functioning as a standalone authenticator application.\n    * `src/debug/`: Contains code and resources specific to the 'debug' build type of the Authenticator.\n    * `src/release/`: Contains code and resources specific to the 'release' build type of the Authenticator.\n\n### Core Library Modules\n\nThese libraries provide foundational capabilities utilized by application modules and other feature-specific libraries:\n\n* **`Bitwarden.core`**: A fundamental library containing shared business logic, essential data models (entities, value objects), domain-specific utilities, and common interfaces. It is designed with minimal or no direct Android framework dependencies to ensure high reusability and testability.\n* **`Bitwarden.data`**: Manages the data layer for the applications. Responsibilities include defining repositories, data sources (for network APIs and local persistence like Room), data transformation logic (mappers), and managing data synchronization. It abstracts data origin and storage details from other parts of the system.\n* **`Bitwarden.network`**: Encapsulates all network communication logic. This includes API client setup (e.g., using Retrofit or Ktor), defining request/response models for network operations, and handling network interceptors and authentication.\n* **`Bitwarden.ui`**: A library module providing common UI elements, theming resources (colors, typography, shapes), custom Jetpack Compose components, base UI classes, and UI-related utility functions. It ensures a consistent user experience across different features and applications.\n\n### Specialized Library Modules\n\nThese libraries offer more targeted functionalities:\n\n* **`Bitwarden.authenticatorbridge`**: Serves as an interface or bridge facilitating communication and integration between the `Bitwarden.authenticator` module's functionalities and the`Bitwarden.app` module. This allows for controlled interaction while maintaining modular decoupling.\n* **`Bitwarden.annotation`**: Contains custom Java/Kotlin annotations used throughout the project. These annotations support compile-time code generation (e.g., for dependency injection with Dagger Hilt, or database schema generation with Room) and provide metadata for other development tools.\n\n### Supporting Modules (Source Sets and Test Fixtures)\n\nThe project structure includes conventional Gradle source sets and test fixture modules that support\ndevelopment and testing:\n\n* **`src/main/`**: The primary source set of a module, containing its functional code.\n* **`src/test/`**: Contains local unit tests (JVM-based) for the associated module.\n* **`src/testFixtures/`**: Provides shared testing utilities, mock objects, sample data, or helper\n  functions reusable across various test modules.\n\n## Data Layer\n\nThe data layer is where all the UI-independent data is stored and retrieved. It consists of both raw data sources as well as higher-level \"repository\" and \"manager\" classes.\n\nNote that any functions exposed by a data layer class that must perform asynchronous work do so by exposing **suspending functions** that may run inside [coroutines](https://kotlinlang.org/docs/coroutines-guide.html) while any streaming sources of data are handled by exposing [Flows](https://kotlinlang.org/docs/flow.html).\n\n### Data Sources\n\nThe lowest level of the data layer are the \"data source\" classes. These are the raw sources of data that include data persisted in [Room](https://developer.android.com/jetpack/androidx/releases/room3) / [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences), data retrieved from network requests using [Retrofit](https://github.com/square/retrofit), and data retrieved via interactions with the [Bitwarden SDK](https://github.com/bitwarden/sdk).\n\nNote that these data sources are constructed in a manner that adheres to a very important principle of the app: **that function calls should not throw exceptions** (see the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--kotlin) for more details.) In the case of data sources, this tends to mean that suspending functions like those representing network requests or Bitwarden SDK calls should return a [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/) type. This is an important responsibility of the data layer as a wrapper around other third party libraries, as dependencies like Retrofit and the Bitwarden SDK tend to throw exceptions to indicate errors instead.\n\n### Managers\n\nManager classes represent something of a middle level of the data layer. While some manager classes like [VaultLockManager](../app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt) depend on the the lower-level data sources, others are wrappers around OS-level classes (ex: [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt)) while others have no dependencies at all (ex: [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt)). The commonality amongst the manager classes is that they tend to have a single discrete responsibility. These classes may also exist solely in the data layer for use inside a repository or manager class, like [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt), or may be exposed directly to the UI layer, like [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt).\n\n### Repositories\n\nRepository classes represent the outermost level of the data layer. They can take data sources, managers, and in rare cases even other repositories as dependencies and are meant to be exposed directly to the UI layer. They synthesize data from multiple sources and combine various asynchronous requests as necessary in order to expose data to the UI layer in a more appropriate form. These classes tend to have broad responsibilities that generally cover a major domain of the app, such as authentication ([AuthRepository](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt)) or vault access ([VaultRepository](../app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt)).\n\nRepository classes also feature functions that do not throw exceptions, but unlike the lower levels of the data layer the `Result` type should be avoided in favor of custom sealed classes that represent the various success/error cases in a more processed form. Returning raw `Throwable`/`Exception` instances as part of \"error\" states should be avoided when possible.\n\nIn some cases a source of data may be continuously observed and in these cases a repository may choose to expose a [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) that emits data updates using the [DataState](../app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/DataState.kt) wrapper.\n\n### Note on Dependency Injection\n\nNearly all classes in the data layer consist of interfaces representing exposed behavior and a corresponding `...Impl` class implementing that interface (ex: [AuthDiskSource](../app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt) / [AuthDiskSourceImpl](../app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt)). All `...Impl` classes are intended to be manually constructed while their associated interfaces are provided for dependency injection via a [Hilt Module](https://dagger.dev/hilt/modules.html) (ex: [PlatformNetworkModule](../app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt)). This prevents the `...Impl` classes from being injected by accident and allows the interfaces to be easily mocked/faked in tests.\n\n## UI Layer\n\nThe UI layer adheres to the concept of [unidirectional data flow](https://developer.android.com/develop/ui/compose/architecture#udf) and makes use of the MVVM design pattern. Both concepts are in line what Google currently recommends as the best approach for building the UI-layer of a modern Android application and this allows us to make use of all the available tooling Google provides as part of the [Jetpack suite of libraries](https://developer.android.com/jetpack). The MVVM implementation is built around the Android [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) class and the UI itself is constructed using the [Jetpack Compose](https://developer.android.com/develop/ui/compose), a declarative UI framework specifically built around the unidirectional data flow approach.\n\nEach screen in the app is associated with at least the following three classes/files:\n\n- A `...ViewModel` class responsible for managing the data and state for the screen.\n- A `...Screen` class that contains the Compose implementation of the UI.\n- A `...Navigation` file containing the details for how to add the screen to the overall navigation graph and how navigate to it within the graph.\n\n### ViewModels / MVVM\n\nThe app's approach to MVVM is based around the handling of \"state\", \"actions\", and \"events\" and is encoded in the [BaseViewModel](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/BaseViewModel.kt) class.\n\n- **State:** The \"state\" represents the complete internal and external **total state** of the ViewModel (VM). Any and all information needed to configure the UI that is associated with the VM should be included in this state and is exposed via the `BaseViewModel.stateFlow` property as `StateFlow<S>` (where `S` represents the unique type of state associated with the VM). This state is typically a combination of state from the data layer (such as account information) and state from the UI layer (such as data entered into a text field).\n\n  There should be no additional `StateFlow` exposed from a VM that would represent some other kind of state; all state should be represented by `S`. Additionally, any internal state not directly needed by the UI but which influences the behavior of the VM should be included as well in order to keep all state managed by the VM in a single place.\n\n- **Actions:** The \"actions\" represent interactions with the VM in some way that could potentially cause an update to that total state. These can be external actions coming from the user's interaction with the UI, like click events, or internal actions coming from some asynchronous process internal to the VM itself, like the result of some suspending functions. Actions are sent by interacting directly with `BaseViewModel.actionChannel` or by using the `BaseViewModel.sendAction` and `BaseViewModel.trySendAction` helpers. All actions are then processed synchronously in a queue in the `handleAction` function.\n\n  It is worth emphasizing that state should never be updated inside a coroutine in a VM; all asynchronous work that results in a state update should do so by posting an internal action. This ensures that the only place that state changes can occur is synchronously inside the `handleAction` function. This makes the process of finding and reasoning about state changes easier and simplifies debugging.\n\n- **Events:** The \"events\" represent discrete, one-shot side-effects and are typically associated with navigation events triggered by some user action. They are sent internally using `BaseViewModel.sendEvent` and may be consumed by the UI layer via the `BaseViewModel.eventFlow` property. An [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) should typically be used to simplify the consumption of these events.\n\nVMs are typically injected using [Dagger Hilt](https://developer.android.com/training/dependency-injection/hilt-android) by annotating them with `@HiltViewModel`. This allows them to be constructed, retrieved, and cached in the Compose UI layer by calling `hiltViewModel()` with the appropriate type. Dependencies passed into the VM constructor will typically be singletons of the graph, such as [AuthRepository](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt). In cases where the VM needs to initialized with some specific data (like an ID) that is sent from the previous screen, this data should be retrieved by injecting the [SavedStateHandle](https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate) and pulling out data using a type-safe wrapper (ex: [LoginArgs](../app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt)).\n\n#### Example\n\nThe following is an example that demonstrates many of the above principles and best practices when using `BaseViewModel` to implement a VM. It has the following features:\n\n- It is injected with a repository (`ExampleRepository`) that provides streaming `ExampleData` and a `SavedStateHandle` in order to pull initial data using a `ExampleArgs` wrapper class.\n- It has state that manages several properties of interest to the UI: `exampleData`, `isToggledEnabled`, and `dialogState`.\n- It receives external actions from the UI (`ContinueButtonClick` and `ToggleValueUpdate`) as well as internal actions launch from inside different coroutines coroutines (`Internal.ExampleDataReceive`, `Internal.ToggleValueSync`). These actions result in state updates or the emission of an event (`NavigateToNextScreen`).\n- It saves the current state to the `SavedStateHandle` in order to restore it later after process death and restoration. This ensures the user's actions and associated state will not be lost if the app goes into the background and is temporarily killed by the OS to conserve memory.\n\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\nprivate const val KEY_STATE = \"state\"\n\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    private val exampleRepository: ExampleRepository,\n    private val savedStateHandle: SavedStateHandle,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    // If previously saved state data is present in the SavedStateHandle, use that as the initial\n    // state, otherwise initialize from repository and navigation data.\n    initialState = savedStateHandle[KEY_STATE] ?: ExampleState(\n        exampleData = exampleRepository.exampleDataStateFlow.value,\n        isToggleEnabled = ExampleArgs(savedStateHandle).isToggleEnabledInitialValue,\n        dialogState = null,\n    )\n) {\n    init {\n        // As the state updates, write to saved state handle for retrieval after process death and\n        // restoration.\n        stateFlow\n            .onEach { savedStateHandle[KEY_STATE] = it }\n            .launchIn(viewModelScope)\n\n        exampleRepository\n            .exampleDataStateFlow\n            // Asynchronously received data is converted to an internal action and sent in order to\n            // be handled by `handleAction`.\n            .map { ExampleAction.Internal.ExampleDataReceive(it) }\n            .onEach(::sendAction)\n            .launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        when (action) {\n            is ExampleAction.ContinueButtonClick -> handleContinueButtonClick()\n            is ExampleAction.ToggleValueUpdate -> handleToggleValueUpdate(action)\n            is ExampleAction.Internal -> handleInternalAction(action)\n        }\n    }\n\n    private fun handleContinueButtonClick() {\n        // Update the state to show a dialog\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Loading,\n            )\n        }\n\n        // Run a suspending call in a coroutine to fetch data and post the result back as an\n        // internal action for further processing so that all state changes and event emissions\n        // happen synchronously in `handleAction`.\n        viewModelScope.launch {\n            val completionData = exampleRepository\n                .fetchCompletionData(isToggleEnabled = state.isToggleEnabled)\n\n            sendAction(\n                ExampleAction.Internal.CompletionDataReceive(\n                    completionData = completionData,\n                )\n            )\n        }\n    }\n\n    private fun handleToggleValueUpdate(action: ExampleAction.ToggleValueUpdate) {\n        // Update the state\n        mutableStateFlow.update {\n            it.copy(\n                isToggleEnabled = action.isToggleEnabled,\n            )\n        }\n    }\n\n    private fun handleInternalAction(action: ExampleAction.Internal) {\n        when (action) {\n          is ExampleAction.Internal.CompletionDataReceive -> handleCompletionDataReceive(action)\n          is ExampleAction.Internal.ExampleDataReceive -> handleExampleDataReceive(action)\n        }\n    }\n\n    private fun handleCompletionDataReceive(action: ExampleAction.Internal.CompletionDataReceive) {\n        // Update the state to clear the dialog\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = null,\n            )\n        }\n\n        // Send event with data from the action to navigate\n        sendEvent(\n            ExampleEvent.NavigateToNextScreen(\n                completionData = action.completionData,\n            )\n        )\n    }\n\n    private fun handleExampleDataReceive(action: ExampleAction.Internal.ExampleDataReceive) {\n        // Update the state\n        mutableStateFlow.update {\n            it.copy(\n                exampleData = action.exampleData,\n            )\n        }\n    }\n}\n\n@Parcelize\ndata class ExampleState(\n    val exampleData: String,\n    val isToggleEnabled: Boolean,\n    val dialogState: DialogState?,\n) : Parcelable {\n\n    sealed class DialogState : Parcelable {\n        @Parcelize\n        data object Loading : DialogState()\n    }\n}\n\nsealed class ExampleEvent {\n    data class NavigateToNextScreen(\n        val completionData: CompletionData,\n    ) : ExampleEvent()\n}\n\nsealed class ExampleAction {\n    data object ContinueButtonClick : ExampleAction()\n\n    data class ToggleValueUpdate(\n        val isToggleEnabled: Boolean,\n    ) : ExampleAction()\n\n    sealed class Internal : ExampleAction() {\n        data class CompletionDataReceive(\n          val completionData: CompletionData,\n        ) : Internal()\n\n        data class ExampleDataReceive(\n            val exampleData: String,\n        ) : Internal()\n    }\n}\n```\n</details>\n\n\n### Screens / Compose\n\nEach unique screen destination is represented by a composable `...Screen` function annotated with `@Composable`. The responsibilities of this layer include:\n\n- Receiving \"state\" data from the associated `ViewModel` and rendering the UI accordingly.\n- Receiving \"events\" from the associated `ViewModel` and taking the corresponding action, which is typically to trigger a passed-in callback function for navigation purposes. An [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) should be used to simplify this process.\n- Sending \"actions\" to the `ViewModel` indicating user interactions or UI-layer events as necessary.\n- Interacting with purely UI-layer \"manager\" classes that are not appropriate or possible to be injected into the `ViewModel` (such as those concerning permissions or camera access).\n\nIn order to both provide consistency to the app and to simplify the development of new screens, an extensive system of reusable composable components has been developed and may be found in the [components package](../app/src/main/java/com/x8bit/bitwarden/ui/platform/components). These tend to bear the prefix `Bitwarden...` to easily distinguish them from similar OS-level components. If there is any new unique UI component that is added, it should always be considered if it should be made a shareable component.\n\nRefer to the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--jetpack-compose) for additional information on best practices when using Compose.\n\n#### State-hoisting\n\nJetpack Compose is built around the idea that the state required to render any given composable function can be \"hoisted\" to higher levels that may need access to that state. This means that the responsibility for keeping track of the current state of a component may not typically reside with the component itself. It is important, therefore, to understand where the best place is to manage any given piece of UI state.\n\nThis is discussed in detail in [Google's state-hoisting documentation](https://developer.android.com/develop/ui/compose/state-hoisting) and can be summarized roughly as follows: **state should be hoisted as high as is necessary to perform any relevant logic and no higher.** This is a pattern that is followed in the Bitwarden app and tends to lead to the following:\n\n- Any state that will eventually need to be used by the VM should be hoisted to the VM. For example, any text input, toggle values, etc. that may be updated by user interactions should be completely controlled by the VM. These state changes are communicated via the \"actions\" that the Compose layer sends, which trigger corresponding updates the VM's total \"state\".\n- Any UI state that will not be used by logic inside the VM should remain in the UI layer. For example, visibility toggle states for password inputs should typically remain out of the VM.\n\nThese rules can lead to some notable differences in how certain dialogs are handled. For example, loading dialogs are controlled by events that occur in the VM, such as when a network request is started and when it completes. This requires the VM to be in charge of managing the visibility state of the dialog. However, some dialogs appear as the result of clicking on an item in the UI in order to simply display information or to ask for the user's confirmation before some action is taken. Because there is no logic in the VM that depends on the visibility of these particular dialogs, their visibility state can be controlled by the UI. Note, however, that any user interaction that results in a navigation to a new screen should always be routed to the VM first, as the VM should always be in charge of triggering changes at the per-screen level.\n\n#### Example\n\nThe following shows off the basic structure of a composable `...Screen` implementation. Note that:\n\n- The VM is \"injected\" using the `hiltViewModel()` helper function. This will correctly create, cache, and scope the VM in production code while making it easier to pass in mock/fake implementations in test code.\n- An `onNavigateToNextScreen` function is also passed in, which can be called to trigger a navigation via a call to a [NavController](https://developer.android.com/reference/androidx/navigation/NavController) in the outer layers of the navigation graph. Passing in a function rather than the `NavController` itself decouples the screen code from the navigation framework and greatly simplifies the testing of screens.\n- The VM \"state\" is consumed using `viewModel.stateFlow.collectAsStateWithLifecycle()`. This will cause the composable to \"recompose\" and update whenever updates are pushed to the `viewModel.stateFlow`.\n- The VM \"events\" are consumed using an [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) and demonstrate how the `onNavigateToNextScreen` may be triggered.\n- The current state of the text, switch, and button are hoisted to the VM and pulled out from the `state`. User interactions with the switch and button result in \"actions\" being sent to the VM using `viewModel.trySendAction`.\n- Reusable components (`BitwardenLoadingDialog`, `BitwardenSwitch`, and `BitwardenFilledButton`) are used where possible in order to build the screen using the correct theming and reduce code duplication. When this is not possible (such as when rending the `Text` composable) all colors and styles are pulled from the BitwardenTheme object.\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\n@Composable\nfun ExampleScreen(\n    onNavigateToNextScreen: (CompletionData) -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    // Collect state\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    // Handle events\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            is ExampleEvent.NavigateToNextScreen -> {\n                onNavigateToNextScreen(event.completionData)\n            }\n        }\n    }\n\n    // Show state-based dialogs if necessary\n    when (state.dialogState) {\n        ExampleState.DialogState.Loading -> {\n            BitwardenLoadingDialog(\n                visibilityState = LoadingDialogState.Shown(\n                    text = R.string.loading.asText(),\n                )\n            )\n        }\n\n        else -> Unit\n    }\n\n    // Render the remaining state\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = Modifier\n            .verticalScroll(rememberScrollState()),\n    ) {\n        Text(\n            text = state.exampleData,\n            textAlign = TextAlign.Center,\n            style = BitwardenTheme.typography.headlineSmall,\n            color = BitwardenTheme.colorScheme.textColors.primary,\n            modifier = Modifier\n                .padding(horizontal = 24.dp)\n                .wrapContentHeight(),\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        BitwardenSwitch(\n            label = stringResource(id = R.string.toggle_label),\n            isChecked = state.isToggleEnabled,\n            onCheckedChange = { viewModel.trySendAction(ExampleAction.ToggleValueUpdate(it)) },\n            modifier = Modifier\n                .padding(horizontal = 16.dp)\n                .fillMaxWidth(),\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        BitwardenFilledButton(\n            label = stringResource(id = R.string.continue_text),\n            onClick = { viewModel.trySendAction(ExampleAction.ContinueButtonClick) },\n            modifier = Modifier\n                .padding(horizontal = 16.dp)\n                .fillMaxWidth(),\n        )\n    }\n}\n```\n</details>\n\n### Navigation\n\nNavigation in the app is achieved via the [Compose Navigation](https://developer.android.com/develop/ui/compose/navigation) component. This involves specifying \"destinations\" within a [NavHost](https://developer.android.com/reference/androidx/navigation/NavHost) and using a [NavController](https://developer.android.com/reference/androidx/navigation/NavController) to trigger actual navigation events. The app uses a mix of state-based navigation at the highest level (in order to determine the overall app flow the user is in) and event-based navigation (to handle navigation within a flow based on user interactions).\n\n#### State-based Navigation\n\nState-based navigation is handled by the [RootNavViewModel](../app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt) and [RootNavScreen](../app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt). The current navigation state is determined within the `RootNavViewModel` by monitoring the overall [UserState](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt) along with any [SpecialCircumstance](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt). The `UserState` encodes whether there are any user accounts, whether they are logged in, etc., while the `SpecialCircumstance` describes a particular circumstance under which the app may have been launched (such as being opened in order to manually select an autofill value). The `RootNavState` calculated by the `RootNavViewModel` is then used by the `RootNavScreen` in order to trigger a navigation only if the overall state has changed.\n\nThe benefit of state-based navigation at this level is that it allows for a variety of state changes that may happen deep within the data layer to automatically trigger navigation to the correct top-level flow without the rest of the UI needing to be concerned with these details. For example, when logging out the current user, the data layer can decide the new state and the `RootNavViewModel` can decide where to go next, rather than having the screen that triggered the logout action choose the next navigation state (which would then need to be redundantly handled in multiple places in the app).\n\nState-based navigation should be limited to specific cases that affect the overall flow of the app and are driven by data emitted from the data layer. All other navigation should use event-based navigation.\n\n#### Event-based Navigation\n\nEvent-based navigation is ultimately the result of some \"action\" sent to a VM, whether that comes directly from a user-interaction with the current screen or from some internal action of the VM sent as the result of some asynchronous call. The VM will send an \"event\" to the Compose layer, which will trigger a callback that will eventually make a call to a [NavController](https://developer.android.com/reference/androidx/navigation/NavController).\n\n#### Navigation Implementation\n\nAt its most basic level, Compose Navigation relies on constructing String-based URIs with a root path to specify a destination and both path and query parameters to handle the passing of required and optional parameters. This process can typically be very error prone. The Bitwarden app borrows a pattern best exemplified by the [Now In Android sample project](https://github.com/android/nowinandroid) in order to make this process more type-safe. This consists of:\n\n- A type-safe `...Destination` extension function on [NavGraphBuilder](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) for any single screen destination or a type-safe `...Graph` extension function on [NavGraphBuilder](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) for any nested graph destination.\n- A type-safe `navigateTo...` extension function on [NavController](https://developer.android.com/reference/androidx/navigation/NavController) for any destination that is not simply the root of a graph.\n- A type-safe `...Args` class that pulls data from a `SavedStateHandle` for consumption by a VM.\n\nThese are all then grouped into a single `...Navigation.kt` file where all of the raw String-based details can be hidden and only the exposed type-safe functions must be used by the rest of the app.\n\n#### Example\n\nThe following example demonstrates a sample `ExampleNavigation.kt` file with:\n\n- An `ExampleArgs` class that wraps `SavedStateHandle` and allows for the `isToggleEnabledInitialValue` to be extracted in a type-safe way.\n- A `NavGraphBuilder.exampleDestination` extension function that can be used to safely add the `ExampleScreen` to the navigation graph.\n- A `NavController.navigateToExample` extension function that can be called to navigate to the `ExampleScreen` in a type-safe way.\n\nNote in particular how consumers of the above **do not need to know the details of the actual route or path parameter**.\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\n@Serializable\ndata class ExampleRoute(\n    val isToggleEnabled: Boolean,\n)\n\ndata class ExampleArgs(\n    val isToggleEnabledInitialValue: Boolean,\n)\n\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(isToggleEnabledInitialValue = route.isToggleEnabled)\n}\n\nfun NavGraphBuilder.exampleDestination(\n    onNavigateToNextScreen: (CompletionData) -> Unit,\n) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(onNavigateToNextScreen = onNavigateToNextScreen)\n    }\n}\n\nfun NavController.navigateToExample(\n    isToggleEnabled: Boolean,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(\n        route = ExampleRoute(isToggleEnabled = isToggleEnabled),\n        navOptions = navOptions,\n    )\n}\n```\n\n</details>\n",".cursorrules (android-jetpack-compose-cursorrules-prompt-file.mdc)":"---\ndescription: \"Cursor rules for Android development with Jetpack Compose integration.\"\nglobs: **/*\nalwaysApply: false\n---\n// Android Jetpack Compose .cursorrules\n\n// Flexibility Notice\n\n// Note: This is a recommended project structure, but be flexible and adapt to existing project structures.\n// Do not enforce these structural patterns if the project follows a different organization.\n// Focus on maintaining consistency with the existing project architecture while applying Jetpack Compose best practices.\n\n// Project Architecture and Best Practices\n\nconst androidJetpackComposeBestPractices = [\n    \"Adapt to existing project architecture while maintaining clean code principles\",\n    \"Follow Material Design 3 guidelines and components\",\n    \"Implement clean architecture with domain, data, and presentation layers\",\n    \"Use Kotlin coroutines and Flow for asynchronous operations\",\n    \"Implement dependency injection using Hilt\",\n    \"Follow unidirectional data flow with ViewModel and UI State\",\n    \"Use Compose navigation for screen management\",\n    \"Implement proper state hoisting and composition\",\n];\n\n// Folder Structure\n\n// Note: This is a reference structure. Adapt to the project's existing organization\n\nconst projectStructure = `\napp/\n  src/\n    main/\n      java/com/package/\n        data/\n          repository/\n          datasource/\n          models/\n        domain/\n          usecases/\n          models/\n          repository/\n        presentation/\n          screens/\n          components/\n          theme/\n          viewmodels/\n        di/\n        utils/\n      res/\n        values/\n        drawable/\n        mipmap/\n    test/\n    androidTest/\n`;\n\n// Compose UI Guidelines\n\nconst composeGuidelines = `\n1. Use remember and derivedStateOf appropriately\n2. Implement proper recomposition optimization\n3. Use proper Compose modifiers ordering\n4. Follow composable function naming conventions\n5. Implement proper preview annotations\n6. Use proper state management with MutableState\n7. Implement proper error handling and loading states\n8. Use proper theming with MaterialTheme\n9. Follow accessibility guidelines\n10. Implement proper animation patterns\n`;\n\n// Testing Guidelines\n\nconst testingGuidelines = `\n1. Write unit tests for ViewModels and UseCases\n2. Implement UI tests using Compose testing framework\n3. Use fake repositories for testing\n4. Implement proper test coverage\n5. Use proper testing coroutine dispatchers\n`;\n\n// Performance Guidelines\n\nconst performanceGuidelines = `\n1. Minimize recomposition using proper keys\n2. Use proper lazy loading with LazyColumn and LazyRow\n3. Implement efficient image loading\n4. Use proper state management to prevent unnecessary updates\n5. Follow proper lifecycle awareness\n6. Implement proper memory management\n7. Use proper background processing\n`;\n"},"files":{".claude/CLAUDE.md":"# Bitwarden Android - Claude Code Configuration\n\nOfficial Android application for Bitwarden Password Manager and Bitwarden Authenticator, providing secure password management, two-factor authentication, and credential autofill services with zero-knowledge encryption.\n\n## Overview\n\n- Multi-module Android application: `:app` (Password Manager), `:authenticator` (2FA TOTP generator)\n- Zero-knowledge architecture: encryption/decryption happens client-side via Bitwarden SDK\n- Target users: End-users via Google Play Store and F-Droid\n\n### Key Concepts\n\n- **Zero-Knowledge Architecture**: Server never has access to unencrypted vault data or encryption keys\n- **Bitwarden SDK**: Rust-based cryptographic SDK handling all encryption/decryption operations\n- **DataState**: Wrapper for streaming data states (Loading, Loaded, Pending, Error, NoNetwork)\n- **Result Types**: Custom sealed classes for operation results (never throw exceptions from data layer)\n- **UDF (Unidirectional Data Flow)**: State flows down, actions flow up through ViewModels\n\n---\n\n## Architecture\n\n```\nUser Request (UI Action)\n         |\n    Screen (Compose)\n         |\n    ViewModel (State/Action/Event)\n         |\n    Repository (Business Logic)\n         |\n    +----+----+----+\n    |    |    |    |\n  Disk  Network  SDK\n   |      |      |\n Room  Retrofit  Bitwarden\n  DB    APIs     Rust SDK\n```\n\n### Key Principles\n\n1. **No Exceptions from Data Layer**: All suspending functions return `Result<T>` or custom sealed classes\n2. **State Hoisting to ViewModel**: All state that affects behavior must live in the ViewModel's state\n3. **Interface-Based DI**: All implementations use interface/`...Impl` pairs with Hilt injection\n4. **Encryption by Default**: All sensitive data encrypted via SDK before storage\n\n### Core Patterns\n\n- **BaseViewModel**: Enforces UDF with State/Action/Event pattern. See `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt`.\n- **Repository Result Pattern**: Type-safe error handling using custom sealed classes for discrete operations and `DataState<T>` wrapper for streaming data.\n- **Common Patterns**: Flow collection via `Internal` actions, error handling via `when` branches, `DataState` streaming with `.map { }` and `.stateIn()`.\n\n> For complete architecture patterns, code templates, and module organization, see `docs/ARCHITECTURE.md`.\n\n---\n\n## Development Guide\n\n### Workflow Skills\n\n> **Quick start**: Use the `bitwarden-tech-lead:bitwarden-tech-lead` agent (or `/plan-android-work <task>`) to refine\n> requirements and plan,\n> then the `bitwarden-software-engineer:bitwarden-software-engineer` agent (or `/work-on-android <task>`) for implementation,\n> then `/review-android <PR#>` to review the result.\n\n## Skills & Commands\n\n| Skill | Triggers |\n|-------|---------|\n| `build-test-verify` | \"build\", \"run tests\", \"lint\", \"format\", \"verify build\" |\n| `implementing-android-code` | \"implement\", \"write code\", \"add screen\", \"create feature\" |\n| `planning-android-implementation` | \"plan implementation\", \"architecture design\", \"phased task breakdown\" |\n| `refining-android-requirements` | \"refine requirements\", \"analyze ticket\", \"gap analysis\" |\n| `reviewing-changes` | \"review\", \"code review\", \"check PR\" |\n| `testing-android-code` | \"write tests\", \"add test coverage\", \"unit test\" |\n\n| Command | Usage |\n|---------|-------|\n| `/plan-android-work <task>` | Fetch ticket → refine requirements → design implementation approach |\n| `/work-on-android <task>` | Full workflow: implement → test → verify → preflight → commit → review → PR |\n| `/review-android <PR#>` | Full review workflow: PR context gathering → Android checklist → output |\n\n---\n\n## Security Rules\n\n**MANDATORY - These rules have no exceptions:**\n\n1. **Zero-Knowledge Architecture**: Never transmit unencrypted vault data or master passwords to the server. All encryption happens client-side via the Bitwarden SDK.\n2. **No Plaintext Key Storage**: Encryption keys must be stored using Android Keystore (biometric unlock) or encrypted with PIN/master password.\n3. **Sensitive Data Cleanup**: On logout, all sensitive data must be cleared from memory and storage via `UserLogoutManager.logout()`.\n4. **Input Validation**: Validate all user inputs before processing, especially URLs and credentials.\n5. **SDK Isolation**: Use scoped SDK sources (`ScopedVaultSdkSource`) to prevent cross-user crypto context leakage.\n\n---\n\n## Code Style & Standards\n\n- **Formatter**: Android Studio with `bitwarden-style.xml` | **Line Limit**: 100 chars | **Detekt**: Enabled\n- **Naming**: `camelCase` (vars/fns), `PascalCase` (classes), `SCREAMING_SNAKE_CASE` (constants), `...Impl` (implementations)\n- **KDoc**: Required for all public APIs\n- **String Resources**: Add new strings to `:ui` module (`ui/src/main/res/values/strings.xml`). Use typographic quotes/apostrophes (`\"` `\"` `'`) not escaped ASCII (`\\\"` `\\'`). Name each resource from its own text content in `snake_case` — not with generic suffixes (`_message`, `_title`). E.g., `one_or_more_email_addresses_are_incorrect`, not `invalid_email_addresses_message`.\n\n> For complete style rules (imports, formatting, documentation, Compose conventions), see `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n---\n\n## Anti-Patterns\n\nIn addition to the Key Principles above, follow these rules:\n\n### DO\n- Map async results to internal actions before updating state\n- Inject `Clock` for time-dependent operations\n- Return early to reduce nesting\n\n### DON'T\n- Update state directly inside coroutines (use internal actions)\n- Use `any` types or suppress null safety\n- Catch generic `Exception` (catch specific types)\n- Use `e.printStackTrace()` (use Timber logging)\n- Create new patterns when established ones exist\n- Skip KDoc for public APIs\n\n---\n\n## Quick Reference\n\n- **Code style**: Full rules: `docs/STYLE_AND_BEST_PRACTICES.md`\n- **Before writing code**: Use `implementing-android-code` skill for Bitwarden-specific patterns, gotchas, and templates\n- **Before writing tests**: Use `testing-android-code` skill for test patterns and templates\n- **Building/testing**: Use `build-test-verify` skill | App tests: `./gradlew app:testStandardDebugUnitTest`\n- **Before committing**: Use `bitwarden-delivery-tools:perform-preflight` skill, then `bitwarden-delivery-tools:committing-changes` skill for message format\n- **Code review**: Use `/review-android` for the full review workflow; `reviewing-changes` skill for checklist-only\n- **Creating PRs**: Use `bitwarden-delivery-tools:creating-pull-request` skill for PR workflow and templates\n- **Troubleshooting**: See `docs/TROUBLESHOOTING.md`\n- **Architecture**: `docs/ARCHITECTURE.md` | [Bitwarden SDK](https://github.com/bitwarden/sdk) | [Jetpack Compose](https://developer.android.com/jetpack/compose) | [Hilt DI](https://dagger.dev/hilt/)\n",".claude/commands/plan-android-work.md":"---\ndescription: Guided requirements refinement and implementation planning for Bitwarden Android\nargument-hint: <Jira ticket (PM-12345), Confluence URL, or free-text description>\n---\n\n# Android Planning Workflow\n\nYou are guiding the developer through requirements refinement and implementation planning for the Bitwarden Android project. The input to plan from is:\n\n**Input**: $ARGUMENTS\n\n## Prerequisites\n\n- **Jira/Confluence access**: Fetching tickets and wiki pages requires the `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin. If the plugin is not installed, Jira ticket IDs and Confluence URLs cannot be fetched automatically.\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** The user may skip phases that are not applicable. If starting from a partially completed plan, skip to the appropriate phase.\n\n### Phase 1: Ingest Requirements\n\nParse the input to detect and fetch all available sources:\n\n**Source Detection Rules:**\n- **Jira tickets** (patterns like `PM-\\d+`, `BWA-\\d+`, `EC-\\d+`): Fetch via `get_issue` and `get_issue_comments`. Also fetch linked issue summaries (parent epic, sub-tasks, blockers) for context.\n- **Confluence URLs** (containing `atlassian.net/wiki` or confluence page IDs): Extract page ID and fetch via `get_confluence_page`. If the page is a parent page, fetch child pages via `get_child_pages` and ask the user which are relevant.\n- **Free text**: Treat as direct requirements — no fetching needed.\n- **Multiple inputs**: All are first-class sources. Fetch each independently and consolidate.\n- **Tool unavailable**: If `get_issue`, `get_confluence_page`, or other Atlassian tools are not available, inform the user that the `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin is required and prompt them to install and configure it. In the meantime, ask the user to paste the relevant content directly. Treat pasted content as free-text input.\n\n**Present a structured summary:**\n1. Sources identified and fetched (with links)\n2. Raw requirements extracted from each source\n3. Initial scope assessment (small / medium / large)\n\n**Edge cases:**\n- Jira ticket with no description → flag as critical gap that Phase 2 must address\n- Multiple tickets → fetch all, consolidate, flag any contradictions\n- Ticket + free text → both treated as first-class; free text supplements ticket\n\n**Gate**: User confirms the summary is complete and may add additional sources or context before proceeding.\n\n### Phase 2: Refine Requirements\n\nInvoke the `refining-android-requirements` skill and use it to perform gap analysis on the raw requirements from Phase 1.\n\nThe skill will:\n1. Consolidate all sources into a working document\n2. Evaluate requirements against a structured rubric (functional, technical, security, UX, cross-cutting)\n3. Present categorized gaps as blocking or non-blocking questions\n4. After user answers, produce a structured specification with numbered IDs\n\n**Gate**: User approves the refined specification. They may request changes or provide additional answers.\n\n### Phase 3: Plan Implementation\n\nInvoke the `planning-android-implementation` skill and use it to design the implementation approach based on the refined spec from Phase 2.\n\nThe skill will:\n1. Classify the change type\n2. Explore the codebase for reference implementations and integration points\n3. Design the architecture with component relationships\n4. Produce a file inventory and phased implementation plan\n5. Assess risks and define verification criteria\n\n**Gate**: User reviews the implementation plan and may request changes to architecture, phasing, or scope.\n\n### Phase 4: Finalize & Save\n\nMerge the outputs from Phase 2 (specification) and Phase 3 (implementation plan) into a single design document using this template:\n\n```markdown\n# [Feature Name] — Design Document\n\n**Feature**: [concise description]\n**Date**: [current date]\n**Status**: Ready for Implementation\n**Jira**: [ticket ID if available]\n**Sources**: [list of all input sources with links]\n\n---\n\n## Requirements Specification\n\n[Full output from Phase 2 — the refined specification with numbered IDs]\n\n---\n\n## Implementation Plan\n\n[Full output from Phase 3 — architecture, file inventory, phases, risks]\n\n---\n\n## Executing This Plan\n\nTo implement this plan, run:\n\n    /work-on-android [ticket or feature reference]\n\nReference this design document during implementation for architecture decisions,\nfile locations, and phase ordering.\n```\n\n**Save the document:**\n- With ticket: `.claude/outputs/plans/PM-XXXXX-FEATURE-NAME-PLAN.md`\n- Without ticket: `.claude/outputs/plans/FEATURE-NAME-PLAN.md`\n- Feature name should be uppercase with hyphens (e.g., `BIOMETRIC-TIMEOUT-CONFIG-PLAN.md`)\n- Create the output directory if it does not exist\n\n**On completion**: Present the saved file path and remind the user they can execute the plan with `/work-on-android`.\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- When fetching from Jira/Confluence, summarize what was found rather than dumping raw content.\n- Questions in Phase 2 should be specific and actionable, not generic.\n- The implementation plan in Phase 3 should reference concrete files in the codebase, not abstract descriptions.\n",".claude/commands/review-android.md":"---\ndescription: Guided Android code review workflow through context gathering, Android-specific review, and output\nargument-hint: [PR# | PR URL | \"local\"]\n---\n\n# Android Code Review Workflow\n\nYou are guiding the developer through a comprehensive Android code review for the Bitwarden Android project.\n\n**Input**: $ARGUMENTS\n\n## Prerequisites\n\n- **Jira/Confluence access**: The `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin is required to fetch linked Jira tickets. If unavailable, skip ticket context.\n- **GitHub CLI**: Required for fetching PR metadata. Verify with `gh auth status`.\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** The user may skip phases that are not applicable.\n\n### Phase 1: Ingest\n\nParse the input to determine review context:\n\n**Source Detection Rules:**\n- **PR number** (`123`, `PR #123`, `https://github.com/.../pull/123`): Extract the numeric ID. Fetch PR metadata via `gh pr view <N> --json title,body,headRefName,baseRefName,author,files`. Fetch existing review threads to avoid duplicate comments via `gh api graphql` with `reviewThreads(first: 100)`.\n- **\"local\"** or no argument: Review current branch changes via `git diff main...HEAD` and `git log main...HEAD --oneline --no-merges`.\n- **No input**: Ask the user whether to review a PR (provide number/URL) or local branch changes.\n\n**Additional context:**\n- Detect Jira ticket references in PR title/body (patterns like `PM-\\d+`, `BWA-\\d+`). Fetch via `get_issue` if the MCP plugin is available.\n- Summarize what was fetched rather than dumping raw content.\n\n**Present a structured summary:**\n1. What is being reviewed (PR title/number, branch, or local changes description)\n2. Jira ticket context if found (summary and acceptance criteria)\n3. Files changed (count and modules affected)\n4. Existing review thread count (PR reviews only — avoids duplicate comments)\n\n**Gate**: User confirms the summary is complete before proceeding.\n\n### Phase 2: Review\n\nInvoke the `reviewing-changes` skill and use it to perform the Android-specific code review. Use the PR context from Phase 1 (change type, files affected, modules, Jira requirements) to inform the skill's change type detection and checklist selection.\n\nThe skill will:\n1. Detect the change type based on files and PR context from Phase 1\n2. Load the appropriate type-specific checklist\n3. Execute the multi-pass review strategy\n4. Consult reference materials as needed\n\n**Before advancing**: Share a summary of key findings (critical issues if any, overall assessment) and confirm the user is ready to output the review.\n\n### Phase 3: Output\n\nWrite the completed review to local files:\n\n- `review-summary.md` — Overall assessment (APPROVE / REQUEST CHANGES) plus critical issues list\n- `review-inline-comments.md` — All inline findings with `<details>` tags\n\nFollow the exact output format from `.claude/skills/reviewing-changes/examples/review-outputs.md`.\n\nFor PR reviews: offer to post the review to GitHub using `gh pr review <N> --comment -b \"$(cat review-summary.md)\"` for the summary. For inline comments, use the GitHub API or the `bitwarden-code-review` plugin if installed.\n\n**Before advancing**: Confirm the files were written successfully and ask if the user wants to post to GitHub (PR reviews only).\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- Never proceed to another phase without user confirmation.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- If starting from a partially completed review (e.g., review already written), skip to the appropriate phase.\n",".claude/commands/work-on-android.md":"---\ndescription: Guided Android development workflow through all lifecycle phases\nargument-hint: <task description, plan, or Jira ticket reference>\n---\n\n# Android Development Workflow\n\nYou are guiding the developer through a complete Android development lifecycle for the Bitwarden Android project. The task to work on is:\n\n**Task**: $ARGUMENTS\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** If a phase fails (tests fail, lint errors, etc.), loop on that phase until resolved before advancing. The user may skip phases that are not applicable.\n\n### Phase 1: Implement\n\nInvoke `Skill(implementing-android-code)` to guide your implementation of the task. Understand what needs to be done, explore the relevant code, and write the implementation.\n\n**Before advancing**: Summarize what was implemented and confirm the user is ready to move to testing.\n\n### Phase 2: Test\n\nInvoke `Skill(testing-android-code)` to write tests for the changes made in Phase 1. Follow the project's test patterns and conventions.\n\n**Before advancing**: Summarize what tests were written and confirm readiness for build verification.\n\n### Phase 3: Build & Verify\n\nInvoke `Skill(build-test-verify)` to run tests, lint, and detekt. Ensure everything passes.\n\n**If failures occur**: Fix the issues and re-run verification. Do not advance until all checks pass.\n\n**Before advancing**: Report build/test/lint results and confirm readiness for self-review.\n\n### Phase 4: Self-Review\n\nInvoke `Skill(bitwarden-delivery-tools:perform-preflight)` to perform a quality gate check on all changes. Address any issues found.\n\n**Before advancing**: Share the self-review results and confirm readiness to commit.\n\n### Phase 5: Commit\n\nInvoke `Skill(bitwarden-delivery-tools:committing-changes)` to stage and commit the changes with a properly formatted commit message.\n\n**Before advancing**: Confirm the commit was successful and ask if the user wants to proceed to review and PR creation, or stop here.\n\n### Phase 6: Review\n\n**Pre-requisites:**\n- `bitwarden-code-review` from the [Bitwarden Plugin Marketplace](https://github.com/bitwarden/ai-plugins) must be installed in order to perform this phase. If it is not installed prompt the user to install it, or skip the review phase.\n\nLaunch a subagent with the `/bitwarden-code-review:code-review-local` command to perform a **local** code review of the committed diff. Validate and address any issues found before proceeding.\n\n**Before advancing**: Share review findings and confirm readiness for PR creation.\n\n### Phase 7: Pull Request\n\nPrompt the user to invoke `Skill(bitwarden-delivery-tools:creating-pull-request)` to create the pull request with proper description and formatting. **Create as a draft PR by default** unless the user has explicitly requested a ready-for-review PR.\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- Never proceed to another phase without user confirmation.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- If starting from a partially completed task (e.g., code already written), skip to the appropriate phase.\n",".claude/skills/build-test-verify/SKILL.md":"---\nname: build-test-verify\nversion: 0.1.0\ndescription: Build, test, lint, and deploy commands for the Bitwarden Android project. Use when running tests, building APKs/AABs, running lint/detekt, deploying, using fastlane, or discovering codebase structure. Triggered by \"run tests\", \"build\", \"gradle\", \"lint\", \"detekt\", \"deploy\", \"fastlane\", \"assemble\", \"verify\", \"coverage\".\n---\n\n# Build, Test & Verify\n\n## Environment Setup\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `GITHUB_TOKEN` | Yes (CI) | GitHub Packages auth for SDK (`read:packages` scope) |\n| Build flavors | - | `standard` (Play Store), `fdroid` (no Google services) |\n| Build types | - | `debug`, `beta`, `release` |\n\nIf builds fail resolving the Bitwarden SDK, verify `GITHUB_TOKEN` in `user.properties` or environment and check connectivity to `maven.pkg.github.com`.\n\n---\n\n## Building\n\n```bash\n# Debug builds\n./gradlew app:assembleDebug\n./gradlew authenticator:assembleDebug\n\n# Release builds (requires signing keys)\n./gradlew app:assembleStandardRelease\n./gradlew app:bundleStandardRelease\n\n# F-Droid builds\n./gradlew app:assembleFdroidRelease\n```\n\n---\n\n## Running Tests\n\n**IMPORTANT**: The app module uses the `standard` flavor. Always use `testStandardDebugUnitTest`, NOT `testDebugUnitTest`.\n\n**IMPORTANT**: Always pipe test output through a filter that captures failures on the first run. Gradle suppresses detailed failure output by default, so use `2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30` to see pass/fail results and assertion details without needing a second run.\n\n```bash\n# App module tests (correct flavor!)\n./gradlew app:testStandardDebugUnitTest 2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30\n\n# Run specific test classes\n./gradlew app:testStandardDebugUnitTest --tests \"com.x8bit.bitwarden.SomeTest\" 2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30\n\n# Run all unit tests across all modules\n./gradlew test\n\n# Individual shared modules (no flavor needed)\n./gradlew :core:test\n./gradlew :data:test\n./gradlew :network:test\n./gradlew :ui:test\n\n# Authenticator module\n./gradlew authenticator:testStandardDebugUnitTest\n```\n\n### Reading Test Reports\n\nIf you need full failure details beyond what grep captures, check the HTML test report:\n\n```bash\n# After a test run, open the report at:\n# app/build/reports/tests/testStandardDebugUnitTest/index.html\n# Or read individual failure XML:\nfind app/build/test-results -name \"*.xml\" -exec grep -l \"failure\" {} \\;\n```\n\n### Test Structure\n\n```\napp/src/test/                    # App unit tests\napp/src/testFixtures/            # App test utilities\ncore/src/testFixtures/           # Core test utilities (FakeDispatcherManager)\ndata/src/testFixtures/           # Data test utilities (FakeSharedPreferences)\nnetwork/src/testFixtures/        # Network test utilities (BaseServiceTest)\nui/src/testFixtures/             # UI test utilities (BaseViewModelTest, BaseComposeTest)\n```\n\n### Test Quick Reference\n\n- **Dispatcher Control**: `FakeDispatcherManager` from `:core:testFixtures`\n- **MockK**: `mockk<T> { every { } returns }`, `coEvery { }` for suspend\n- **Flow Testing**: Turbine with `stateEventFlow()` helper from `BaseViewModelTest`\n- **Time Control**: Inject `Clock` for deterministic time testing\n\n---\n\n## Lint & Static Analysis\n\n**IMPORTANT**: Prefer running detekt on modified files only — a full project scan is slow and unnecessary during development. The project supports a `-Pprecommit=true` flag that limits detekt to staged files.\n\n**IMPORTANT**: Always pipe detekt output through a filter to capture errors on the first run. Detekt prints violation details to stderr/stdout but Gradle can obscure them. Use the grep pattern below to see violations immediately.\n\n```bash\n# Detekt on staged files only (preferred during development)\ngit add -u && ./gradlew -Pprecommit=true detekt 2>&1 | grep -E \"FAILED|BUILD|Line |Rule |Signature|detekt\" | head -40\n\n# Detekt on all files (full scan, use sparingly)\n./gradlew detekt 2>&1 | grep -E \"FAILED|BUILD|Line |Rule |Signature|detekt\" | head -40\n\n# Android Lint\n./gradlew lint\n\n# Full validation suite (detekt + lint + tests + coverage)\n./fastlane check\n```\n\n### How `-Pprecommit=true` Works\n\nThe root `build.gradle.kts` configures detekt tasks to use `git diff --name-only --cached` when this property is set, limiting analysis to staged files only. This is the same mechanism used by the project's pre-commit hook. Stage your changes with `git add` before running.\n\n---\n\n## Codebase Discovery\n\n```bash\n# Find existing Bitwarden UI components\nfind ui/src/main/kotlin/com/bitwarden/ui/platform/components/ -name \"Bitwarden*.kt\" | sort\n\n# Find all ViewModels\ngrep -rl \"BaseViewModel<\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Find all Navigation files with @Serializable routes\nfind app/src/main/kotlin/ -name \"*Navigation.kt\" | sort\n\n# Find all Hilt modules\nfind app/src/main/kotlin/ -name \"*Module.kt\" -path \"*/di/*\" | sort\n\n# Find all repository interfaces\nfind app/src/main/kotlin/ -name \"*Repository.kt\" -not -name \"*Impl.kt\" -path \"*/repository/*\" | sort\n\n# Find encrypted disk source examples\ngrep -rl \"EncryptedPreferences\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Find Clock injection usage\ngrep -rl \"private val clock: Clock\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Search existing strings before adding new ones\ngrep -n \"search_term\" ui/src/main/res/values/strings.xml\n```\n\n---\n\n## Deployment & Versioning\n\n**Version location**: `gradle/libs.versions.toml`\n```toml\nappVersionCode = \"1\"\nappVersionName = \"2025.11.1\"\n```\nPattern: `YEAR.MONTH.PATCH`\n\n**Publishing channels**:\n- **Play Store**: GitHub Actions workflow with signed AAB\n- **F-Droid**: Dedicated workflow with F-Droid signing keys\n- **Firebase App Distribution**: Beta testing\n",".claude/skills/evaluating-sdk-internal-updates/SKILL.md":"---\nname: evaluating-sdk-internal-updates\ndescription: Evaluates a bitwarden/android \"Update SDK to\" PR against the sdk-internal commit range for compile-time and runtime breaking changes, maps affected symbols to Android call sites, and applies clear in-scope fixes. Use when reviewing an SDK bump PR, a bitwardenSdk version change, or triaging sdk-internal breaking changes.\nallowed-tools:\n  - Bash(gh pr diff:*)\n  - Bash(git -C * log *)\n  - Bash(git -C * show *)\n  - Bash(git add:*)\n  - Bash(git commit:*)\n  - Bash(grep:*)\n  - Bash(./gradlew *)\n  - Read\n  - Grep\n  - Glob\n  - Edit\n  - Write\n  - Skill(implementing-android-code)\n  - Skill(bitwarden-delivery-tools:committing-changes)\n---\n\n# Evaluating sdk-internal Updates\n\n**Identify both compile-time and runtime breaks before fixing anything — fixing the first break found is not finishing.** Steps 3-7 always cover the entire commit range before step 8 starts, no matter how obvious or urgent an early compile-time break looks.\n\n## Identify\n\nBinding surface facts specific to this SDK: `#[uniffi::export]` / `derive(uniffi::...)` annotations are scattered across many crates, not just `crates/bitwarden-uniffi`; `crates/bitwarden-ffi` is unrelated (\"do not use\"). No `.udl` files. UniFFI emits one Kotlin package per crate (`com.bitwarden.core`, `com.bitwarden.vault`, `com.bitwarden.crypto`, etc.) — only the top-level `Client`/`AuthClient`/`GeneratorClients` actually live under `com.bitwarden.sdk`. Both `app` and `authenticator` depend on the SDK; neither is optional to search.\n\nA hunk that only touches a macro invocation (e.g. `state_bridge! { ... }`) doesn't show the binding surface — the expansion lives in the macro's definition, often in a different crate (e.g. `bitwarden-state-bridge-macro`). `#[uniffi::export(with_foreign)]` marks a callback interface: a trait Kotlin must implement, where adding a field/method is never additive-safe for the implementor.\n\n1. Locate the local `bitwarden/sdk-internal` clone (check sibling directories to this repo). If none exists, stop and tell the user it's a required prerequisite for this skill — do not clone it yourself.\n2. `gh pr diff <PR> -R bitwarden/android | grep bitwardenSdk` → old/new `bitwardenSdk` string. Everything after the second `-` is the git ref — a commit SHA or a branch name (`.dev` SDK builds use both); resolve a branch name as `origin/<branch>` in the clone.\n3. Attempt `./gradlew app:compileStandardDebugKotlin authenticator:compileDebugKotlin` at the current checkout before crawling sdk-internal. A failure confirms a compile-time break directly, with a more precise location than any git search — note it and continue to steps 4-7 for the full commit range; do not fix it yet. A clean build only rules out compile-time breaks, not runtime ones.\n4. `git -C <sdk-internal-path> log --oneline OLD..NEW -G'uniffi::export|derive\\(uniffi|#\\[uniffi' -- '*.rs'` → candidate binding-surface commits.\n5. Classify per hunk, not per commit — a commit with one additive headline change can still have a second, unrelated breaking hunk. If a hunk only touches a macro invocation, read the macro's definition before classifying. `git -C <sdk-internal-path> show <sha> -- '*.rs'`.\n6. For every distinct symbol/type touched (every hunk, not just the commit's headline change), grep the whole repo for the bare symbol name to find Android call sites — a fixed module list or a `com.bitwarden.sdk.<Symbol>` import-prefix check both miss real consumers.\n7. Report: compile-time breaks, runtime breaks, safe/no-call-site — each with commit, symbol, and call sites. Cite sdk-internal commits and PRs as `bitwarden/sdk-internal#<NNN>` or a full commit URL/SHA — never a bare `#<NNN>` copied from a commit subject line. A bare reference auto-links within whatever repo the report is posted to (e.g. `bitwarden/android`), tagging an unrelated issue or PR there.\n\n## Resolve\n\nResolve the findings from Step 7 by deciding on a fix (step 8), implementing and committing it directly (step 9), then verifying (step 10).\n\n8. Decide the fix for anything found, compile-time or runtime, whenever the correct behavior is clear and within scope. For a new required method, grep for the underlying concept, not the new method/type name (it won't exist yet) — no existing consumer means stub it: a `// no-op` comment or a null/default return that satisfies the compiler, not a behavioral decision. Never `TODO()` — it throws at runtime, which is a crash, not a stub. A sibling's structure (naming, placement, style) is a template; its behavior (storage, defaulting, error handling, side effects) is not evidence for yours. If unsure, report it instead of guessing, along with anything needing a product decision.\n9. Implement every fix from step 8 directly: invoke `Skill(implementing-android-code)` first if the fix isn't purely mechanical, then commit with `Skill(bitwarden-delivery-tools:committing-changes)`.\n10. Verify with the same compile task used in step 3.\n",".claude/skills/implementing-android-code/SKILL.md":"---\nname: implementing-android-code\nversion: 0.1.4\ndescription: This skill should be used when implementing Android code in Bitwarden. Covers critical patterns, gotchas, and anti-patterns unique to this codebase. Triggered by \"How do I implement a ViewModel?\", \"Create a new screen\", \"Add navigation\", \"Write a repository\", \"BaseViewModel pattern\", \"State-Action-Event\", \"type-safe navigation\", \"@Serializable route\", \"SavedStateHandle persistence\", \"process death recovery\", \"handleAction\", \"sendAction\", \"Hilt module\", \"Repository pattern\", \"implementing a screen\", \"adding a data source\", \"handling navigation\", \"encrypted storage\", \"security patterns\", \"Clock injection\", \"DataState\", or any questions about implementing features, screens, ViewModels, data sources, or navigation in the Bitwarden Android app.\n---\n\n# Implementing Android Code - Bitwarden Quick Reference\n\n**This skill provides tactical guidance for Bitwarden-specific patterns.** For comprehensive architecture decisions and complete code style rules, consult `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n---\n\n## Critical Patterns Reference\n\n### A. ViewModel Implementation (State-Action-Event Pattern)\n\nAll ViewModels follow the **State-Action-Event (SAE)** pattern via `BaseViewModel<State, Event, Action>`.\n\n**Key Requirements:**\n- Annotate with `@HiltViewModel`\n- State class MUST be `@Parcelize data class : Parcelable`\n- Implement `handleAction(action: A)` - MUST be synchronous\n- Post internal actions from coroutines using `sendAction()`\n- Save/restore state via `SavedStateHandle[KEY_STATE]`\n- Private action handlers: `private fun handle*` naming convention\n\n**Template**: See [ViewModel template](templates.md#viewmodel-template-state-action-event-pattern)\n\n**Pattern Summary:**\n```kotlin\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val repository: ExampleRepository,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    initialState = savedStateHandle[KEY_STATE] ?: ExampleState(),\n) {\n    init {\n        stateFlow.onEach { savedStateHandle[KEY_STATE] = it }.launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        // Synchronous dispatch only\n        when (action) {\n            is Action.Click -> handleClick()\n            is Action.Internal.DataReceived -> handleDataReceived(action)\n        }\n    }\n\n    private fun handleClick() {\n        viewModelScope.launch {\n            val result = repository.fetchData()\n            sendAction(Action.Internal.DataReceived(result))  // Post internal action\n        }\n    }\n\n    private fun handleDataReceived(action: Action.Internal.DataReceived) {\n        mutableStateFlow.update { it.copy(data = action.result) }\n    }\n}\n```\n\n**Reference:**\n- `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)\n- `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt` (see class declaration)\n\n**Critical Gotchas:**\n- ❌ **NEVER** update `mutableStateFlow` directly inside coroutines\n- ✅ **ALWAYS** post internal actions from coroutines, update state in `handleAction()`\n- ❌ **NEVER** forget `@IgnoredOnParcel` for sensitive data (causes security leak)\n- ✅ **ALWAYS** use `@Parcelize` on state classes for process death recovery\n- ✅ State restoration happens automatically if properly saved to `SavedStateHandle`\n\n---\n\n### B. Navigation Implementation (Type-Safe)\n\nAll navigation uses **type-safe routes** with kotlinx.serialization.\n\n**Pattern Structure:**\n1. `@Serializable` route data class with parameters\n2. `...Args` helper class for extracting from `SavedStateHandle`\n3. `NavGraphBuilder.{screen}Destination()` extension for adding screen to graph\n4. `NavController.navigateTo{Screen}()` extension for navigation calls\n\n**Template**: See [Navigation template](templates.md#navigation-template-type-safe-routes)\n\n**Pattern Summary:**\n```kotlin\n@Serializable\ndata class ExampleRoute(val userId: String, val isEditMode: Boolean = false)\n\ndata class ExampleArgs(val userId: String, val isEditMode: Boolean)\n\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(userId = route.userId, isEditMode = route.isEditMode)\n}\n\nfun NavController.navigateToExample(\n    userId: String,\n    isEditMode: Boolean = false,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(route = ExampleRoute(userId, isEditMode), navOptions = navOptions)\n}\n\nfun NavGraphBuilder.exampleDestination(onNavigateBack: () -> Unit) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(onNavigateBack = onNavigateBack)\n    }\n}\n```\n\n**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt` (see `LoginRoute` and extensions)\n\n**Key Benefits:**\n- ✅ Type safety: Compile-time errors for missing parameters\n- ✅ No string literals in navigation code\n- ✅ Automatic serialization/deserialization\n- ✅ Clear contract for screen dependencies\n\n---\n\n### C. Screen/Compose Implementation\n\nAll screens follow consistent Compose patterns.\n\n**Template**: See [Screen/Compose template](templates.md#screencompose-template)\n\n**Key Patterns:**\n```kotlin\n@Composable\nfun ExampleScreen(\n    onNavigateBack: () -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            ExampleEvent.NavigateBack -> onNavigateBack()\n        }\n    }\n\n    BitwardenScaffold(\n        topBar = {\n            BitwardenTopAppBar(\n                title = stringResource(R.string.title),\n                navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_back),\n                onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },\n            )\n        },\n    ) {\n        // UI content\n    }\n}\n```\n\n**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt` (see `LoginScreen` composable)\n\n**Essential Requirements:**\n- ✅ Use `hiltViewModel()` for dependency injection\n- ✅ Use `collectAsStateWithLifecycle()` for state (not `collectAsState()`)\n- ✅ Use `EventsEffect(viewModel)` for one-shot events\n- ✅ Use `Bitwarden*` prefixed components from `:ui` module\n\n**State Hoisting Rules:**\n- **ViewModel state**: Data that needs to survive process death or affects business logic\n- **UI-only state**: Temporary UI state (scroll position, text field focus) using `remember` or `rememberSaveable`\n\n---\n\n### D. Data Layer Implementation\n\nThe data layer follows strict patterns for repositories, managers, and data sources.\n\n**Interface + Implementation Separation (ALWAYS)**\n\n**Template**: See [Data Layer template](templates.md#data-layer-template-repository--hilt-module)\n\n**Pattern Summary:**\n```kotlin\n// Interface (injected via Hilt)\ninterface ExampleRepository {\n    suspend fun fetchData(id: String): ExampleResult\n    val dataFlow: StateFlow<DataState<ExampleData>>\n}\n\n// Implementation (NOT directly injected)\nclass ExampleRepositoryImpl(\n    private val exampleDiskSource: ExampleDiskSource,\n    private val exampleService: ExampleService,\n) : ExampleRepository {\n    override suspend fun fetchData(id: String): ExampleResult {\n        // NO exceptions thrown - return Result or sealed class\n        return exampleService.getData(id).fold(\n            onSuccess = { ExampleResult.Success(it.toModel()) },\n            onFailure = { ExampleResult.Error(it.message) },\n        )\n    }\n}\n\n// Sealed result class (domain-specific)\nsealed class ExampleResult {\n    data class Success(val data: ExampleData) : ExampleResult()\n    data class Error(val message: String?) : ExampleResult()\n}\n\n// Hilt Module\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleRepositoryModule {\n    @Provides\n    @Singleton\n    fun provideExampleRepository(\n        exampleDiskSource: ExampleDiskSource,\n        exampleService: ExampleService,\n    ): ExampleRepository = ExampleRepositoryImpl(exampleDiskSource, exampleService)\n}\n```\n\n**Reference:**\n- `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt`\n- `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`\n\n**Three-Layer Data Architecture:**\n1. **Data Sources** - Raw data access (network, disk, SDK). Return `Result<T>`, never throw.\n2. **Managers** - Single responsibility business logic. Wrap OS/external services.\n3. **Repositories** - Aggregate sources/managers. Return domain-specific sealed classes.\n\n**Critical Rules:**\n- ❌ **NEVER** throw exceptions in data layer\n- ✅ **ALWAYS** use interface + `...Impl` pattern\n- ✅ **ALWAYS** inject interfaces, never implementations\n- ✅ Data sources return `Result<T>`, repositories return domain sealed classes\n- ✅ Use `StateFlow` for continuously observed data\n\n**KDoc on Interfaces vs. Implementations**\n\nKDoc on an interface member describes the **contract** — what the caller can rely on. It must not describe **how** the implementation fulfills that contract. Implementation details (which data source is consulted, what is cached, what is logged, which manager is delegated to, ordering of internal calls, etc.) belong on the `...Impl` member — and only when the *why* is non-obvious from the code itself.\n\nThis applies to every interface in the codebase: repositories, managers, data sources, validators, and UI-layer interfaces alike. The rule is the same one CLAUDE.md states for comments in general — describe the contract or the non-obvious *why*, never the *what* a well-named identifier already conveys.\n\n❌ **Wrong** — interface KDoc leaks the implementation:\n```kotlin\ninterface ExampleRepository {\n    /**\n     * Fetches data by reading from [ExampleDiskSource] first, falling back to\n     * [ExampleService] on cache miss and writing the network result back to disk.\n     */\n    suspend fun fetchData(id: String): ExampleResult\n}\n```\n\n✅ **Right** — interface KDoc describes the contract; implementation details (if needed at all) live on the override:\n```kotlin\ninterface ExampleRepository {\n    /** Returns the [ExampleData] for [id], or an error result on failure. */\n    suspend fun fetchData(id: String): ExampleResult\n}\n\nclass ExampleRepositoryImpl(...) : ExampleRepository {\n    // No KDoc needed — the disk-then-network pattern is visible in the code.\n    override suspend fun fetchData(id: String): ExampleResult { ... }\n}\n```\n\nRed flags that an interface KDoc has drifted into implementation territory:\n- Names a concrete collaborator (`...DiskSource`, `...Service`, `...Manager`, `...Impl`)\n- Describes ordering of internal calls (\"first ... then ...\", \"falls back to ...\")\n- Mentions caching, retries, logging, or threading behavior that callers don't depend on\n- Restates the method signature in prose (\"Suspends until X returns Y\")\n\nIf callers genuinely depend on a behavior (e.g., \"this method is safe to call before vault unlock\", \"result is cached for the session\"), that *is* part of the contract and belongs on the interface. The test: would a different valid implementation be free to change this? If yes, it's implementation detail — strip it.\n\n---\n\n### E. UI Components\n\n**Use Existing Components First:**\n\nThe `:ui` module provides reusable `Bitwarden*` prefixed components. Search before creating new ones.\n\n**Common Components:**\n- `BitwardenFilledButton` - Primary action buttons\n- `BitwardenOutlinedButton` - Secondary action buttons\n- `BitwardenTextField` - Text input fields\n- `BitwardenPasswordField` - Password input with show/hide\n- `BitwardenSwitch` - Toggle switches\n- `BitwardenTopAppBar` - Toolbar/app bar\n- `BitwardenScaffold` - Screen container with scaffold\n- `BitwardenBasicDialog` - Simple dialogs\n- `BitwardenLoadingDialog` - Loading indicators\n\n**Component Discovery:**\nSearch `ui/src/main/kotlin/com/bitwarden/ui/platform/components/` for existing `Bitwarden*` components. For build, test, and codebase discovery commands, use the **`build-test-verify`** skill.\n\n**When to Create New Reusable Components:**\n- Component used in 3+ places\n- Component needs consistent theming across app\n- Component has semantic meaning (accessibility)\n- Component has complex state management\n\n**New Component Requirements:**\n- Prefix with `Bitwarden`\n- Accept themed colors/styles from `BitwardenTheme`\n- Include preview composables for testing\n- Support accessibility (content descriptions, semantics)\n\n**String Resources:**\n\nNew strings belong in the `:ui` module: `ui/src/main/res/values/strings.xml`\n\n- Use typographic apostrophes and quotes to avoid escape characters: `you’ll` not `you\\'ll`, `“word”` not `\\\"word\\\"`\n- Reference strings via generated `BitwardenString` resource IDs\n- Do not add strings to other modules unless explicitly instructed\n\n---\n\n### F. Security Patterns\n\n**Encrypted vs Unencrypted Storage:**\n\n**Template**: See [Security templates](templates.md#security-templates)\n\n**Pattern Summary:**\n```kotlin\nclass ExampleDiskSourceImpl(\n    @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,\n    @UnencryptedPreferences sharedPreferences: SharedPreferences,\n) : BaseEncryptedDiskSource(\n    encryptedSharedPreferences = encryptedSharedPreferences,\n    sharedPreferences = sharedPreferences,\n),\n    ExampleDiskSource {\n    fun storeAuthToken(token: String) {\n        putEncryptedString(KEY_TOKEN, token)  // Sensitive — uses base class method\n    }\n\n    fun storeThemePreference(isDark: Boolean) {\n        putBoolean(KEY_THEME, isDark)  // Non-sensitive — uses base class method\n    }\n}\n```\n\n**Android Keystore (Biometric Keys):**\n- User-scoped encryption keys: `BiometricsEncryptionManager`\n- Keys stored in Android Keystore (hardware-backed when available)\n- Integrity validation on biometric state changes\n\n**Input Validation:**\n```kotlin\n// Validation returns boolean, NEVER throws\ninterface RequestValidator {\n    fun validate(request: Request): Boolean\n}\n\n// Sanitization removes dangerous content\nfun String?.sanitizeTotpUri(issuer: String?, username: String?): String? {\n    if (this.isNullOrBlank()) return null\n    // Sanitize and return safe value\n}\n```\n\n**Security Checklist:**\n- ✅ Use `@EncryptedPreferences` for credentials, keys, tokens\n- ✅ Use `@UnencryptedPreferences` for UI state, preferences\n- ✅ Use `@IgnoredOnParcel` for sensitive ViewModel state\n- ❌ **NEVER** log sensitive data (passwords, tokens, vault items)\n- ✅ Validate all user input before processing\n- ✅ Use Timber for non-sensitive logging only\n\n---\n\n### G. Testing Patterns\n\n**ViewModel Testing:**\n\n**Template**: See [Testing templates](templates.md#testing-templates)\n\n**Pattern Summary:**\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n    private val mockRepository: ExampleRepository = mockk()\n\n    @Test\n    fun `ButtonClick should fetch data and update state`() = runTest {\n        val expectedResult = ExampleResult.Success(data = \"test\")\n        coEvery { mockRepository.fetchData(any()) } returns expectedResult\n\n        val viewModel = createViewModel()\n        viewModel.trySendAction(ExampleAction.ButtonClick)\n\n        viewModel.stateFlow.test {\n            assertEquals(EXPECTED_STATE.copy(data = \"test\"), awaitItem())\n        }\n    }\n\n    private fun createViewModel(): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to EXPECTED_STATE)),\n        repository = mockRepository,\n    )\n}\n```\n\n**Reference:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n**Key Testing Patterns:**\n- ✅ Extend `BaseViewModelTest` for proper dispatcher management\n- ✅ Use `runTest` from `kotlinx.coroutines.test`\n- ✅ Use Turbine's `.test { awaitItem() }` for Flow assertions\n- ✅ Use MockK: `coEvery` for suspend functions, `every` for sync\n- ✅ Test both state changes and event emissions\n- ✅ Test both success and failure Result paths\n\n**Flow Testing with Turbine:**\n```kotlin\n// Test state and events simultaneously\nviewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n    viewModel.trySendAction(ExampleAction.Submit)\n    assertEquals(ExpectedState.Loading, stateFlow.awaitItem())\n    assertEquals(ExampleEvent.ShowSuccess, eventFlow.awaitItem())\n}\n```\n\n**MockK Quick Reference:**\n```kotlin\ncoEvery { repository.fetchData(any()) } returns Result.success(\"data\")  // Suspend\nevery { diskSource.getData() } returns \"cached\"  // Sync\ncoVerify { repository.fetchData(\"123\") }  // Verify\n```\n\n---\n\n### H. Clock/Time Handling\n\nAll code needing current time must inject `Clock` for testability.\n\n**Key Requirements:**\n- ✅ Inject `Clock` via Hilt in ViewModels\n- ✅ Pass `Clock` as parameter in extension functions\n- ✅ Use `clock.instant()` to get current time\n- ❌ Never call `Instant.now()` or `DateTime.now()` directly\n- ❌ Never use `mockkStatic` for datetime classes in tests\n\n**Pattern Summary:**\n```kotlin\n// ViewModel with Clock\nclass MyViewModel @Inject constructor(\n    private val clock: Clock,\n) {\n    val timestamp = clock.instant()\n}\n\n// Extension function with Clock parameter\nfun State.getTimestamp(clock: Clock): Instant =\n    existingTime ?: clock.instant()\n\n// Test with fixed clock\nval FIXED_CLOCK = Clock.fixed(\n    Instant.parse(\"2023-10-27T12:00:00Z\"),\n    ZoneOffset.UTC,\n)\n```\n\n**Reference:**\n- `docs/STYLE_AND_BEST_PRACTICES.md` (see Time and Clock Handling section)\n- `core/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt` (see `provideClock` function)\n\n**Critical Gotchas:**\n- ❌ `Instant.now()` creates hidden dependency, non-testable\n- ❌ `mockkStatic(Instant::class)` is fragile, can leak between tests\n- ✅ `Clock.fixed(...)` provides deterministic test behavior\n\n---\n\n### I. Kotlin Style Rules\n\nProject-specific style conventions enforced in code review. These supplement (not replace) `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n**`when` branches with wrapped right-hand side require curly braces.**\n\nWhen a `when` branch's expression is too long to fit on the same line as the arrow and is wrapped to the next line, wrap the body in `{ }`. A bare `->` followed by an indented expression on its own line is rejected in review.\n\n❌ **Wrong** — wrapped body without braces:\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT ->\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    VaultItemCipherType.DRIVERS_LICENSE ->\n        VaultAddEditState.ViewState.Content.ItemType.DriversLicense()\n}\n```\n\n✅ **Right** — wrapped body with braces:\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT -> {\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    }\n    VaultItemCipherType.DRIVERS_LICENSE -> {\n        VaultAddEditState.ViewState.Content.ItemType.DriversLicense()\n    }\n}\n```\n\nSingle-line branches (body fits on the same line as `->`) do **not** need braces.\n\n---\n\n## Bitwarden-Specific Anti-Patterns\n\n**General anti-patterns are documented in CLAUDE.md.** This section covers violations specific to Bitwarden's State-Action-Event, navigation, and data layer patterns:\n\n❌ **NEVER update ViewModel state directly in coroutines**\n- Post internal actions, update state synchronously in `handleAction()`\n\n❌ **NEVER inject `...Impl` classes**\n- Only inject interfaces via Hilt\n\n❌ **NEVER create navigation without `@Serializable` routes**\n- No string-based navigation, always type-safe\n\n❌ **NEVER use raw `Result<T>` in repositories**\n- Use domain-specific sealed classes for better error handling\n\n❌ **NEVER make state classes without `@Parcelize`**\n- All ViewModel state must survive process death\n\n❌ **NEVER skip `SavedStateHandle` persistence for ViewModels**\n- Users lose form progress on process death\n\n❌ **NEVER forget `@IgnoredOnParcel` for passwords/tokens**\n- Causes security vulnerability (sensitive data in parcel)\n\n❌ **NEVER use generic `Exception` catching**\n- Catch specific exceptions only (`RemoteException`, `IOException`)\n\n❌ **NEVER call `Instant.now()` or `DateTime.now()` directly**\n- Inject `Clock` via Hilt, use `clock.instant()` for testability\n\n❌ **NEVER put implementation details in interface KDoc**\n- Interface KDoc describes the contract; concrete collaborators, call ordering, caching, and fallback behavior belong on the `...Impl` override (and only when the *why* is non-obvious). See section D for examples.\n\n---\n\n## Quick Reference\n\nFor build, test, and codebase discovery commands, use the **`build-test-verify`** skill.\n\n**File Reference Format:**\nWhen pointing to specific code, use: `file_path:line_number`\n\nExample: `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)\n",".claude/skills/implementing-android-code/templates.md":"# Code Templates - Bitwarden Android\n\nCopy-pasteable templates derived from actual codebase patterns. Replace `Example` with your feature name.\n\n---\n\n## ViewModel Template (State-Action-Event Pattern)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt`\n\n### State Class\n\n```kotlin\n@Parcelize\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    @IgnoredOnParcel val sensitiveInput: String = \"\", // Sensitive data excluded from parcel\n    val dialogState: DialogState? = null,\n) : Parcelable {\n\n    /**\n     * Dialog states for the Example screen.\n     */\n    sealed class DialogState : Parcelable {\n        @Parcelize\n        data class Error(\n            val title: Text? = null,\n            val message: Text,\n            val error: Throwable? = null,\n        ) : DialogState()\n\n        @Parcelize\n        data class Loading(val message: Text) : DialogState()\n    }\n}\n```\n\n### Event Sealed Class\n\n```kotlin\n/**\n * One-shot UI events for the Example screen.\n */\nsealed class ExampleEvent {\n    data object NavigateBack : ExampleEvent()\n\n    data class ShowToast(val message: Text) : ExampleEvent()\n}\n```\n\n### Action Sealed Class (with Internal)\n\n```kotlin\n/**\n * User and system actions for the Example screen.\n */\nsealed class ExampleAction {\n    data object BackClick : ExampleAction()\n\n    data object SubmitClick : ExampleAction()\n\n    data class InputChanged(val input: String) : ExampleAction()\n\n    data object ErrorDialogDismiss : ExampleAction()\n\n    /**\n     * Internal actions dispatched by the ViewModel from coroutines.\n     */\n    sealed class Internal : ExampleAction() {\n        data class ReceiveDataState(\n            val dataState: DataState<ExampleData>,\n        ) : Internal()\n\n        data class ReceiveDataResult(\n            val result: ExampleResult,\n        ) : Internal()\n    }\n}\n```\n\n### ViewModel\n\n```kotlin\nprivate const val KEY_STATE = \"state\"\n\n/**\n * ViewModel for the Example screen.\n */\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val exampleRepository: ExampleRepository,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    initialState = savedStateHandle[KEY_STATE]\n        ?: run {\n            val args = savedStateHandle.toExampleArgs()\n            ExampleState(\n                data = args.itemId,\n            )\n        },\n) {\n\n    init {\n        // Persist state for process death recovery\n        stateFlow\n            .onEach { savedStateHandle[KEY_STATE] = it }\n            .launchIn(viewModelScope)\n\n        // Collect repository flows as internal actions\n        exampleRepository.dataFlow\n            .map { ExampleAction.Internal.ReceiveDataState(it) }\n            .onEach(::sendAction)\n            .launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        when (action) {\n            ExampleAction.BackClick -> handleBackClick()\n            ExampleAction.SubmitClick -> handleSubmitClick()\n            ExampleAction.ErrorDialogDismiss -> handleErrorDialogDismiss()\n            is ExampleAction.InputChanged -> handleInputChanged(action)\n            is ExampleAction.Internal.ReceiveDataState -> {\n                handleReceiveDataState(action)\n            }\n            is ExampleAction.Internal.ReceiveDataResult -> {\n                handleReceiveDataResult(action)\n            }\n        }\n    }\n\n    private fun handleBackClick() {\n        sendEvent(ExampleEvent.NavigateBack)\n    }\n\n    private fun handleErrorDialogDismiss() {\n        mutableStateFlow.update { it.copy(dialogState = null) }\n    }\n\n    private fun handleSubmitClick() {\n        viewModelScope.launch {\n            val result = exampleRepository.submitData(state.data.orEmpty())\n            sendAction(ExampleAction.Internal.ReceiveDataResult(result))\n        }\n    }\n\n    private fun handleInputChanged(action: ExampleAction.InputChanged) {\n        mutableStateFlow.update { it.copy(sensitiveInput = action.input) }\n    }\n\n    private fun handleReceiveDataState(\n        action: ExampleAction.Internal.ReceiveDataState,\n    ) {\n        when (action.dataState) {\n            is DataState.Loaded -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        data = action.dataState.data.toString(),\n                    )\n                }\n            }\n\n            is DataState.Loading -> {\n                mutableStateFlow.update { it.copy(isLoading = true) }\n            }\n\n            is DataState.Error -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        dialogState = ExampleState.DialogState.Error(\n                            message = BitwardenString.generic_error_message.asText(),\n                            error = action.dataState.error,\n                        ),\n                    )\n                }\n            }\n\n            else -> Unit\n        }\n    }\n\n    private fun handleReceiveDataResult(\n        action: ExampleAction.Internal.ReceiveDataResult,\n    ) {\n        when (val result = action.result) {\n            is ExampleResult.Success -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        data = result.data,\n                    )\n                }\n            }\n\n            is ExampleResult.Error -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        dialogState = ExampleState.DialogState.Error(\n                            message = result.message?.asText()\n                                ?: BitwardenString.generic_error_message.asText(),\n                        ),\n                    )\n                }\n            }\n        }\n    }\n}\n```\n\n---\n\n## Navigation Template (Type-Safe Routes)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt`\n\n```kotlin\n@file:OmitFromCoverage\n\npackage com.x8bit.bitwarden.ui.feature.example\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.navigation.NavController\nimport androidx.navigation.NavGraphBuilder\nimport androidx.navigation.NavOptions\nimport androidx.navigation.toRoute\nimport com.bitwarden.annotation.OmitFromCoverage\nimport com.bitwarden.ui.platform.base.util.composableWithSlideTransitions\nimport kotlinx.serialization.Serializable\n\n/**\n * Route for the Example screen.\n */\n@Serializable\n@OmitFromCoverage\ndata class ExampleRoute(\n    val itemId: String,\n    val isEditMode: Boolean = false,\n)\n\n/**\n * Args extracted from [SavedStateHandle] for the Example screen.\n */\n@OmitFromCoverage\ndata class ExampleArgs(\n    val itemId: String,\n    val isEditMode: Boolean,\n)\n\n/**\n * Extracts [ExampleArgs] from the [SavedStateHandle].\n */\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(\n        itemId = route.itemId,\n        isEditMode = route.isEditMode,\n    )\n}\n\n/**\n * Navigate to the Example screen.\n */\nfun NavController.navigateToExample(\n    itemId: String,\n    isEditMode: Boolean = false,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(\n        route = ExampleRoute(\n            itemId = itemId,\n            isEditMode = isEditMode,\n        ),\n        navOptions = navOptions,\n    )\n}\n\n/**\n * Add the Example screen destination to the navigation graph.\n */\nfun NavGraphBuilder.exampleDestination(\n    onNavigateBack: () -> Unit,\n) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(\n            onNavigateBack = onNavigateBack,\n        )\n    }\n}\n```\n\n---\n\n## Screen/Compose Template\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt`\n\n```kotlin\npackage com.x8bit.bitwarden.ui.feature.example\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.bitwarden.ui.platform.base.util.EventsEffect\nimport com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar\nimport com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold\nimport com.bitwarden.ui.platform.components.util.rememberVectorPainter\nimport com.bitwarden.ui.platform.resource.BitwardenDrawable\nimport com.bitwarden.ui.platform.resource.BitwardenString\n\n/**\n * The Example screen.\n */\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ExampleScreen(\n    onNavigateBack: () -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            ExampleEvent.NavigateBack -> onNavigateBack()\n            is ExampleEvent.ShowToast -> {\n                // Handle toast\n            }\n        }\n    }\n\n    // Dialogs\n    ExampleDialogs(\n        dialogState = state.dialogState,\n        onDismissRequest = { viewModel.trySendAction(ExampleAction.ErrorDialogDismiss) },\n    )\n\n    val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())\n    BitwardenScaffold(\n        modifier = Modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            BitwardenTopAppBar(\n                title = stringResource(id = BitwardenString.example),\n                scrollBehavior = scrollBehavior,\n                navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),\n                onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },\n            )\n        },\n    ) {\n        ExampleScreenContent(\n            state = state,\n            onInputChanged = { viewModel.trySendAction(ExampleAction.InputChanged(it)) },\n            onSubmitClick = { viewModel.trySendAction(ExampleAction.SubmitClick) },\n            modifier = Modifier\n                .fillMaxSize(),\n        )\n    }\n}\n```\n\n---\n\n## Data Layer Template (Repository + Hilt Module)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`\n\n### Interface\n\n```kotlin\n/**\n * Provides data operations for the Example feature.\n */\ninterface ExampleRepository {\n    /**\n     * Submits data and returns a typed result.\n     */\n    suspend fun submitData(input: String): ExampleResult\n\n    /**\n     * Continuously observed data stream.\n     */\n    val dataFlow: StateFlow<DataState<ExampleData>>\n}\n```\n\n### Sealed Result Class\n\n```kotlin\n/**\n * Domain-specific result for Example operations.\n */\nsealed class ExampleResult {\n    data class Success(val data: String) : ExampleResult()\n    data class Error(val message: String?) : ExampleResult()\n}\n```\n\n### Implementation\n\n```kotlin\n/**\n * Default implementation of [ExampleRepository].\n */\nclass ExampleRepositoryImpl(\n    private val exampleDiskSource: ExampleDiskSource,\n    private val exampleService: ExampleService,\n    private val dispatcherManager: DispatcherManager,\n) : ExampleRepository {\n\n    override val dataFlow: StateFlow<DataState<ExampleData>>\n        get() = // ...\n\n    override suspend fun submitData(input: String): ExampleResult {\n        return exampleService\n            .postData(input)\n            .fold(\n                onSuccess = { ExampleResult.Success(it.toModel()) },\n                onFailure = { ExampleResult.Error(it.message) },\n            )\n    }\n}\n```\n\n### Hilt Module\n\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleRepositoryModule {\n\n    @Provides\n    @Singleton\n    fun provideExampleRepository(\n        exampleDiskSource: ExampleDiskSource,\n        exampleService: ExampleService,\n        dispatcherManager: DispatcherManager,\n    ): ExampleRepository = ExampleRepositoryImpl(\n        exampleDiskSource = exampleDiskSource,\n        exampleService = exampleService,\n        dispatcherManager = dispatcherManager,\n    )\n}\n```\n\n---\n\n## Security Templates\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt` and `AuthDiskSourceImpl.kt`\n\n### Encrypted Disk Source (Module)\n\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleDiskModule {\n\n    @Provides\n    @Singleton\n    fun provideExampleDiskSource(\n        @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,\n        @UnencryptedPreferences sharedPreferences: SharedPreferences,\n        json: Json,\n    ): ExampleDiskSource = ExampleDiskSourceImpl(\n        encryptedSharedPreferences = encryptedSharedPreferences,\n        sharedPreferences = sharedPreferences,\n        json = json,\n    )\n}\n```\n\n### Encrypted Disk Source (Implementation)\n\n```kotlin\n/**\n * Disk source for Example data using encrypted and unencrypted storage.\n */\nclass ExampleDiskSourceImpl(\n    encryptedSharedPreferences: SharedPreferences,\n    sharedPreferences: SharedPreferences,\n    private val json: Json,\n) : BaseEncryptedDiskSource(\n    encryptedSharedPreferences = encryptedSharedPreferences,\n    sharedPreferences = sharedPreferences,\n),\n    ExampleDiskSource {\n\n    private companion object {\n        const val ENCRYPTED_TOKEN_KEY = \"exampleToken\"\n        const val UNENCRYPTED_PREF_KEY = \"examplePreference\"\n    }\n\n    override var authToken: String?\n        get() = getEncryptedString(ENCRYPTED_TOKEN_KEY)\n        set(value) { putEncryptedString(ENCRYPTED_TOKEN_KEY, value) }\n\n    override var uiPreference: Boolean\n        get() = getBoolean(UNENCRYPTED_PREF_KEY) ?: false\n        set(value) { putBoolean(UNENCRYPTED_PREF_KEY, value) }\n}\n```\n\n---\n\n## Testing Templates\n\n**Based on**: `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n### ViewModel Test\n\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n\n    // Mock dependencies\n    private val mockRepository = mockk<ExampleRepository>()\n    private val mutableDataFlow = MutableStateFlow<DataState<ExampleData>>(DataState.Loading)\n\n    @BeforeEach\n    fun setup() {\n        every { mockRepository.dataFlow } returns mutableDataFlow\n    }\n\n    @Test\n    fun `initial state should be correct when there is no saved state`() {\n        val viewModel = createViewModel(state = null)\n        assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)\n    }\n\n    @Test\n    fun `initial state should be correct when there is a saved state`() {\n        val savedState = DEFAULT_STATE.copy(data = \"saved\")\n        val viewModel = createViewModel(state = savedState)\n        assertEquals(savedState, viewModel.stateFlow.value)\n    }\n\n    @Test\n    fun `SubmitClick should call repository and update state on success`() = runTest {\n        val expected = ExampleResult.Success(data = \"result\")\n        coEvery { mockRepository.submitData(any()) } returns expected\n\n        val viewModel = createViewModel()\n        viewModel.stateFlow.test {\n            // Initial state\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n\n            // Updated state after result\n            assertEquals(\n                DEFAULT_STATE.copy(data = \"result\", isLoading = false),\n                awaitItem(),\n            )\n        }\n    }\n\n    @Test\n    fun `SubmitClick should show error dialog on failure`() = runTest {\n        val expected = ExampleResult.Error(message = \"Network error\")\n        coEvery { mockRepository.submitData(any()) } returns expected\n\n        val viewModel = createViewModel()\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n\n            val errorState = awaitItem()\n            assertTrue(errorState.dialogState is ExampleState.DialogState.Error)\n        }\n    }\n\n    @Test\n    fun `BackClick should emit NavigateBack event`() = runTest {\n        val viewModel = createViewModel()\n        viewModel.eventFlow.test {\n            viewModel.trySendAction(ExampleAction.BackClick)\n            assertEquals(ExampleEvent.NavigateBack, awaitItem())\n        }\n    }\n\n    // Helper to create ViewModel with optional saved state\n    private fun createViewModel(\n        state: ExampleState? = DEFAULT_STATE,\n    ): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle(\n            mapOf(KEY_STATE to state),\n        ),\n        exampleRepository = mockRepository,\n    )\n\n    companion object {\n        private val DEFAULT_STATE = ExampleState(\n            isLoading = false,\n            data = null,\n        )\n    }\n}\n```\n\n### Flow Testing with stateEventFlow\n\n```kotlin\n@Test\nfun `SubmitClick should update state and emit event`() = runTest {\n    coEvery { mockRepository.submitData(any()) } returns ExampleResult.Success(\"data\")\n\n    val viewModel = createViewModel()\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        viewModel.trySendAction(ExampleAction.SubmitClick)\n\n        // Assert state change\n        assertEquals(\n            DEFAULT_STATE.copy(data = \"data\"),\n            stateFlow.awaitItem(),\n        )\n\n        // Assert event emission\n        assertEquals(\n            ExampleEvent.ShowToast(\"Success\".asText()),\n            eventFlow.awaitItem(),\n        )\n    }\n}\n```\n",".claude/skills/interacting-with-android-device/SKILL.md":"---\nname: interacting-with-android-device\ndescription: Instructions for capturing UI state, comparing with mocks, and interacting with an Android device using MCP tools backed by ADB.\nallowed-tools: mcp__android-device__capture, mcp__android-device__find_element, mcp__android-device__tap_at, mcp__android-device__tap_element, mcp__android-device__navigate, mcp__android-device__input_text, Bash(adb:*), Bash(sleep:*), Bash(./gradlew install*:*), Read, Glob\n---\n\n# Interacting with Android Device\n\n## Quick Start: MCP Tools\n\nThe `android-device` MCP server provides 6 tools for device interaction. These replace the previous shell scripts with proper XML parsing, structured dumpsys parsing, and native obstruction detection.\n\n**Available tools:**\n- `capture` — Capture UI hierarchy XML and/or screenshot. Params: `{ xml?: boolean, screenshot?: boolean }`. Default: both.\n- `find_element` — Find element by `text` or `content-desc`, return coordinates with **obstruction detection**. Params: `{ text: string }`. Returns JSON with coordinates, bounds, and obstruction status.\n- `tap_at` — Tap at specific coordinates, wait, capture screenshot. Params: `{ x, y, waitSeconds? }`.\n- `tap_element` — Find, tap, and capture in one call (recommended). Params: `{ text, waitSeconds? }`. Auto-adjusts coordinates when obstructed.\n- `navigate` — Navigation actions: home, back, app-drawer. Params: `{ action, waitSeconds? }`. Captures screenshot after action.\n- `input_text` — Type text into the focused field. Params: `{ text, clear? }`. Set `clear: true` to erase existing content first.\n\n**Use these MCP tools instead of raw ADB commands** to save tokens, get structured results, and benefit from automatic obstruction detection.\n\n## 1. Capturing Current State\nTo understand what is currently on the device, use the `capture` tool:\n*   It saves `view.xml` (UI hierarchy) and `screen.png` (screenshot) to the working directory\n*   Read `view.xml` to find coordinates (`bounds`) and properties (like `text` or `resource-id`) of UI elements\n*   Use `screen.png` for visual verification against design mocks\n\n## 2. Interacting with the Device\n\n### Using MCP Tools (Recommended)\n\n*   **Find and tap an element by text** — use `tap_element`:\n    This finds the element, detects obstructions, taps (with adjusted coordinates if needed), and captures a screenshot — all in one call.\n\n*   **Tap at specific coordinates** — use `tap_at`:\n    When you already have coordinates from `find_element` or manual inspection.\n\n*   **Navigate (home, back, app-drawer)** — use `navigate`:\n    Performs the action and captures a screenshot.\n\n*   **Find element without tapping** — use `find_element`:\n    Returns coordinates and full element info. Useful when you need to inspect before acting.\n\n*   **Type text into a field** — use `input_text`:\n    Types text into the currently focused field. Set `clear: true` to erase existing content first. Tap the field before calling this if it isn't already focused.\n\n### Raw ADB Commands (When MCP Tools Aren't Sufficient)\n*   **Key Events**:\n    *   Back: `adb shell input keyevent 4`\n    *   Home: `adb shell input keyevent 3`\n    *   Enter: `adb shell input keyevent 66`\n*   **Scrolling/Swiping**: Use `adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms>` where:\n    *   `(x1, y1)` = starting point\n    *   `(x2, y2)` = ending point\n    *   `duration_ms` = duration in milliseconds (1000ms is typical; adjust for speed/distance)\n    *   **Note**: For expanding containers/drawers, use large distances (e.g., 2400->300 for a 2992px tall screen)\n\n## 3. Obstruction Detection\n\nThe `find_element` and `tap_element` tools automatically detect when another element would intercept the tap. This catches:\n*   **System overlays** (Layer 1): TalkBack floating menu, PiP windows, accessibility services — detected via `dumpsys window windows` touchable regions\n*   **In-app elements** (Layer 2): FABs, dialogs, bottom sheets, snackbars — detected by finding the topmost clickable element at the tap point in the UI hierarchy\n\nWhen obstruction is detected:\n*   Coordinates are **auto-adjusted** to the center of the largest unobstructed strip (top/bottom/left/right of the obstructor)\n*   The response includes the obstructor identity, bounds, and visible region info\n*   If fully obscured (no visible region), the original center is returned as best-effort\n*   **Compose parent wrapper** pattern (identical bounds) is recognized as non-obstruction\n\n## 4. Verification Workflow\nFollow these steps for a complete UI test:\n1.  **Build and Install**: Ensure the latest version of the app is running: `./gradlew installDebug`.\n2.  **Inspect**: Use `capture` to dump the UI hierarchy and take a screenshot.\n3.  **Compare**: Check the current UI against any mock image files in the project.\n4.  **Interact**: Use `tap_element` to tap a UI element by text. The tool handles coordinate calculation and obstruction detection automatically.\n5.  **Verify**: Use `capture` again to confirm the UI has updated as expected (e.g., a new screen is shown, or a success message appeared).\n\n## 5. Examples\n\n### Example: Navigate to Settings and Check for Updates\n```\n# Go to home screen\nnavigate({ action: \"home\" })\n\n# Open app drawer\nnavigate({ action: \"app-drawer\" })\n\n# Find and tap through settings\ntap_element({ text: \"Settings\", waitSeconds: 2 })\ntap_element({ text: \"System\", waitSeconds: 2 })\ntap_element({ text: \"Software updates\", waitSeconds: 2 })\ntap_element({ text: \"Check for update\", waitSeconds: 5 })\n```\n\n### Example: Swiping\nFor swipe gestures not covered by the navigate tool, use raw ADB:\n```bash\nadb shell input swipe 672 2800 672 500 1000 && sleep 1 && adb shell screencap -p /sdcard/screen.png && adb pull /sdcard/screen.png .\n```\n\n## 6. Best Practices\n\n### Coordinate Calculation\n*   Prefer `find_element` or `tap_element` over manual coordinate calculation — they handle bounds parsing, center computation, and obstruction detection automatically\n*   When multiple instances of an element exist (e.g., in prediction row and full list), check the `find_element` response to verify you're targeting the correct one\n\n### Navigation and State Evaluation\n*   **Verify after each interaction**: Don't assume an action succeeded — use `capture` after interactions to confirm the UI changed as expected\n*   **Check both visual and structural state**: Use screenshot for visual verification, XML dump for structural confirmation (element presence, text content, state changes)\n*   **Identify navigation failures early**: If a tap opened the wrong screen, use `navigate({ action: \"back\" })` to recover immediately\n\n### Interaction Patterns\n*   **Scrolling before interaction**: When looking for an element, check if it's visible on screen first. If not, scroll using swipe gestures to reveal it\n*   **Use consistent scroll direction**: For vertical scrolling in lists/settings, use downward swipes (higher Y -> lower Y) to scroll down\n*   **Handle app crashes gracefully**: Don't retry the same action — use back button and try an alternative approach\n*   **Check Accessibility**: Use the `content-desc` and `text` properties in the UI hierarchy to ensure the UI is accessible for screen readers\n\n## 7. Troubleshooting\n\n### Device Not Connected\nIf tools report ADB errors:\n*   Check USB connection or emulator status\n*   Enable USB debugging on the device (Settings > Developer Options > USB Debugging)\n*   Accept the RSA key prompt on the device if asked\n*   Restart the device or disconnect/reconnect the USB cable\n*   Run `adb devices` to verify the device is visible\n\n### MCP Server Not Available\nIf tools are not listed in `/mcp`:\n*   Ensure Node.js 18+ is installed\n*   The server auto-builds on first use via `.mcp.json` at the project root\n*   Check `.claude/mcp/android-device-server/` exists with `package.json`\n*   Try manual build: `cd .claude/mcp/android-device-server && npm install && npm run build`\n",".claude/skills/planning-android-implementation/SKILL.md":"---\nname: planning-android-implementation\nversion: 0.1.0\ndescription: Architecture design and phased implementation planning for Bitwarden Android. Use when planning implementation, designing architecture, creating file inventories, or breaking features into phases. Triggered by \"plan implementation\", \"architecture design\", \"implementation plan\", \"break this into phases\", \"what files do I need\", \"design the architecture\".\n---\n\n# Implementation Planning\n\nThis skill takes a refined specification (ideally from the `refining-android-requirements` skill) and produces a phased implementation plan with architecture design, file inventory, and risk assessment.\n\n**Prerequisite**: A clear set of requirements. If requirements are vague or incomplete, invoke the `refining-android-requirements` skill first.\n\n---\n\n## Step 1: Classify Change\n\nDetermine the change type to guide scope and planning depth:\n\n| Type | Description | Typical Scope |\n|------|-------------|---------------|\n| **New Feature** | Entirely new functionality, screens, or flows | New files + modifications, multi-phase |\n| **Enhancement** | Extending existing feature with new capabilities | Mostly modifications, 1-2 phases |\n| **Bug Fix** | Correcting incorrect behavior | Targeted modifications, single phase |\n| **Refactoring** | Restructuring without behavior change | Modifications only, migration-aware |\n| **Infrastructure** | Build, CI, tooling, or dependency changes | Config files, minimal code changes |\n\nState the classification and rationale before proceeding.\n\n---\n\n## Step 2: Codebase Exploration\n\nSearch the codebase to find reference implementations and integration points. Use the discovery commands from the `build-test-verify` skill as needed.\n\n### Find Pattern Anchors\n\nIdentify 2-3 existing files that serve as templates for the planned work:\n\n```\n**Pattern Anchors:**\n1. [file path] — [why this is a good reference]\n2. [file path] — [why this is a good reference]\n3. [file path] — [why this is a good reference]\n```\n\n### Map Integration Points\n\nIdentify files that must be modified to integrate the new work:\n\n- **Navigation**: Nav graph registrations, route definitions\n- **Dependency Injection**: Hilt modules, `@Provides` / `@Binds` functions\n- **Data Layer**: Repository interfaces, data source interfaces, Room DAOs\n- **API Layer**: Retrofit service interfaces, request/response models\n- **Feature Flags**: Feature flag definitions and checks\n- **Managers**: Single-responsibility data layer classes (see `docs/ARCHITECTURE.md` Managers section)\n- **Test Fixtures**: Shared test utilities in `src/testFixtures/` directories\n- **Product Flavor Source Sets**: Code in `src/standard/` vs `src/main/` for Play Services dependencies\n\n### Document Existing Patterns\n\nNote the specific patterns used by the pattern anchors:\n- State class structure (sealed class, data class fields)\n- Action/Event naming conventions\n- Repository method signatures and return types\n- Test structure and assertion patterns\n\n---\n\n## Step 3: Architecture Design\n\nProduce an ASCII diagram showing component relationships for the planned work:\n\n```\n┌─────────────────┐\n│   Screen        │ ← Compose UI\n│  (Composable)   │\n└────────┬────────┘\n         │ State / Action / Event\n┌────────▼────────┐\n│   ViewModel     │ ← Business logic orchestration\n└────────┬────────┘\n         │ Repository calls\n┌────────▼────────┐\n│   Repository    │ ← Data coordination (sealed class results)\n└───┬────┬────┬───┘\n    │    │    │\n┌───▼───┐ │ ┌─▼──────┐\n│Manager│ │ │Manager │ ← Single-responsibility (optional)\n└───┬───┘ │ └─┬──────┘\n    │     │   │\n┌───▼─────▼───▼────┐\n│   Data Sources   │ ← Raw data (Result<T>, never throw)\n└─┬────┬────┬──────┘\n  │    │    │\n Room Retrofit SDK\n```\n\nAdapt the diagram to show the actual components planned. _Consult `docs/ARCHITECTURE.md` for full data layer patterns and conventions._\n\n### Design Decisions\n\nDocument key architectural decisions in a table:\n\n| Decision | Resolution | Rationale |\n|----------|-----------|-----------|\n| [What needed deciding] | [What was chosen] | [Why] |\n\n---\n\n## Step 4: File Inventory\n\n### Files to Create\n\n| File Path | Type | Pattern Reference |\n|-----------|------|-------------------|\n| [full path] | [ViewModel / Screen / Repository / etc.] | [pattern anchor file] |\n\n**Include in file inventory:**\n- `...Navigation.kt` files for new screens\n- `...Module.kt` Hilt module files for new DI bindings\n- Paired test files (`...Test.kt`) for each new class\n\n### Files to Modify\n\n| File Path | Change Description | Risk Level |\n|-----------|-------------------|------------|\n| [full path] | [what changes] | Low / Medium / High |\n\n**Risk levels:**\n- **Low**: Additive changes (new entries in nav graph, new bindings in Hilt module)\n- **Medium**: Modifying existing logic (adding parameters, new branches)\n- **High**: Changing interfaces, data models, or shared utilities\n\n---\n\n## Step 5: Implementation Phases\n\nBreak the work into sequential phases. Each phase should be independently testable and committable.\n\n**Phase ordering principle**: Foundation → SDK/Data → Network → UI (tests accompany each phase)\n\nFor each phase:\n\n```markdown\n### Phase N: [Name]\n\n**Goal**: [What this phase accomplishes]\n\n**Files**:\n- Create: [list]\n- Modify: [list]\n\n**Tasks**:\n1. [Specific implementation task]\n2. [Specific implementation task]\n3. ...\n\n**Verification**:\n- [Test command or manual verification step]\n\n**Skills**: [Which workflow skills apply — e.g., `implementing-android-code`, `testing-android-code`]\n```\n\n### Phase Guidelines\n\n- Each phase should be small enough to be independently testable and committable\n- Tests are written within the same phase as the code they verify (not deferred to a \"testing phase\")\n- UI phases come after their data dependencies are in place\n- If a phase has more than 5 tasks, consider splitting it\n\n---\n\n## Step 6: Risk & Verification\n\n### Risk Assessment\n\n| Risk | Likelihood | Impact | Mitigation |\n|------|-----------|--------|------------|\n| [What could go wrong] | Low/Med/High | Low/Med/High | [How to prevent or handle] |\n\n### Verification Plan\n\n**Automated Verification:**\n- Unit test commands (from `build-test-verify` skill)\n- Lint/detekt commands\n- Build verification\n\n**Manual Verification:**\n- [Specific manual test scenarios]\n- [Edge cases to manually verify]\n- Verify ViewModel state survives process death (test via `SavedStateHandle` persistence and `Don't keep activities` developer option)\n",".claude/skills/refining-android-requirements/SKILL.md":"---\nname: refining-android-requirements\nversion: 0.1.0\ndescription: Requirements gap analysis and structured specification for Bitwarden Android. Use when refining requirements, analyzing specs, identifying gaps, or producing structured specifications from tickets or descriptions. Triggered by \"refine requirements\", \"gap analysis\", \"spec review\", \"requirements analysis\", \"what's missing from this spec\", \"analyze this ticket\".\n---\n\n# Requirements Refinement\n\nThis skill takes raw requirements (from Jira tickets, Confluence pages, or free-text descriptions) and produces a structured, implementation-ready specification through systematic gap analysis.\n\n**Key principle**: This skill identifies gaps and produces specifications. It does NOT propose solutions or architecture — that is the responsibility of the `planning-android-implementation` skill.\n\n---\n\n## Step 1: Source Consolidation\n\nCombine all input sources into a single working document. For each requirement, note its source:\n\n```\n- [Source: PM-12345] User must be able to configure timeout\n- [Source: Confluence] Timeout range is 1-60 minutes\n- [Source: User] Default timeout should be 15 minutes\n```\n\nFlag any contradictions between sources for immediate resolution.\n\n---\n\n## Step 2: Gap Analysis\n\nEvaluate the consolidated requirements against the following 5-category rubric. For each category, check every item and note whether it is **covered**, **partially covered**, or **missing**.\n\n### A. Functional Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| User actions defined? | What specific user actions trigger this feature? |\n| All states covered? (empty, loading, error, success) | What should the user see in [empty/loading/error] state? |\n| Edge cases identified? | What happens when [boundary condition]? |\n| Cancellation/back navigation flows? | Can the user cancel mid-flow? What happens to partial data? |\n| Input validation rules? | What are the valid ranges/formats for [input]? |\n| Success/failure criteria? | How does the user know the operation succeeded or failed? |\n| Offline behavior? | What happens if this is attempted offline? |\n\n### B. Technical Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Module scope identified? (`:app`, `:authenticator`, shared) | Which module(s) does this feature belong to? |\n| SDK dependencies? | Does this require Bitwarden SDK operations? Which ones? |\n| Data storage approach? (Room, DataStore, in-memory) | Where is the data for this feature persisted? |\n| Network API endpoints? | Which API endpoints are involved? Are they existing or new? |\n| Process death handling? | What state needs to survive process death? |\n| Migration requirements? | Does existing data need migration? |\n| Feature flag needed? | Should this be behind a feature flag for staged rollout? |\n| Product flavors (standard vs fdroid)? | Does this feature depend on Google Play Services? Available on F-Droid? |\n| Data layer tier? | Does this need a new Manager (single-responsibility) or only Repository/DataSource? Consult `docs/ARCHITECTURE.md` Data Layer section. |\n| Streaming vs discrete data? | Is data continuously observed (`DataState<T>` + `StateFlow`) or a one-shot operation (custom sealed class)? See `docs/ARCHITECTURE.md` Repositories section. |\n\n### C. Security Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Data sensitivity classified? | What sensitivity level does this data have? (vault-level, account-level, non-sensitive) |\n| Storage encryption required? | Must this data be encrypted at rest? Via SDK or Android Keystore? |\n| Logout cleanup behavior? | What must be cleared when the user logs out? |\n| Auth-gating? | Does accessing this feature require active authentication? |\n| Input sanitization? | Are there URL or credential inputs that need validation? |\n| Sensitive data in ViewModel state? | Will passwords, tokens, or keys appear in state? Must use `@IgnoredOnParcel`. See `implementing-android-code` skill Section F. |\n| SDK crypto context isolation? | Does this use vault encryption? Must use `ScopedVaultSdkSource` for multi-account safety. See CLAUDE.md Security Rules. |\n\n### D. UX/UI Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| UI copy/strings defined? | What text should appear for [label/button/message]? |\n| Error messages specified? | What should the error message say when [failure case]? |\n| Loading states designed? | Should loading show a spinner, skeleton, or shimmer? |\n| Navigation flow clear? | Where does the user go after [action]? Back stack behavior? |\n| Accessibility considerations? | Are there content descriptions or focus order requirements? |\n| Toast/snackbar/dialog for feedback? | What feedback mechanism for [action result]? |\n\n### E. Cross-Cutting Concerns\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Multi-account behavior? | How does this behave with multiple accounts? Per-account or global? |\n| Backwards compatibility? | Does this affect existing users? Migration path? |\n| Feature flag strategy? | Is this behind a server-side or local feature flag? |\n| Analytics/logging? | Are there analytics events to track? |\n| Bitwarden Authenticator impact? | Does this affect the `:authenticator` module? |\n| F-Droid compatibility? | Does this degrade gracefully without Google Play Services (no push notifications, no Play Integrity)? |\n\n---\n\n## Step 3: Present Gaps\n\nOrganize all identified gaps into two categories:\n\n### Blocking Questions\n\nQuestions that **must** be answered before implementation can begin because they change the architecture, data model, or core flow.\n\nFormat each question as:\n\n```\n**G[N]** ([Category]) — [Question text]\n  Context: [Why this matters / what depends on the answer]\n```\n\n### Non-Blocking Questions\n\nQuestions that have **reasonable defaults** and can be resolved during implementation. Note the assumed default.\n\nFormat each question as:\n\n```\n**G[N]** ([Category]) — [Question text]\n  Default assumption: [What we'll assume if not answered]\n  Context: [Why this matters]\n```\n\n---\n\n## Step 4: Produce Specification\n\nAfter the user answers blocking questions (and optionally non-blocking ones), produce a structured specification:\n\n```markdown\n## Overview\n\n[1-2 paragraph summary of the feature, its purpose, and scope]\n\n## Functional Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| FR1 | [requirement] | [source] | [any notes] |\n| FR2 | ... | ... | ... |\n\n## Technical Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| TR1 | [requirement] | [source] | [any notes] |\n| TR2 | ... | ... | ... |\n\n## Security Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| SR1 | [requirement] | [source] | [any notes] |\n\n## UX Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| UX1 | [requirement] | [source] | [any notes] |\n\n## Open Items\n\nNon-blocking items with assumed defaults that may be revisited:\n\n| ID | Question | Assumed Default | Category |\n|----|----------|----------------|----------|\n| G[N] | [question] | [default] | [category] |\n\n## Source Documentation\n\n| Source | Type | Link |\n|--------|------|------|\n| [name] | Jira / Confluence / User-provided | [link if available] |\n```\n\n### Output Guidelines\n\n- Requirements use numbered IDs (FR1, TR1, SR1, UX1) for traceability through implementation\n- Each requirement cites its source (ticket, page, or user-provided)\n- Technical requirements use table format for structured key/value data\n- Interface signatures are included as fenced code blocks when applicable\n- Open items preserve the gap ID (G[N]) for cross-referencing",".claude/skills/reviewing-changes/SKILL.md":"---\nname: reviewing-changes\ndescription: Android-specific code review checklist and MVVM/Compose pattern validation for Bitwarden Android — use this for any review task, even if the user doesn't explicitly ask for a \"checklist\". Detects change type automatically and loads the right review strategy (feature additions, bug fixes, UI refinements, refactoring, dependency updates, infrastructure). Triggered by \"review PR\", \"review changes\", \"review this code\", \"check this code\", \"Android review\", code review requests on Kotlin/ViewModel/Composable/Repository/Gradle files, or any time someone asks to look at a diff, PR, or code changes in bitwarden/android.\n---\n\n# Reviewing Changes - Android Additions\n\nThis skill provides Android-specific workflow additions that complement the base `bitwarden-code-reviewer` agent standards.\n\n## Instructions\n\n**IMPORTANT**: Work systematically through each step before providing feedback. Each checklist file includes structured thinking guidance for its review passes.\n\n### Step 1: Retrieve Additional Details\n\nRetrieve any additional information linked to the pull request using available tools (JIRA MCP, GitHub API).\n\nIf pull request title and message do not provide enough context, request additional details from the reviewer:\n- Link a JIRA ticket\n- Associate a GitHub issue\n- Link to another pull request\n- Add more detail to the PR title or body\n\n**Android metadata checks** — flag as ❓ if any of these are missing:\n- PR includes `*Screen.kt` or Composable changes but has no screenshots\n- PR adds new `ViewModel` or `Repository` but has no test plan or test file changes\n\n### Step 2: Detect Change Type with Android Refinements\n\nUse the base change type detection from the agent, with Android-specific refinements:\n\n**Android-specific patterns:**\n- **Feature Addition**: New `ViewModel`, new `Repository`, new `@Composable` functions, new `*Screen.kt` files\n- **UI Refinement**: Changes only in `*Screen.kt`, `*Composable.kt`, `ui/` package files\n- **Infrastructure**: Changes to `.github/workflows/`, `gradle/`, `build.gradle.kts`, `libs.versions.toml`\n- **Dependency Update**: Changes only to `libs.versions.toml` or `build.gradle.kts` with version bumps\n\n### Step 3: Load Appropriate Checklist\n\nBased on detected type, read the relevant checklist file:\n\n- **Dependency Update** → `checklists/dependency-update.md` (expedited review)\n- **Bug Fix** → `checklists/bug-fix.md` (focused review)\n- **Feature Addition** → `checklists/feature-addition.md` (comprehensive review)\n- **UI Refinement** → `checklists/ui-refinement.md` (design-focused review)\n- **Refactoring** → `checklists/refactoring.md` (pattern-focused review)\n- **Infrastructure** → `checklists/infrastructure.md` (tooling-focused review)\n\nThe checklist provides:\n- Multi-pass review strategy\n- Type-specific focus areas\n- What to check and what to skip\n- Structured thinking guidance\n\n### Step 4: Execute Review Following Checklist\n\nFollow the checklist's multi-pass strategy, thinking through each pass systematically.\n\n### Step 5: Consult Android Reference Materials As Needed\n\nLoad reference files only when needed for specific questions:\n\n- **Re-reviews** → invoke `reviewing-incremental-changes` agent skill; scope to changed lines only, do not flag new issues in unchanged code\n- **Issue prioritization** → `reference/priority-framework.md` (Critical vs Suggested vs Optional)\n- **Phrasing feedback** → `reference/review-psychology.md` (questions vs commands, I-statements)\n- **Architecture questions** → `reference/architectural-patterns.md` (MVVM, Hilt DI, module org, error handling)\n- **Security questions (quick reference)** → `reference/security-patterns.md` (common patterns and anti-patterns)\n- **Security questions (comprehensive)** → `docs/ARCHITECTURE.md#security` (full zero-knowledge architecture)\n- **Testing questions** → `reference/testing-patterns.md` (unit tests, mocking, null safety)\n- **UI questions** → `reference/ui-patterns.md` (Compose patterns, theming)\n- **Style questions (project-specific)** → `reference/style-patterns.md` (Kotlin rules enforced in review)\n- **Style questions (general)** → `docs/STYLE_AND_BEST_PRACTICES.md`\n\n## Core Principles\n\n- **Priority order**: Security → Correctness → Breaking Changes → Performance → Maintainability\n- **Appropriate depth**: Match review rigor to change complexity and risk\n- **Specific references**: Always use `file:line_number` format for precise location\n- **Actionable feedback**: Say what to do and why, not just what's wrong\n- **Efficient reviews**: Use multi-pass strategy, skip what's not relevant\n- **Android patterns**: Validate MVVM, Hilt DI, Compose conventions, Kotlin idioms\n",".claude/skills/reviewing-changes/checklists/bug-fix.md":"# Bug Fix Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Bug\n\n**1. Understand root cause:**\n- What was the broken behavior?\n- What caused it?\n- How does this fix address the root cause?\n\n**2. Assess scope:**\n- How many files changed?\n- Is this a targeted fix or broader refactoring?\n- Does this affect multiple features?\n\n**3. Check for side effects:**\n- Could this break other features?\n- Are there edge cases not considered?\n\n### Second Pass: Verify the Fix\n\n**4. Code changes:**\n- Does the fix make sense?\n- Is it the simplest solution?\n- Any unnecessary changes included?\n\n**5. Testing:**\n- Is there a regression test?\n- Does test verify the bug is fixed?\n- Are edge cases covered?\n\n**6. Related code:**\n- Same pattern in other places that might have same bug?\n- Should other similar code be fixed too?\n\n## What to CHECK\n\n✅ **Root Cause Analysis**\n- Does the fix address the root cause or just symptoms?\n- Is the explanation in PR/commit clear?\n\n✅ **Regression Testing**\n- Is there a new test that would fail without this fix?\n- Does test cover the reported bug scenario?\n- Are related edge cases tested?\n\n✅ **Side Effects**\n- Could this break existing functionality?\n- Are there similar code paths that need checking?\n- Does this change behavior in unexpected ways?\n\n✅ **Fix Scope**\n- Is the fix appropriately scoped (not too broad, not too narrow)?\n- Are all instances of the bug fixed?\n- Any related bugs discovered during investigation?\n\n## What to SKIP\n\n❌ **Full Architecture Review** - Unless fix reveals architectural problems\n❌ **Comprehensive Testing Review** - Focus on regression tests, not entire test suite\n❌ **Major Refactoring Suggestions** - Unless directly related to preventing similar bugs\n\n## Red Flags\n\n🚩 **No test for the bug** - How will we prevent regression?\n🚩 **Fix doesn't match root cause** - Is this fixing symptoms?\n🚩 **Broad changes beyond the bug** - Should this be split into separate PRs?\n🚩 **Similar patterns elsewhere** - Should those be fixed too?\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"Can we add a test that would fail without this fix?\"\n- \"I see this pattern in [other file] - does it have the same issue?\"\n- \"Is this fixing the root cause or masking the symptom?\"\n- \"Could this change affect [related feature]?\"\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n**Overall Assessment:** APPROVE\n\nSee inline comments for suggested improvements.\n```\n\n**Inline comment examples:**\n\n```\n**data/auth/BiometricRepository.kt:120** - SUGGESTED: Extract null handling\n\n<details>\n<summary>Details</summary>\n\nRoot cause analysis: BiometricPrompt result was nullable but code assumed non-null, causing crash on cancellation (PM-12345).\n\nConsider extracting null handling pattern:\n\n\\```kotlin\nprivate fun handleBiometricResult(result: BiometricPrompt.AuthenticationResult?): AuthResult {\n    return result?.let { AuthResult.Success(it) } ?: AuthResult.Cancelled\n}\n\\```\n\nThis pattern could be reused if we add other biometric auth points.\n</details>\n```\n\n```\n**app/auth/BiometricViewModel.kt:89** - SUGGESTED: Add regression test\n\n<details>\n<summary>Details</summary>\n\nAdd test for cancellation scenario to prevent regression:\n\n\\```kotlin\n@Test\nfun `when biometric cancelled then returns cancelled state`() = runTest {\n    coEvery { repository.authenticate() } returns Result.failure(CancelledException())\n    viewModel.onBiometricAuth()\n    assertEquals(AuthState.Cancelled, viewModel.state.value)\n}\n\\```\n\nThis prevents regression of the bug just fixed.\n</details>\n```\n",".claude/skills/reviewing-changes/checklists/dependency-update.md":"# Dependency Update Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Identify and Assess\n\n**1. Identify the change:**\n- Which library? Old version → New version?\n- Major (X.0.0), Minor (0.X.0), or Patch (0.0.X) version change?\n- Single dependency or multiple?\n\n**2. Check compilation safety:**\n- Any imports in codebase that might break?\n- Any deprecated APIs we're currently using?\n- Check if this is a breaking change version\n\n### Second Pass: Deep Analysis\n\n**3. Review release notes** (if available):\n- Breaking changes mentioned?\n- Security fixes included?\n- New features we should know about?\n- Deprecations that affect our usage?\n\n**4. Verify consistency:**\n- If updating androidx library, are related libraries updated consistently?\n- BOM (Bill of Materials) consistency if applicable?\n- Test dependencies updated alongside main dependencies?\n\n## What to CHECK\n\n✅ **Compilation Safety**\n- Look for API deprecations in our codebase\n- Check if import statements still valid\n- Major version bumps require extra scrutiny\n- Beta/alpha versions need stability assessment\n\n✅ **Security Implications** (if applicable)\n- Security-related libraries (crypto, auth, networking)?\n- Check for CVEs addressed in release notes\n- Review security advisories for this library\n\n✅ **Testing Implications**\n- Does this affect test utilities?\n- Are there breaking changes in test APIs?\n- Do existing tests still cover the same scenarios?\n\n✅ **Changelog Review**\n- Read release notes for breaking changes\n- Note any behavioral changes\n- Check migration guides if major version\n\n## What to SKIP\n\n❌ **Full Architecture Review** - No code changed, patterns unchanged\n❌ **Code Style Review** - No code to review\n❌ **New Test Requirements** - Unless API changed significantly\n❌ **Security Deep-Dive** - Unless crypto/auth/networking library\n❌ **Performance Analysis** - Unless release notes mention performance changes\n\n## Red Flags (Escalate to Full Review)\n\n🚩 **Major version bump** (e.g., 1.x → 2.0) - Read `checklists/feature-addition.md`\n🚩 **Security/crypto library** - Read `reference/architectural-patterns.md` and `docs/ARCHITECTURE.md#security`\n🚩 **Breaking changes in release notes** - Read relevant code sections carefully\n🚩 **Multiple dependency updates at once** - Check for interaction risks\n🚩 **Beta/Alpha versions** - Assess stability concerns and rollback plan\n\nIf any red flags present, escalate to more comprehensive review using appropriate checklist.\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Reviews\n\n### Example 1: Simple Patch Version (No Critical Issues)\n\n```markdown\n**Overall Assessment:** APPROVE\n\nSee inline comments for all issue details.\n```\n\n**Inline comment example:**\n```\n**libs.versions.toml:45** - SUGGESTED: Beta version in production\n\n<details>\n<summary>Details</summary>\n\nandroidx.credentials updated from 1.5.0 to 1.6.0-beta03\n\nMonitor for stability issues - beta releases may have unexpected behavior in production.\n\nChangelog: Adds support for additional credential types, internal bug fixes.\n</details>\n```\n\n### Example 2: Major Version with Breaking Changes (With Critical Issues)\n\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Breaking API changes in Retrofit 3.0.0 (network/api/BitwardenApiService.kt)\n- Breaking API changes in Retrofit 3.0.0 (network/api/VaultApiService.kt)\n\nSee inline comments for migration details.\n```\n\n**Inline comment example:**\n```\n**network/api/BitwardenApiService.kt:15** - CRITICAL: Breaking API changes\n\n<details>\n<summary>Details and fix</summary>\n\nRetrofit 3.0.0 removes `Call<T>` return type. Migration required:\n\n\\```kotlin\n// Before\nfun getUser(): Call<UserResponse>\n\n// After\nsuspend fun getUser(): Response<UserResponse>\n\\```\n\nUpdate all API service interfaces to use suspend functions, update call sites to use coroutines instead of enqueue/execute, and update tests accordingly.\n\nConsider creating a separate PR for this migration due to scope.\n\nReference: https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-300\n</details>\n```\n",".claude/skills/reviewing-changes/checklists/feature-addition.md":"# Feature Addition Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: High-Level Assessment\n\n**1. Understand the feature:**\n- Read PR description - what problem does this solve?\n- Identify user-facing changes vs internal changes\n- Note any security implications (auth, encryption, data handling)\n\n**2. Scan file structure:**\n- Which modules affected? (app, data, network, ui, core?)\n- Are files organized correctly per module structure?\n- Any new public APIs introduced?\n\n**3. Initial risk assessment:**\n- Does this touch sensitive data or security-critical paths?\n- Does this affect existing features or only add new ones?\n- Are there obvious compilation or null safety issues?\n\n### Second Pass: Architecture Deep-Dive\n\n**4. MVVM + UDF Pattern Compliance:**\n- ViewModels properly structured?\n- State management using StateFlow?\n- Business logic in correct layer?\n\n**5. Dependency Injection:**\n- Hilt DI used correctly?\n- Dependencies injected, not manually instantiated?\n- Proper scoping applied?\n\n**6. Module Organization:**\n- Code placed in correct modules?\n- No circular dependencies introduced?\n- Proper separation of concerns?\n\n**7. Error Handling:**\n- Using Result types, not exception-based handling?\n- Errors propagated correctly through layers?\n\n### Third Pass: Details and Quality\n\n**8. Testing:**\n- Unit tests for ViewModels and repositories?\n- Test coverage for edge cases and error scenarios?\n- Tests verify behavior, not implementation?\n\n**9. Code Quality:**\n- Null safety handled properly?\n- Public APIs have KDoc documentation?\n- Naming follows project conventions?\n\n**10. Security:**\n- Sensitive data encrypted properly?\n- Authentication/authorization handled correctly?\n- Zero-knowledge architecture preserved?\n\n## Architecture Review\n\nRead `reference/architectural-patterns.md` for full patterns and code examples.\n\n**Check these four areas:**\n- **MVVM/UDF**: ViewModel exposes `StateFlow` (not `MutableStateFlow`), business logic in Repository, UI is stateless\n- **Hilt DI**: `@HiltViewModel` + `@Inject constructor`, inject interfaces not implementations, no manual instantiation\n- **Module placement**: UI in `:ui`/`:app`, data in `:data`, network in `:network`, no circular dependencies\n- **Error handling**: `Result<T>` / `runCatching` throughout — no thrown exceptions from data layer\n\n## Security Review\n\nReference: `docs/ARCHITECTURE.md#security`\n\n**Critical Security Checks:**\n\n- **Sensitive data encrypted**: Passwords, keys, tokens use Android Keystore or EncryptedSharedPreferences\n- **No plaintext secrets**: No passwords/keys in logs, memory dumps, or SharedPreferences\n- **Input validation**: All user-provided data validated and sanitized\n- **Authentication tokens**: Securely stored and transmitted\n- **Zero-knowledge architecture**: Encryption happens client-side, server never sees plaintext\n\n**Red Flags:**\n```kotlin\n// ❌ CRITICAL - Plaintext storage\nsharedPreferences.edit {\n    putString(\"pin\", userPin)  // Must use EncryptedSharedPreferences\n}\n\n// ❌ CRITICAL - Logging sensitive data\nLog.d(\"Auth\", \"Password: $password\")  // Never log sensitive data\n\n// ❌ CRITICAL - Weak encryption\nval cipher = Cipher.getInstance(\"DES\")  // Use AES-256-GCM\n\n// ✅ GOOD - Keystore encryption\nval encryptedData = keystoreManager.encrypt(sensitiveData)\nsecureStorage.store(encryptedData)\n```\n\n**If security concerns found, classify as CRITICAL using `reference/priority-framework.md`**\n\n## Testing Review\n\nReference: `reference/testing-patterns.md`\n\n**Required Test Coverage:**\n\n- **ViewModels**: Unit tests for state transitions, actions, error scenarios\n- **Repositories**: Unit tests for data transformations, error handling\n- **Business logic**: Unit tests for complex algorithms, calculations\n- **Edge cases**: Null inputs, empty states, network failures, concurrent operations\n\n**Test Quality:**\n```kotlin\n// ✅ GOOD - Tests behavior\n@Test\nfun `when login succeeds then state updates to success`() = runTest {\n    val viewModel = LoginViewModel(mockRepository)\n\n    coEvery { mockRepository.login(any(), any()) } returns Result.success(User())\n\n    viewModel.onLoginClicked(\"user\", \"pass\")\n\n    viewModel.state.test {\n        assertEquals(LoginState.Success, awaitItem())\n    }\n}\n\n// ❌ BAD - Tests implementation\n@Test\nfun `repository is called with correct parameters`() {\n    // This is testing internal implementation, not behavior\n}\n```\n\n**Testing Frameworks:**\n- JUnit 5 for test structure\n- MockK for mocking\n- Turbine for Flow testing\n- Kotlinx-coroutines-test for coroutine testing\n\n## Code Quality\n\n### Null Safety\n\n- No `!!` (non-null assertion) without clear safety guarantee\n- Platform types (from Java) handled with explicit nullability\n- Nullable types have proper null checks or use safe operators (`?.`, `?:`)\n\n```kotlin\n// ❌ BAD - Unsafe assertion\nval result = apiService.getData()!!  // Could crash\n\n// ✅ GOOD - Safe handling\nval result = apiService.getData() ?: return State.Error(\"No data\")\n\n// ❌ BAD - Platform type unchecked\nval intent: Intent = getIntent()  // Could be null from Java\nintent.getStringExtra(\"key\")  // Potential NPE\n\n// ✅ GOOD - Explicit nullability\nval intent: Intent? = getIntent()\nintent?.getStringExtra(\"key\")\n```\n\n### Documentation\n\n- **Public APIs**: Have KDoc comments explaining purpose, parameters, return values\n- **Complex algorithms**: Explained in comments\n- **Non-obvious behavior**: Documented with rationale\n\n```kotlin\n// ✅ GOOD - Documented public API\n/**\n * Encrypts the given data using AES-256-GCM with a key from Android Keystore.\n *\n * @param plaintext The data to encrypt\n * @return Result containing encrypted data or encryption error\n */\nsuspend fun encrypt(plaintext: ByteArray): Result<EncryptedData>\n```\n\n### Style Compliance\n\nReference: `docs/STYLE_AND_BEST_PRACTICES.md`\n\nOnly flag style issues if:\n- Not caught by linters (Detekt, ktlint)\n- Have architectural implications\n- Significantly impact readability\n\nSkip minor formatting (spaces, line breaks, etc.) - linters handle this.\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Providing Feedback\n\nUse `reference/review-psychology.md` for phrasing guidance.\n\n**Key principles:**\n- **Ask questions** for design decisions: \"Can we use the existing BitwardenTextField component here?\"\n- **Be prescriptive** for clear violations: \"Change MutableStateFlow to StateFlow (MVVM pattern requirement)\"\n- **Explain rationale**: \"This exposes mutable state, violating unidirectional data flow\"\n- **Use I-statements**: \"It's hard for me to understand this logic without comments\"\n- **Avoid condescension**: Don't use \"just\", \"simply\", \"obviously\"\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n",".claude/skills/reviewing-changes/checklists/infrastructure.md":"# Infrastructure Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Change\n\n**1. Identify the goal:**\n- What problem does this solve?\n- Is this optimization, fix, or new capability?\n- What's the expected impact?\n\n**2. Assess risk:**\n- Does this affect production builds?\n- Could this break CI/CD pipelines?\n- Impact on developer workflow?\n\n**3. Performance implications:**\n- Will builds be faster or slower?\n- CI time impact?\n- Resource usage changes?\n\n### Second Pass: Verify Implementation\n\n**4. Configuration correctness:**\n- Syntax valid?\n- References correct?\n- Secrets/credentials handled securely?\n\n**5. Impact analysis:**\n- What workflows/builds are affected?\n- Rollback plan if this breaks?\n- Documentation for team?\n\n**6. Testing strategy:**\n- How can this be tested before merge?\n- Canary/gradual rollout possible?\n- Monitoring for issues post-merge?\n\n## What to CHECK\n\n✅ **Configuration Correctness**\n- YAML/Groovy syntax valid\n- File references correct\n- Version numbers/tags valid\n- Conditional logic sound\n\n✅ **Security**\n- No hardcoded secrets or credentials\n- GitHub secrets used properly\n- Permissions appropriately scoped\n- No sensitive data in logs\n\n✅ **Performance Impact**\n- Build time implications understood\n- CI queue time impact assessed\n- Resource usage reasonable\n\n✅ **Rollback Plan**\n- Can this be reverted easily?\n- Dependencies on other changes?\n- Gradual rollout possible?\n\n✅ **Documentation**\n- Changes documented for team?\n- README or CONTRIBUTING updated?\n- Breaking changes clearly noted?\n\n## What to SKIP\n\n❌ **Bikeshedding Configuration** - Unless clear performance/maintenance benefit\n❌ **Over-Optimization** - Unless current system has proven problems\n❌ **Suggesting Major Rewrites** - Unless current approach is fundamentally broken\n\n## Red Flags\n\n🚩 **Hardcoded secrets** - Use GitHub secrets or secure storage\n🚩 **No rollback plan** - Critical infrastructure should be revertible\n🚩 **Untested changes** - CI changes should be validated\n🚩 **Breaking changes without notice** - Team needs advance warning\n🚩 **Performance regression** - Builds shouldn't get significantly slower\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"What's the rollback plan if this breaks CI?\"\n- \"Can we test this on a feature branch before main?\"\n- \"Will this impact build times? By how much?\"\n- \"Should this be documented in CONTRIBUTING.md?\"\n\n## Common Infrastructure Patterns\n\n### GitHub Actions\n\n```yaml\n# ✅ GOOD - Secure, clear, tested\nname: Build and Test\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30  # Prevent runaway builds\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run tests\n        env:\n          API_KEY: ${{ secrets.API_KEY }}  # Secure secret usage\n        run: ./gradlew test\n\n# ❌ BAD - Insecure, unclear\nname: Build\non: push  # Too broad, runs on all branches\njobs:\n  build:\n    runs-on: ubuntu-latest\n    # No timeout - could run forever\n    steps:\n      - run: |\n          export API_KEY=\"hardcoded_key_here\"  # Hardcoded secret!\n          ./gradlew test\n```\n\n### Gradle Configuration\n\n```kotlin\n// ✅ GOOD - Clear, maintainable\ndependencies {\n    implementation(libs.androidx.core.ktx)  // Version catalog\n    implementation(libs.hilt.android)\n\n    testImplementation(libs.junit5)\n    testImplementation(libs.mockk)\n}\n\n// ❌ BAD - Hardcoded versions\ndependencies {\n    implementation(\"androidx.core:core-ktx:1.12.0\")  // Hardcoded version\n    implementation(\"com.google.dagger:hilt-android:2.48\")\n}\n```\n\n### Build Optimization\n\n```kotlin\n// ✅ GOOD - Parallel, cached\ntasks.register(\"checkAll\") {\n    dependsOn(\"detekt\", \"ktlintCheck\", \"testStandardDebug\")\n    group = \"verification\"\n    description = \"Run all checks in parallel\"\n\n    // Enable caching for faster builds\n    outputs.upToDateWhen { false }\n}\n\n// ❌ BAD - Sequential, no caching\ntasks.register(\"checkAll\") {\n    doLast {\n        exec { commandLine(\"./gradlew\", \"detekt\") }\n        exec { commandLine(\"./gradlew\", \"ktlintCheck\") }  // Sequential\n        exec { commandLine(\"./gradlew\", \"test\") }\n    }\n}\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n## Summary\nOptimizes CI build by parallelizing test execution and caching dependencies\n\nImpact: Estimated 40% reduction in CI time (12 min → 7 min per build)\n\n## Critical Issues\nNone\n\n## Suggested Improvements\n\n**.github/workflows/build.yml:23** - Add timeout for safety\n```yaml\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30  # Prevent builds from hanging\n    steps:\n      # ...\n```\nThis prevents runaway builds if something goes wrong.\n\n**.github/workflows/build.yml:45** - Consider matrix strategy for module tests\nCan we run module tests in parallel using a matrix strategy?\n```yaml\nstrategy:\n  matrix:\n    module: [app, data, network, ui]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: ./gradlew :${{ matrix.module }}:test\n```\nThis could further reduce CI time.\n\n**build.gradle.kts:12** - Document caching strategy\nCan we add a comment explaining the caching configuration?\nFuture maintainers will appreciate understanding why these specific cache keys are used.\n\n## Rollback Plan\nIf CI breaks:\n- Revert commit: `git revert [commit-hash]`\n- Previous workflow available at: `.github/workflows/build.yml@main^`\n- Monitor CI times at: https://github.com/[org]/[repo]/actions\n```\n",".claude/skills/reviewing-changes/checklists/refactoring.md":"# Refactoring Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Refactoring\n\n**1. Understand the goal:**\n- What pattern is being improved?\n- Why is this refactoring needed?\n- What's the scope of changes?\n\n**2. Assess completeness:**\n- Are all instances refactored or just some?\n- Are there related areas that should also change?\n- Is the migration complete or partial?\n\n**3. Risk assessment:**\n- Does this change behavior?\n- How many files affected?\n- Are tests updated to reflect changes?\n\n### Second Pass: Verify Consistency\n\n**4. Pattern consistency:**\n- Is the new pattern applied consistently throughout?\n- Are there missed instances of the old pattern?\n- Does this match established project patterns?\n\n**5. Migration completeness:**\n- Old pattern fully removed or deprecated?\n- All usages updated?\n- Documentation updated?\n\n**6. Test coverage:**\n- Do tests still pass?\n- Are tests refactored to match?\n- Does behavior remain unchanged?\n\n## What to CHECK\n\n✅ **Pattern Consistency**\n- New pattern applied consistently across all touched code\n- Follows established project patterns (MVVM, DI, error handling)\n- No mix of old and new patterns\n\n✅ **Migration Completeness**\n- All instances of old pattern updated?\n- Deprecated methods removed or marked @Deprecated?\n- Related code also updated (tests, docs)?\n\n✅ **Behavior Preservation**\n- Refactoring doesn't change behavior\n- Tests still pass\n- Edge cases still handled\n\n✅ **Deprecation Strategy** (if applicable)\n- Old APIs marked @Deprecated with migration guidance\n- Replacement clearly documented\n- Timeline for removal specified\n\n## What to SKIP\n\n❌ **Suggesting Additional Refactorings** - Unless directly related to current changes\n❌ **Scope Creep** - Don't request refactoring of untouched code\n❌ **Perfection** - Better code is better than perfect code\n\n## Red Flags\n\n🚩 **Incomplete migration** - Mix of old and new patterns\n🚩 **Behavior changes** - Refactoring shouldn't change behavior\n🚩 **Broken tests** - Tests should be updated to match refactoring\n🚩 **Undocumented pattern** - New pattern should be clear to team\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"I see the old pattern still used in [file:line] - should that be updated too?\"\n- \"Can we add @Deprecated to the old method with migration guidance?\"\n- \"How do we ensure this behavior remains the same?\"\n- \"Should this pattern be documented in ARCHITECTURE.md?\"\n\n## Common Refactoring Patterns\n\n### Extract Interface/Repository\n\n```kotlin\n// ✅ GOOD - Complete migration\ninterface FeatureRepository {\n    suspend fun getData(): Result<Data>\n}\n\nclass FeatureRepositoryImpl @Inject constructor(\n    private val apiService: FeatureApiService\n) : FeatureRepository {\n    override suspend fun getData(): Result<Data> = runCatching {\n        apiService.fetchData()\n    }\n}\n\n// All usages updated to inject interface\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository  // Interface\n) : ViewModel()\n\n// ❌ BAD - Incomplete migration\n// Some files still inject FeatureRepositoryImpl directly\n```\n\n### Modernize Error Handling\n\n```kotlin\n// ✅ GOOD - Complete migration\n// Old exception-based removed\nsuspend fun fetchData(): Result<Data> = runCatching {\n    apiService.getData()\n}\n\n// All call sites updated\nrepository.fetchData().fold(\n    onSuccess = { /* handle */ },\n    onFailure = { /* handle */ }\n)\n\n// ❌ BAD - Mixed patterns\n// Some functions use Result, others still throw exceptions\n```\n\n### Extract Reusable Component\n\n```kotlin\n// ✅ GOOD - Complete extraction\n// Component moved to :ui module\n@Composable\nfun BitwardenButton(\n    text: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier\n)\n\n// All usages updated to use new component\n// Old inline button implementations removed\n\n// ❌ BAD - Incomplete extraction\n// Some screens use new component, others still have inline implementation\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Reviews\n\n### Example 1: Refactoring with Incomplete Migration\n\n**Context**: Refactoring authentication to Repository pattern, but one ViewModel still uses old pattern\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Incomplete migration (app/vault/VaultViewModel.kt:89)\n\nSee inline comments for details.\n```\n\n**Inline Comment 1** (on `app/vault/VaultViewModel.kt:89`):\n```markdown\n**IMPORTANT**: Incomplete migration\n\n<details>\n<summary>Details and fix</summary>\n\nThis ViewModel still injects AuthManager directly. Should it use AuthRepository like the other 11 ViewModels?\n\n\\```kotlin\n// Current (old pattern)\nclass VaultViewModel @Inject constructor(\n    private val authManager: AuthManager\n)\n\n// Should be (new pattern)\nclass VaultViewModel @Inject constructor(\n    private val authRepository: AuthRepository\n)\n\\```\n\nThis is the only ViewModel still using the old pattern.\n</details>\n```\n\n**Inline Comment 2** (on `data/auth/AuthManager.kt:1`):\n```markdown\n**SUGGESTED**: Add deprecation notice\n\n<details>\n<summary>Details</summary>\n\nCan we add @Deprecated to AuthManager to guide future development?\n\n\\```kotlin\n@Deprecated(\n    message = \"Use AuthRepository interface instead\",\n    replaceWith = ReplaceWith(\"AuthRepository\"),\n    level = DeprecationLevel.WARNING\n)\nclass AuthManager @Inject constructor(...)\n\\```\n\nThis helps prevent new code from using the old pattern.\n</details>\n```\n\n---\n\n### Example 2: Clean Refactoring (No Issues)\n\n**Context**: Refactoring with complete migration, all patterns followed correctly, tests passing\n\n**Review Comment:**\n```markdown\n**Overall Assessment:** APPROVE\n\nClean refactoring moving ExitManager to :ui module. Follows established patterns, eliminates duplication, tests updated correctly.\n```\n\n**Token count:** ~30 tokens (vs ~800 for verbose format)\n\n**Why this works:**\n- 3 lines total\n- Clear approval decision\n- Briefly notes what was done\n- No elaborate sections, checkmarks, or excessive praise\n- Author gets immediate green light to merge\n\n**What NOT to do for clean refactorings:**\n```markdown\n❌ DO NOT create these sections:\n\n## Summary\nThis PR successfully refactors ExitManager into shared code...\n\n## Key Strengths\n- ✅ Follows established module organization patterns\n- ✅ Removes code duplication between apps\n- ✅ Improves test coverage\n- ✅ Maintains consistent behavior\n[...20 more checkmarks...]\n\n## Code Quality & Architecture\n**Architectural Compliance:** ✅\n- Correctly places manager in :ui module\n- Follows established pattern for UI-layer managers\n[...detailed analysis...]\n\n## Changes\n- ✅ Moved ExitManager interface from app → ui module\n- ✅ Moved ExitManagerImpl from app → ui module\n[...listing every file...]\n```\n\nThis is excessive. **For clean PRs: 2-3 lines maximum.**\n",".claude/skills/reviewing-changes/checklists/ui-refinement.md":"# UI Refinement Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Visual Changes\n\n**1. Understand the changes:**\n- What visual/UX problem is being solved?\n- Are there designs or screenshots to reference?\n- Is this a bug fix or enhancement?\n\n**2. Component usage:**\n- Using existing components from `:ui` module?\n- Any new custom components created?\n- Could existing components be reused?\n\n### Second Pass: Implementation Review\n\n**3. Compose best practices:**\n- Composables properly structured?\n- State hoisted correctly?\n- Preview composables included?\n\n**4. Accessibility:**\n- Content descriptions for images/icons?\n- Semantic properties for screen readers?\n- Touch targets meet minimum size (48dp)?\n\n**5. Design consistency:**\n- Using theme colors, spacing, typography?\n- Consistent with other screens?\n- Responsive to different screen sizes?\n\n## What to CHECK\n\n✅ **Compose Best Practices**\n- Composables are stateless where possible\n- State hoisting follows patterns\n- Side effects (LaunchedEffect, DisposableEffect) used correctly\n- Preview composables provided for development\n\n✅ **Component Reuse**\n- Using existing BitwardenButton, BitwardenTextField, etc.?\n- Could custom UI be replaced with existing components?\n- New reusable components placed in `:ui` module?\n\n✅ **Accessibility**\n- `contentDescription` for icons and images\n- `semantics` for custom interactions\n- Sufficient contrast ratios\n- Touch targets ≥ 48dp minimum\n\n✅ **Design Consistency**\n- Using `BitwardenTheme` colors (not hardcoded)\n- Using `BitwardenTheme` spacing (16.dp, 8.dp, etc.)\n- Using `BitwardenTheme` typography styles\n- Consistent with existing screen patterns\n\n✅ **Responsive Design**\n- Handles different screen sizes?\n- Scrollable content where appropriate?\n- Landscape orientation considered?\n\n## What to SKIP\n\n❌ **Deep Architecture Review** - Unless ViewModel changes are substantial\n❌ **Business Logic Review** - Focus is on presentation, not logic\n❌ **Security Review** - Unless UI exposes sensitive data improperly\n\n## Red Flags\n\n🚩 **Duplicating existing components** - Should reuse from `:ui` module\n🚩 **Hardcoded colors/dimensions** - Should use theme\n🚩 **Missing accessibility properties** - Critical for screen readers\n🚩 **State management in UI** - Should be hoisted to ViewModel\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"Can we use BitwardenButton here instead of this custom button?\"\n- \"Should this color come from BitwardenTheme instead of being hardcoded?\"\n- \"How will this look on a small screen?\"\n- \"Is there a contentDescription for this icon?\"\n\n## Common Patterns\n\n### Composable Structure\n\n```kotlin\n// ✅ GOOD - Stateless, hoisted state\n@Composable\nfun FeatureScreen(\n    state: FeatureState,\n    onActionClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    // UI rendering only\n}\n\n// ❌ BAD - Business state in composable\n@Composable\nfun FeatureScreen() {\n    var userData by remember { mutableStateOf<User?>(null) }  // Business state should be in ViewModel\n    var isLoading by remember { mutableStateOf(false) }  // App state should be in ViewModel\n    // ...\n}\n\n// ✅ OK - UI-local state in composable\n@Composable\nfun LoginForm(onSubmit: (String, String) -> Unit) {\n    var username by remember { mutableStateOf(\"\") }  // UI-local input state is fine\n    var password by remember { mutableStateOf(\"\") }\n    // Hoist only as high as needed\n}\n```\n\n### Theme Usage\n\n```kotlin\n// ✅ GOOD - Using theme\nText(\n    text = \"Title\",\n    style = BitwardenTheme.typography.titleLarge,\n    color = BitwardenTheme.colorScheme.primary\n)\n\n// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)\nSpacer(modifier = Modifier.height(16.dp))\n\n// ❌ BAD - Hardcoded\nText(\n    text = \"Title\",\n    style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold),  // Should use theme\n    color = Color(0xFF0000FF)  // Should use theme color\n)\n\nSpacer(modifier = Modifier.height(17.dp))  // Non-standard spacing\n```\n\n### Accessibility\n\n```kotlin\n// ✅ GOOD - Interactive element with description\nIcon(\n    painter = painterResource(R.drawable.ic_password),\n    contentDescription = \"Password visibility toggle\",\n    modifier = Modifier.clickable { onToggle() }\n)\n\n// ✅ GOOD - Decorative icon with explicit null\nIcon(\n    painter = painterResource(R.drawable.ic_check),\n    contentDescription = null,  // Decorative icon next to descriptive text\n    tint = BitwardenTheme.colorScheme.success\n)\n\n// ❌ BAD - Interactive element missing description\nIcon(\n    painter = painterResource(R.drawable.ic_delete),\n    contentDescription = null,  // Interactive elements need descriptions\n    modifier = Modifier.clickable { onDelete() }\n)\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n## Summary\nUpdates login screen layout for improved visual hierarchy and touch targets\n\n## Critical Issues\nNone\n\n## Suggested Improvements\n\n**app/auth/LoginScreen.kt:67** - Can we use BitwardenTextField?\nThis custom text field looks very similar to `ui/components/BitwardenTextField.kt:89`.\nWould using the existing component maintain consistency?\n\n**app/auth/LoginScreen.kt:123** - Add contentDescription\n```kotlin\nIcon(\n    painter = painterResource(R.drawable.ic_visibility),\n    contentDescription = \"Show password\",  // Add for accessibility\n    modifier = Modifier.clickable { onToggleVisibility() }\n)\n```\n\n**app/auth/LoginScreen.kt:145** - Use design system spacing\n```kotlin\n// Current\nSpacer(modifier = Modifier.height(17.dp))\n\n// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)\nSpacer(modifier = Modifier.height(16.dp))\n```\n```\n",".claude/skills/reviewing-changes/examples/review-outputs.md":"# Review Output Examples\n\nWell-structured code reviews demonstrating appropriate depth, tone, and formatting for different change types.\n\n## Table of Contents\n\n**Format Reference:**\n- [Quick Format Reference](#quick-format-reference)\n  - [Inline Comment Format](#inline-comment-format-required)\n  - [Summary Comment Format](#summary-comment-format)\n\n**Examples:**\n- [Example 1: Clean PR (No Issues)](#example-1-clean-pr-no-issues)\n- [Example 2: Dependency Update with Breaking Changes](#example-2-dependency-update-with-breaking-changes)\n- [Example 3: Feature Addition with Critical Issues](#example-3-feature-addition-with-critical-issues)\n\n**Anti-Patterns:**\n- [❌ Anti-Patterns to Avoid](#-anti-patterns-to-avoid)\n  - [Problem: Verbose Summary with Multiple Sections](#problem-verbose-summary-with-multiple-sections)\n  - [Problem: Praise-Only Inline Comments](#problem-praise-only-inline-comments)\n  - [Problem: Missing `<details>` Tags](#problem-missing-details-tags)\n\n**Summary:**\n- [Summary](#summary)\n\n---\n\n## Quick Format Reference\n\n### Inline Comment Format (REQUIRED)\n\n**MUST use `<details>` tags.** Only severity + description visible; all other content collapsed.\n\n```\n[emoji] **[SEVERITY]**: [One-line issue description]\n\n<details>\n<summary>Details and fix</summary>\n\n[Code example or specific fix]\n\n[Rationale explaining why]\n\nReference: [docs link if applicable]\n</details>\n```\n\n**Severity Levels:**\n- ❌ **CRITICAL** - Blocking, must fix (security, crashes, architecture violations)\n- ⚠️ **IMPORTANT** - Should fix (missing tests, quality issues)\n- ♻️ **DEBT** - Technical debt (duplication, convention violations, future rework needed)\n- 🎨 **SUGGESTED** - Nice to have (refactoring, improvements)\n- ❓ **QUESTION** - Seeking clarification (requirements, design decisions)\n\n### Summary Comment Format\n\nUses the agent's `posting-review-summary` skill format. Surface ❌ CRITICAL issues at the top level for immediate visibility, wrap the full findings list in `<details>` for scannability.\n\n```\n**Overall Assessment:** APPROVE / REQUEST CHANGES\n\n[1-2 neutral sentences describing what was reviewed]\n\n**Critical Issues** (if any):\n- ❌ [One-line summary with file:line]\n\n<details>\n<summary>All findings</summary>\n\n- ❌ **CRITICAL**: [description] (`file:line`)\n- ⚠️ **IMPORTANT**: [description] (`file:line`)\n- ♻️ **DEBT**: [description] (`file:line`)\n- 🎨 **SUGGESTED**: [description] (`file:line`)\n- ❓ **QUESTION**: [description] (`file:line`)\n</details>\n```\n\nFor clean PRs with no findings, omit both sections entirely — verdict + 1-2 sentences is sufficient.\n\n**GitHub pitfall**: Never use `#` followed by a number in comment text (e.g., `#42`, `#PR123`). GitHub autolinks these to issues/PRs. Use `Finding 1:` or `item 42` instead.\n\n---\n\n## Example 1: Clean PR (No Issues)\n\n**Context**: Moving shared code to common module, complete migration, all patterns followed\n\n**Review Comment:**\n```markdown\n**Overall Assessment:** APPROVE\n\nClean refactoring that moves ExitManager to :ui module, eliminating duplication between apps.\n```\n\n**Why this works:**\n- Immediate approval visible (2-3 lines)\n- One sentence acknowledging the work\n- No unnecessary sections or elaborate praise\n- Author gets quick feedback and can proceed\n\n---\n\n## Example 2: Dependency Update with Breaking Changes\n\n**Context**: Major version update requiring code migration\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- API migration required for Retrofit 3.0 breaking changes (network/api/BitwardenApiService.kt:34)\n\nSee inline comments for migration details.\n```\n\n**Inline Comment 1** (on `network/api/BitwardenApiService.kt:34`):\n```markdown\n❌ **CRITICAL**: API migration required for Retrofit 3.0\n\n<details>\n<summary>Details and fix</summary>\n\nRetrofit 3.0 removes the `Call<T>` return type. All 12 API methods in this file need migration:\n\n```kotlin\n// Current (deprecated in Retrofit 3.0)\n@GET(\"api/accounts/profile\")\nfun getProfile(): Call<ProfileResponse>\n\n// Must migrate to\n@GET(\"api/accounts/profile\")\nsuspend fun getProfile(): Response<ProfileResponse>\n```\n\nBreaking API change affects:\n- 12 methods in BitwardenApiService\n- 8 methods in VaultApiService\n- All call sites using enqueue/execute\n- Test utilities\n\nConsider creating separate PR for this migration given the scope.\n\nReference: [Retrofit 3.0 migration guide](https://square.github.io/retrofit/changelogs/changelog-3.x/)\n</details>\n```\n\n**Key Features:**\n- Minimal summary (2-3 lines)\n- Full details in collapsed inline comment\n- Specific file:line references\n- Code examples in <details>\n- Migration guidance and scope assessment\n\n---\n\n## Example 3: Feature Addition with Critical Issues\n\n**Context**: Implements PIN unlock for vault access\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Exposes mutable state violating MVVM (UnlockViewModel.kt:78)\n- PIN stored without encryption - SECURITY ISSUE (UnlockRepository.kt:145)\n\nSee inline comments for all issues and suggestions.\n```\n\n**Inline Comment 1** (on `app/vault/unlock/UnlockViewModel.kt:78`):\n```markdown\n❌ **CRITICAL**: Exposes mutable state\n\n<details>\n<summary>Details and fix</summary>\n\nChange `MutableStateFlow<State>` to `StateFlow<State>`:\n\n```kotlin\n// Current (problematic)\nval unlockState: MutableStateFlow<UnlockState>\n\n// Should be\nprivate val _unlockState = MutableStateFlow<UnlockState>()\nval unlockState: StateFlow<UnlockState> = _unlockState.asStateFlow()\n```\n\nExposing MutableStateFlow allows external mutation, violating MVVM unidirectional data flow.\n\nReference: docs/ARCHITECTURE.md#mvvm-pattern\n</details>\n```\n\n**Inline Comment 2** (on `data/vault/UnlockRepository.kt:145`):\n```markdown\n❌ **CRITICAL**: PIN stored without encryption - SECURITY ISSUE\n\n<details>\n<summary>Details and fix</summary>\n\nStoring PIN in plaintext SharedPreferences exposes it to backup systems and rooted devices.\n\n```kotlin\n// Current (CRITICAL SECURITY ISSUE)\nsharedPreferences.edit {\n    putString(KEY_PIN, pin)\n}\n\n// Must use Android Keystore encryption\nsuspend fun storePin(pin: String): Result<Unit> = runCatching {\n    val encrypted = keystoreManager.encrypt(pin.toByteArray())\n    encryptedPrefs.putBytes(KEY_PIN, encrypted)\n}\n```\n\nUse Android Keystore encryption or EncryptedSharedPreferences per security architecture.\n\nReference: docs/ARCHITECTURE.md#security\n</details>\n```\n\n**Inline Comment 3** (on `app/vault/unlock/UnlockViewModel.kt:92`):\n```markdown\n⚠️ **IMPORTANT**: Missing error handling test\n\n<details>\n<summary>Details and fix</summary>\n\nAdd test to prevent regression if error handling changes:\n\n```kotlin\n@Test\nfun `when incorrect PIN entered then returns error state`() = runTest {\n    val viewModel = UnlockViewModel(mockRepository)\n    coEvery { mockRepository.validatePin(\"1234\") }\n        returns Result.failure(InvalidPinException())\n\n    viewModel.onPinEntered(\"1234\")\n\n    assertEquals(UnlockState.Error(\"Invalid PIN\"), viewModel.state.value)\n}\n```\n\nEnsures error flow remains robust across refactorings.\n</details>\n```\n\n**Inline Comment 4** (on `app/vault/unlock/UnlockViewModel.kt:105`):\n```markdown\n🎨 **SUGGESTED**: Consider rate limiting for PIN attempts\n\n<details>\n<summary>Details and fix</summary>\n\nCurrently allows unlimited attempts, which could enable brute force attacks.\n\n```kotlin\nprivate var attemptCount = 0\nprivate var lockoutUntil: Instant? = null\n\nfun onPinEntered(pin: String) {\n    if (isLockedOut()) {\n        _state.value = UnlockState.LockedOut(lockoutUntil!!)\n        return\n    }\n    // ... validate PIN ...\n    if (invalid) {\n        attemptCount++\n        if (attemptCount >= MAX_ATTEMPTS) {\n            lockoutUntil = clock.millis() + 15.minutes\n        }\n    }\n}\n```\n\nWould add security layer against brute force. Consider discussing threat model with security team.\n</details>\n```\n\n**Inline Comment 5** (on `app/vault/unlock/UnlockScreen.kt:134`):\n```markdown\n❓ **QUESTION**: Can we use BitwardenTextField?\n\n<details>\n<summary>Details</summary>\n\nThis custom PIN input field looks similar to `ui/components/BitwardenTextField.kt:67`.\n\nWould using the existing component maintain consistency and reduce custom UI code?\n</details>\n```\n\n**Key Features:**\n- Minimal summary (3-4 lines) with critical issues only\n- Each issue gets separate inline comment with `<details>` tag\n- Multiple severity levels demonstrated (CRITICAL, IMPORTANT, SUGGESTED, QUESTION)\n- Mix of prescriptive fixes and collaborative questions\n- Code examples collapsed in <details>\n- No \"Good Practices\" or \"Action Items\" sections\n\n---\n\n## ❌ Anti-Patterns to Avoid\n\n### Problem: Verbose Summary with Multiple Sections\n\n**What NOT to do:**\n```markdown\n### Review Complete ✅\n\n## Summary\n[Lengthy description of what the PR does]\n\n### Strengths 👍\n1. **Excellent documentation** - KDoc comments are comprehensive\n2. **Proper fail-closed design** - Security defaults to rejection\n3. **Defense in depth** - Multiple validation layers\n[7 total items with elaboration]\n\n### Critical Issues ⚠️\n- Missing test coverage for security-critical code (with full details)\n- [More issues with full explanations]\n\n### Recommendations 🎨\n- [Multiple recommendations]\n\n### Test Coverage Status 📊\n- [Analysis]\n\n### Architecture Compliance ✅\n- [Analysis]\n\n## Recommendation\n**Conditional approval** with follow-up...\n```\n\n**Why this is wrong:**\n- 800+ tokens for a summary comment\n- Multiple sections (Strengths, Recommendations, Test Coverage, Architecture)\n- Elaborates on positive aspects (\"Excellent documentation...\")\n- Duplicates critical issues (summary has details + inline comments have same details)\n- Creates visual clutter in PR conversation\n\n**Correct approach:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Missing test coverage for security-critical code (PasswordManagerSignatureVerifierImpl.kt:47)\n\nSee inline comments for details.\n```\n\n**Key differences:**\n- 3-5 lines vs 800+ tokens\n- Verdict + critical issues only\n- All details belong in inline comments\n- No positive commentary sections\n- Scales with PR complexity, not analysis thoroughness\n\n### Problem: Praise-Only Inline Comments\n\n**What NOT to do:**\n\nCreating inline comment on `AuthenticatorBridgeManagerImpl.kt:73`:\n```markdown\n👍 **Excellent integration of signature verification**\n\nThe signature verification is properly integrated into the connection flow:\n- Checked during initialization (line 73)\n- Checked before binding (line 134)\n- Ensures only validated apps can connect\n\nThis is exactly the right approach for fail-safe security.\n```\n\n**Why this is wrong:**\n- Entire comment is positive feedback with no actionable issue\n- Takes up space in PR conversation\n- Distracts from actual issues\n- Violates \"focus on actionable feedback\" principle\n\n**Correct approach:**\n- Do not create this comment at all\n- Reserve inline comments exclusively for issues requiring attention\n\n### Problem: Missing `<details>` Tags\n\n**What NOT to do:**\n\n```markdown\n❌ **CRITICAL**: Missing test coverage for security-critical code\n\nThe `@OmitFromCoverage` annotation excludes this entire class from test coverage.\n\n**Problems:**\n1. No validation that certificate hashes match actual Bitwarden certificates\n2. No verification of fail-closed behavior on edge cases\n3. No tests for multiple signer rejection logic\n4. Certificate hash typos would go undetected until production\n\n**Recommendation:**\nReplace `@OmitFromCoverage` with proper unit tests.\n\nExample test structure:\n[long code block]\n\nSecurity-critical code should have the highest test coverage, not be omitted.\n```\n\n**Why this is wrong:**\n- All content visible immediately (code examples, problems list, rationale)\n- Creates visual clutter in PR conversation\n- Makes it hard to scan multiple issues quickly\n\n**Correct approach:**\n```markdown\n❌ **CRITICAL**: Missing test coverage for security-critical code\n\n<details>\n<summary>Details and fix</summary>\n\nThe `@OmitFromCoverage` annotation excludes this entire class from test coverage.\n\n**Problems:**\n1. No validation that certificate hashes match actual Bitwarden certificates\n2. No verification of fail-closed behavior on edge cases\n3. No tests for multiple signer rejection logic\n4. Certificate hash typos would go undetected until production\n\n**Recommendation:**\nReplace `@OmitFromCoverage` with proper unit tests.\n\nExample test structure:\n[code block]\n\nSecurity-critical code should have the highest test coverage, not be omitted.\n</details>\n```\n\n**Key difference:** Only severity + one-line description visible. All details collapsed.\n\n---\n\n## Summary\n\n**Always use:**\n- Minimal summary (verdict + critical issues)\n- Separate inline comments with `<details>` tags\n- Hybrid emoji + text severity prefixes\n- Focus exclusively on actionable feedback\n\n**Never use:**\n- Multiple summary sections (Strengths, Recommendations, etc.)\n- Praise-only inline comments\n- Duplication between summary and inline comments\n- Verbose analysis in summary (belongs in inline comments)\n",".claude/skills/reviewing-changes/reference/architectural-patterns.md":"# Architectural Patterns Quick Reference\n\nQuick reference for Bitwarden Android architectural patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## Table of Contents\n\n**Core Patterns:**\n- [MVVM + UDF Pattern](#mvvm--udf-pattern)\n  - [ViewModel Structure](#viewmodel-structure)\n  - [UI Layer (Compose)](#ui-layer-compose)\n- [Hilt Dependency Injection](#hilt-dependency-injection)\n  - [ViewModels](#viewmodels)\n  - [Repositories and Managers](#repositories-and-managers)\n  - [Clock/Time Handling](#clocktime-handling)\n- [Module Organization](#module-organization)\n- [Error Handling](#error-handling)\n  - [Use Result Types, Not Exceptions](#use-result-types-not-exceptions)\n- [Quick Checklist](#quick-checklist)\n\n---\n\n## MVVM + UDF Pattern\n\n### ViewModel Structure\n\n**✅ GOOD - Proper state encapsulation**:\n```kotlin\n@HiltViewModel\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository\n) : ViewModel() {\n    // Private mutable state\n    private val _state = MutableStateFlow<FeatureState>(FeatureState.Initial)\n\n    // Public immutable state\n    val state: StateFlow<FeatureState> = _state.asStateFlow()\n\n    // Actions as functions, state updated via internal action\n    fun onActionClicked() {\n        viewModelScope.launch {\n            val result = repository.performAction()\n            sendAction(FeatureAction.Internal.ActionComplete(result))\n        }\n    }\n\n    // The ViewModel has a handler that processes the internal action\n    private fun handleInternalAction(action: FeatureAction.Internal) {\n        when (action) {\n            is FeatureAction.Internal.ActionComplete -> {\n                // The action handler evaluates the result and updates state\n                action.result.fold(\n                    onSuccess = { _state.value = State.Success },\n                    onFailure = { _state.value = State.Error(it) }\n                )\n            }\n        }\n    }\n}\n```\n\n**❌ BAD - Common violations**:\n```kotlin\nclass FeatureViewModel : ViewModel() {\n    // ❌ Exposes mutable state\n    val state: MutableStateFlow<FeatureState>\n\n    // ❌ Business logic in ViewModel\n    fun onSubmit() {\n        val encrypted = encryptionManager.encrypt(data)  // Should be in Repository\n        _state.value = FeatureState.Success\n    }\n\n    // ❌ Direct Android framework dependency\n    fun onCreate(context: Context) {  // ViewModels shouldn't depend on Context\n        // ...\n    }\n}\n```\n\n**Key Rules**:\n- Expose `StateFlow<T>`, never `MutableStateFlow<T>`\n- Delegate business logic to Repository/Manager\n- No direct Android framework dependencies (except ViewModel, SavedStateHandle)\n- Use `viewModelScope` for coroutines\n\nReference: `docs/ARCHITECTURE.md#mvvm-pattern`\n\n---\n\n### UI Layer (Compose)\n\n**✅ GOOD - Stateless, observes only**:\n```kotlin\n@Composable\nfun FeatureScreen(\n    state: FeatureState,\n    onActionClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    Column(modifier = modifier) {\n        when (state) {\n            is FeatureState.Loading -> LoadingIndicator()\n            is FeatureState.Success -> SuccessContent(state.data)\n            is FeatureState.Error -> ErrorMessage(state.error)\n        }\n\n        BitwardenButton(\n            text = \"Action\",\n            onClick = onActionClick  // Sends event to ViewModel\n        )\n    }\n}\n```\n\n**❌ BAD - Stateful, modifies state**:\n```kotlin\n@Composable\nfun FeatureScreen(viewModel: FeatureViewModel) {\n    var localState by remember { mutableStateOf(...) }  // ❌ State in UI\n\n    Button(onClick = {\n        viewModel._state.value = FeatureState.Loading  // ❌ Directly modifying ViewModel state\n    })\n}\n```\n\n**Key Rules**:\n- Compose screens observe state, never modify\n- User actions passed as events/callbacks to ViewModel\n- No business logic in UI layer\n- Use existing components from `:ui` module\n\n---\n\n## Hilt Dependency Injection\n\n### ViewModels\n\n**✅ GOOD - Interface injection**:\n```kotlin\n@HiltViewModel\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository,  // Interface, not implementation\n    private val authManager: AuthManager,\n    savedStateHandle: SavedStateHandle\n) : ViewModel()\n```\n\n**❌ BAD - Common violations**:\n```kotlin\n// ❌ No @HiltViewModel annotation\nclass FeatureViewModel @Inject constructor(...)\n\n// ❌ Injecting implementation instead of interface\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepositoryImpl  // Should inject interface\n)\n\n// ❌ Manual instantiation\nclass FeatureViewModel : ViewModel() {\n    private val repository = FeatureRepositoryImpl()  // Should use @Inject\n}\n```\n\n**Key Rules**:\n- Annotate with `@HiltViewModel`\n- Use `@Inject constructor`\n- Inject interfaces, not implementations\n- Use `SavedStateHandle` for process death survival\n\nReference: `docs/ARCHITECTURE.md#dependency-injection`\n\n---\n\n### Repositories and Managers\n\n**✅ GOOD - Implementation with @Inject**:\n```kotlin\ninterface FeatureRepository {\n    suspend fun fetchData(): Result<Data>\n}\n\nclass FeatureRepositoryImpl @Inject constructor(\n    private val apiService: FeatureApiService,\n    private val database: FeatureDao\n) : FeatureRepository {\n    override suspend fun fetchData(): Result<Data> = runCatching {\n        apiService.getData()\n    }\n}\n```\n\n**Module provides interface**:\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nabstract class DataModule {\n    @Binds\n    @Singleton\n    abstract fun bindFeatureRepository(\n        impl: FeatureRepositoryImpl\n    ): FeatureRepository\n}\n```\n\n**Key Rules**:\n- Define interface for abstraction\n- Implementation uses `@Inject constructor`\n- Module binds implementation to interface\n- Appropriate scoping (`@Singleton`, `@ViewModelScoped`)\n\n---\n\n### Clock/Time Handling\n\nTime-dependent code must use injected `Clock` rather than direct `Instant.now()` or `DateTime.now()` calls. This follows the same DI principle as other dependencies.\n\n**✅ GOOD - Injected Clock**:\n```kotlin\n// ViewModel with Clock injection\nclass MyViewModel @Inject constructor(\n    private val clock: Clock,\n) {\n    fun save() {\n        val timestamp = clock.instant()\n    }\n}\n\n// Extension function with Clock parameter\nfun State.getTimestamp(clock: Clock): Instant =\n    existingTime ?: clock.instant()\n```\n\n**❌ BAD - Static/direct calls**:\n```kotlin\n// Hidden dependency, non-testable\nval timestamp = Instant.now()\nval dateTime = DateTime.now()\n```\n\n**Key Rules**:\n- Inject `Clock` via Hilt constructor (like other dependencies)\n- Pass `Clock` as parameter to extension functions\n- `Clock` is provided via `CoreModule` as singleton\n- Enables deterministic testing with `Clock.fixed(...)`\n\nReference: `docs/STYLE_AND_BEST_PRACTICES.md#best-practices--time-and-clock-handling`\n\n---\n\n## Module Organization\n\n```\nandroid/\n├── core/           # Shared utilities (cryptography, analytics, logging)\n├── data/           # Repositories, database, domain models\n├── network/        # API clients, network utilities\n├── ui/             # Reusable Compose components, theme\n├── app/            # Application, feature screens, ViewModels\n└── authenticator/  # Authenticator app (separate from password manager)\n```\n\n**Correct Placement**:\n- UI screens and ViewModels → `:app`\n- Reusable Compose components → `:ui`\n- Data models and Repositories → `:data`\n- API services → `:network`\n- Cryptography, logging → `:core`\n\n**Check for**:\n- No circular dependencies\n- Correct module placement\n- Proper visibility (internal vs public)\n\nReference: `docs/ARCHITECTURE.md#module-structure`\n\n---\n\n## Error Handling\n\n### Use Result Types, Not Exceptions\n\n**✅ GOOD - Result-based**:\n```kotlin\n// Repository\nsuspend fun fetchData(): Result<Data> = runCatching {\n    apiService.getData()\n}\n\n// ViewModel\nfun onFetch() {\n    viewModelScope.launch {\n        val result = repository.fetchData()\n        sendAction(FeatureAction.Internal.FetchComplete(result))\n    }\n}\n```\n\n**❌ BAD - Exception-based in business logic**:\n```kotlin\n// ❌ Don't throw in business logic\nsuspend fun fetchData(): Data {\n    try {\n        return apiService.getData()\n    } catch (e: Exception) {\n        throw FeatureException(e)  // Don't throw in repositories\n    }\n}\n\n// ❌ Try-catch in ViewModel\nfun onFetch() {\n    viewModelScope.launch {\n        try {\n            val data = repository.fetchData()\n            sendAction(FeatureAction.Internal.FetchComplete(data))\n        } catch (e: Exception) {\n            sendAction(FeatureAction.Internal.FetchComplete(e))\n        }\n    }\n}\n```\n\n**Key Rules**:\n- Use `Result<T>` return types in repositories\n- Use `runCatching { }` to wrap API calls\n- Handle results with `.fold()` in ViewModels\n- Don't throw exceptions in business logic\n\nReference: `docs/ARCHITECTURE.md#error-handling`\n\n---\n\n## Quick Checklist\n\n### Architecture\n- [ ] ViewModels expose StateFlow, not MutableStateFlow?\n- [ ] Business logic in Repository, not ViewModel?\n- [ ] Using Hilt DI (@HiltViewModel, @Inject constructor)?\n- [ ] Injecting interfaces, not implementations?\n- [ ] Time-dependent code uses injected `Clock` (not `Instant.now()`)?\n- [ ] Correct module placement?\n\n### Error Handling\n- [ ] Using Result types, not exceptions in business logic?\n- [ ] Errors handled with .fold() in ViewModels?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n",".claude/skills/reviewing-changes/reference/priority-framework.md":"# Finding Priority Framework\n\nUse this framework to classify findings during code review. Clear prioritization helps authors triage and address issues effectively.\n\n## Table of Contents\n\n**Severity Categories:**\n- [❌ CRITICAL (Blocker - Must Fix Before Merge)](#critical-blocker---must-fix-before-merge)\n- [⚠️ IMPORTANT (Should Fix)](#important-should-fix)\n- [♻️ DEBT (Technical Debt)](#debt-technical-debt)\n- [🎨 SUGGESTED (Nice to Have)](#suggested-nice-to-have)\n- [❓ QUESTION (Seeking Clarification)](#question-seeking-clarification)\n- [Optional (Acknowledge But Don't Require)](#optional-acknowledge-but-dont-require)\n\n**Guidelines:**\n- [Classification Guidelines](#classification-guidelines)\n  - [When Something is Between Categories](#when-something-is-between-categories)\n  - [Context Matters](#context-matters)\n- [Examples by Change Type](#examples-by-change-type)\n- [Special Cases](#special-cases)\n- [Summary](#summary)\n\n---\n\n## ❌ **CRITICAL** (Blocker - Must Fix Before Merge)\n\nThese issues **must** be addressed before the PR can be merged. They pose immediate risks to security, stability, or architecture integrity.\n\n### Security\n- Data leaks or plaintext sensitive data (passwords, keys, tokens)\n- Weak encryption or insecure key storage\n- Missing authentication or authorization checks\n- Input injection vulnerabilities (SQL, XSS, command injection)\n- Sensitive data in logs or error messages\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption\nPIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.\nReference: docs/ARCHITECTURE.md#security\n```\n\n### Stability\n- Compilation errors or warnings\n- Null pointer exceptions in production paths\n- Resource leaks (file handles, network connections, memory)\n- Crashes or unhandled exceptions in critical paths\n- Thread safety violations\n\n**Example**:\n```\n**app/auth/BiometricRepository.kt:120** - CRITICAL: Missing null safety check\nbiometricPrompt result can be null. Add explicit null check to prevent crash.\n```\n\n### Architecture\n- Mutable state exposure in ViewModels (violates MVVM)\n- Exception-based error handling in business logic (should use Result)\n- Circular dependencies between modules\n- Violation of zero-knowledge principles\n- Direct dependency instantiation (should use DI)\n\n**Example**:\n```\n**app/login/LoginViewModel.kt:45** - CRITICAL: Exposes mutable state\nChange MutableStateFlow to StateFlow in public API to prevent external state mutation.\nThis violates MVVM encapsulation pattern.\n```\n\n---\n\n## ⚠️ **IMPORTANT** (Should Fix)\n\nThese issues should be addressed but don't block merge if there's a compelling reason. They improve code quality, maintainability, or robustness.\n\n### Testing\n- Missing tests for critical paths (authentication, encryption, data sync)\n- Missing tests for new public APIs\n- Tests that don't verify actual behavior (test implementation, not behavior)\n- Missing test coverage for error scenarios\n\n**Example**:\n```\n**data/auth/BiometricRepository.kt** - IMPORTANT: Missing test for cancellation\nAdd test for user cancellation scenario to prevent regression.\n```\n\n### Architecture\n- Inconsistent patterns within PR (mixing error handling approaches)\n- Poor separation of concerns\n- Tight coupling between components\n- Not following established project patterns\n\n**Example**:\n```\n**app/vault/VaultViewModel.kt:89** - IMPORTANT: Business logic in ViewModel\nEncryption logic should be in Repository, not ViewModel.\nReference: docs/ARCHITECTURE.md#mvvm-pattern\n```\n\n### Documentation\n- Undocumented public APIs (missing KDoc)\n- Missing documentation for complex algorithms\n- Unclear naming or confusing interfaces\n\n**Example**:\n```\n**core/crypto/EncryptionManager.kt:34** - IMPORTANT: Missing KDoc\nPublic encryption method should document parameters, return value, and exceptions.\n```\n\n### Performance\n- Inefficient algorithms in hot paths (with evidence from profiling)\n- Blocking main thread with I/O operations\n- Memory inefficient data structures (with evidence)\n\n**Example**:\n```\n**app/vault/VaultListViewModel.kt:78** - IMPORTANT: N+1 query pattern\nFetching items one-by-one in loop. Consider batch fetch to reduce database queries.\n```\n\n---\n\n## ♻️ **DEBT** (Technical Debt)\n\nCode that duplicates existing patterns, violates established conventions, or will require rework within 6 months. Introduces technical debt that should be tracked for future cleanup.\n\n### Duplication\n- Copy-pasted code blocks across files\n- Repeated validation or business logic\n- Multiple implementations of same pattern\n- Data transformation duplicated in multiple places\n\n**Example**:\n```\n**app/vault/VaultListViewModel.kt:156** - DEBT: Duplicates encryption logic\nSame encryption pattern exists in VaultRepository.kt:234 and SyncManager.kt:89.\nExtract to shared EncryptionUtil to reduce maintenance burden.\n```\n\n### Convention Violations\n- Inconsistent error handling approaches within same module\n- Mixing architectural patterns (MVVM + MVC)\n- Not following established DI patterns\n- Deviating from project code style significantly\n\n**Example**:\n```\n**data/auth/AuthRepository.kt:78** - DEBT: Exception-based error handling\nProject standard is Result<T> for error handling. This uses try-catch with throws.\nCreates inconsistency and makes testing harder.\nReference: docs/ARCHITECTURE.md#error-handling\n```\n\n### Future Rework Required\n- Hardcoded values that should be configurable\n- Temporary workarounds without TODO/FIXME\n- Code that will need changes when planned features arrive\n- Tight coupling that prevents future extensibility\n\n**Example**:\n```\n**app/settings/SettingsViewModel.kt:45** - DEBT: Hardcoded feature flags\nFeature flags should come from remote config for A/B testing.\nWill require rework when experimentation framework launches.\n```\n\n---\n\n## 🎨 **SUGGESTED** (Nice to Have)\n\nImprovements with measurable value only. A finding qualifies as SUGGESTED if it provides: security gain, cyclomatic complexity reduction, bug class prevention, or elimination of an O(n²) pattern. Subjective style preferences, vague simplifications, and naming nitpicks do not qualify — leave those out entirely or raise in conversation.\n\n### Code Quality\n- Extractable duplicated logic that reduces measurable complexity or improves testability\n- Patterns that would prevent a recurring bug class in this module\n- Architecture improvements that eliminate tight coupling with measurable impact\n\n**Example**:\n```\n**app/vault/VaultScreen.kt:145** - SUGGESTED: Consider extracting helper function\nThis 20-line block appears in 3 places. Consider extracting to reduce duplication.\n```\n\n### Testing\n- Additional test coverage for edge cases (beyond critical paths)\n- More comprehensive integration tests\n- Performance tests for non-critical paths\n\n**Example**:\n```\n**app/login/LoginViewModelTest.kt** - SUGGESTED: Add test for concurrent login attempts\nNot critical, but would increase confidence in edge case handling.\n```\n\n### Refactoring\n- Extracting reusable patterns\n- Modernizing old patterns (if touching related code)\n- Improving testability\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:200** - SUGGESTED: Consider extracting validation logic\nCould be extracted to separate validator class for reusability and testing.\n```\n\n---\n\n## ❓ **QUESTION** (Seeking Clarification)\n\nQuestions about requirements, unclear intent, or potential conflicts that require human knowledge to answer. Open inquiries that cannot be resolved through code inspection alone.\n\n### Requirements Clarification\n- Ambiguous acceptance criteria\n- Multiple valid implementation approaches\n- Unclear business rules or edge case handling\n- Conflicting requirements between specs and implementation\n\n**Example**:\n```\n**app/vault/ItemListViewModel.kt:67** - QUESTION: Expected sort behavior for equal timestamps?\nWhen items have identical timestamps, should secondary sort be by:\n- Name (alphabetical)\n- Creation order\n- Item type priority\n\nSpec doesn't specify tie-breaking logic.\n```\n\n### Design Decisions\n- Architecture choices that could go multiple ways\n- Trade-offs between approaches without clear winner\n- Feature flag strategy or rollout approach\n- API design with multiple valid patterns\n\n**Example**:\n```\n**data/sync/SyncManager.kt:134** - QUESTION: Should sync failures retry automatically?\nCurrent implementation fails immediately. Options:\n- Exponential backoff (3 retries)\n- User-triggered retry only\n- Background retry on network restore\n\nWhat's the expected UX?\n```\n\n### System Integration\n- Unclear contracts with external systems\n- Potential conflicts with other features/modules\n- Assumptions about third-party API behavior\n- Cross-team coordination needs\n\n**Example**:\n```\n**app/auth/BiometricPrompt.kt:89** - QUESTION: Compatibility with pending device credentials PR?\nPR #1234 is refactoring device credentials. Should this:\n- Merge first and adapt later\n- Wait for #1234 to land\n- Coordinate with that author\n\nTiming unclear.\n```\n\n### Testing Strategy\n- Uncertainty about test scope or approach\n- Questions about mocking external dependencies\n- Edge cases that need product input\n- Performance testing requirements\n\n**Example**:\n```\n**data/vault/EncryptionTest.kt:45** - QUESTION: Should we test against real Keystore?\nCurrently using mocked Keystore. Real Keystore testing would:\n+ Catch hardware-specific issues\n- Slow down CI significantly\n- Require API 23+ emulators\n\nWhat's the priority?\n```\n\n---\n\n## Optional (Acknowledge But Don't Require)\n\nNote good practices to reinforce positive patterns. Keep these **brief** - list only, no elaboration.\n\n### Good Practices\n\n**Format**: Simple bullet list, no explanation\n\n```markdown\n## Good Practices\n- Proper Hilt DI usage throughout\n- Comprehensive unit test coverage\n- Clear separation of concerns\n- Well-documented public APIs\n```\n\n**Don't do this** (too verbose):\n```markdown\n## Good Practices\n- Proper Hilt DI usage throughout: Great job using @Inject constructor and injecting interfaces! This follows our established patterns perfectly and makes the code very testable. Really excellent work here! 👍\n```\n\n---\n\n## Classification Guidelines\n\n### When Something is Between Categories\n\n**If unsure between Critical and Important**:\n- Ask: \"Could this cause production incidents, data loss, or security breaches?\"\n- If yes → Critical\n- If no → Important\n\n**If unsure between Important and Debt**:\n- Ask: \"Is this a bug/defect or just duplication/inconsistency?\"\n- If bug/defect → Important\n- If duplication/inconsistency → Debt\n\n**If unsure between Important and Suggested**:\n- Ask: \"Would I block merge over this?\"\n- If yes → Important\n- If no → Suggested\n\n**If unsure between Debt and Suggested**:\n- Ask: \"Will this require rework within 6 months?\"\n- If yes → Debt\n- If no → Suggested\n\n**If unsure between Suggested and Question**:\n- Ask: \"Am I requesting a change or asking for clarification?\"\n- If requesting change → Suggested\n- If seeking clarification → Question\n\n**If unsure between Suggested and Optional**:\n- Ask: \"Is this actionable feedback or just acknowledgment?\"\n- If actionable → Suggested\n- If acknowledgment → Optional\n\n### Context Matters\n\n**Same issue, different contexts**:\n\n```\n// Critical for production code\nMissing null safety check in auth flow → CRITICAL\n\n// Suggested for internal test utility\nMissing null safety check in test helper → SUGGESTED\n```\n\n**Same pattern, different risk levels**:\n\n```\n// Critical for new feature\nMissing tests for new auth method → CRITICAL\n\n// Important for bug fix\nMissing regression test → IMPORTANT\n\n// Suggested for refactoring\nMissing tests for refactored helper → SUGGESTED\n```\n\n---\n\n## Examples by Change Type\n\n### Dependency Update\n- **Critical**: Known CVEs in old version not addressed\n- **Important**: Breaking changes that need migration\n- **Suggested**: Beta/alpha version stability concerns\n\n### Bug Fix\n- **Critical**: Fix doesn't address root cause\n- **Important**: Missing regression test\n- **Suggested**: Similar bugs in related code\n\n### Feature Addition\n- **Critical**: Security vulnerabilities, architecture violations\n- **Important**: Missing tests for critical paths\n- **Suggested**: Additional test coverage, minor refactoring\n\n### UI Refinement\n- **Critical**: Missing accessibility for key actions\n- **Important**: Not using theme (hardcoded colors)\n- **Suggested**: Minor spacing/alignment improvements\n\n### Refactoring\n- **Critical**: Changes behavior (should be behavior-preserving)\n- **Important**: Incomplete migration (mix of old/new patterns)\n- **Suggested**: Additional instances that could be refactored\n\n### Infrastructure\n- **Critical**: Hardcoded secrets, no rollback plan\n- **Important**: Performance regression in build times\n- **Suggested**: Further optimization opportunities\n\n---\n\n## Special Cases\n\n### Technical Debt\n- Acknowledge existing tech debt but don't require fixing in unrelated PR\n- Exception: If change makes tech debt worse, it's Important to address\n\n### Scope Creep\n- Don't request changes outside PR scope\n- Can note as \"Future consideration\" but not required for this PR\n\n### Linter-Catchable Issues\n- Don't flag issues that automated tools handle\n- Exception: If linter is misconfigured and missing real issues\n\n### Personal Preferences\n- Don't flag unless grounded in project standards or architectural principles\n- Use \"I-statements\" if suggesting alternative approaches\n\n---\n\n## Summary\n\n**Critical**: Block merge, must fix (security, stability, architecture)\n**Important**: Should fix before merge (testing, quality, performance)\n**Debt**: Technical debt introduced, track for future cleanup\n**Suggested**: Nice to have, consider effort vs benefit\n**Question**: Seeking clarification on requirements or design\n**Optional**: Acknowledge good practices, keep brief\n",".claude/skills/reviewing-changes/reference/review-psychology.md":"# Review Psychology: Constructive Feedback Phrasing\n\nEffective code review feedback is clear, actionable, and constructive. This guide provides phrasing patterns for inline comments.\n\n## Table of Contents\n\n**Guidelines:**\n- [Phrasing Templates](#phrasing-templates)\n  - [Critical Issues (Prescriptive)](#critical-issues-prescriptive)\n  - [Suggested Improvements (Exploratory)](#suggested-improvements-exploratory)\n  - [Questions (Collaborative)](#questions-collaborative)\n  - [Test Suggestions](#test-suggestions)\n- [When to Be Prescriptive vs Ask Questions](#when-to-be-prescriptive-vs-ask-questions)\n- [Special Cases](#special-cases)\n\n---\n\n## Phrasing Templates\n\n### Critical Issues (Prescriptive)\n\n**Pattern**: State problem + Provide solution + Explain why\n\n```\n**[file:line]** - CRITICAL: [Issue description]\n\n[Specific fix with code example if applicable]\n\n[Rationale explaining why this is critical]\n\nReference: [docs link if applicable]\n```\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption\n\nPIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.\nPlaintext storage exposes the PIN to backup systems and rooted devices.\n\nReference: docs/ARCHITECTURE.md#security\n```\n\n---\n\n### Suggested Improvements (Exploratory)\n\n**Pattern**: Observe + Suggest + Explain benefit\n\n```\n**[file:line]** - Consider [alternative approach]\n\n[Current observation]\nCan we [specific suggestion]?\n\n[Benefit or rationale]\n```\n\n**Example**:\n```\n**app/login/LoginScreen.kt:89** - Consider using existing BitwardenButton\n\nThis custom button implementation looks similar to `ui/components/BitwardenButton.kt:45`.\nCan we use the existing component to maintain consistency across the app?\n```\n\n---\n\n### Questions (Collaborative)\n\n**Pattern**: Ask + Provide context (optional)\n\n```\n**[file:line]** - [Question about intent or approach]?\n\n[Optional context or observation]\n```\n\n**Example**:\n```\n**data/sync/SyncManager.kt:234** - How does this handle concurrent sync attempts?\n\nIt looks like multiple coroutines could call `startSync()` simultaneously.\nIs there a mechanism to prevent race conditions, or is that handled elsewhere?\n```\n\n---\n\n### Test Suggestions\n\n**Pattern**: Observe gap + Suggest specific test + Provide skeleton\n\n```\n**[file:line]** - Consider adding test for [scenario]\n\n[Rationale]\n\n```kotlin\n@Test\nfun `test description`() = runTest {\n    // Test skeleton\n}\n```\n```\n\n**Example**:\n```\n**data/auth/BiometricRepository.kt** - Consider adding test for cancellation scenario\n\nThis would prevent regression of the bug you just fixed:\n\n```kotlin\n@Test\nfun `when biometric cancelled then returns cancelled state`() = runTest {\n    coEvery { biometricPrompt.authenticate() } returns null\n\n    val result = repository.authenticate()\n\n    assertEquals(AuthResult.Cancelled, result)\n}\n```\n```\n\n---\n\n## When to Be Prescriptive vs Ask Questions\n\n**Be Prescriptive** (Tell them what to do):\n- Security issues\n- Architecture pattern violations\n- Null safety problems\n- Compilation errors\n- Documented project standards\n\n**Ask Questions** (Seek explanation):\n- Design decisions with multiple valid approaches\n- Performance trade-offs without data\n- Unclear intent or reasoning\n- Scope decisions (this PR vs future work)\n- Patterns not documented in project guidelines\n\n---\n\n## Special Cases\n\n**Nitpicks** - For truly minor suggestions, use \"Nit:\" prefix:\n```\n**Nit**: Extra blank line at line 145\n```\n\n**Uncertainty** - If unsure, acknowledge it:\n```\nI'm not certain, but this might be called frequently.\nHas this been profiled?\n```\n\n**Positive Feedback** - Brief list only, no elaboration:\n```\n## Good Practices\n- Proper Hilt DI usage throughout\n- Comprehensive unit test coverage\n- Clear separation of concerns\n```\n",".claude/skills/reviewing-changes/reference/security-patterns.md":"# Security Patterns Quick Reference\n\nQuick reference for Bitwarden Android security patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md#security`.\n\n## Encryption and Key Storage\n\n**✅ GOOD - Android Keystore**:\n```kotlin\n// Sensitive data encrypted with Keystore\nclass SecureStorage @Inject constructor(\n    private val keystoreManager: KeystoreManager\n) {\n    suspend fun storePin(pin: String): Result<Unit> = runCatching {\n        val encrypted = keystoreManager.encrypt(pin.toByteArray())\n        securePreferences.putBytes(KEY_PIN, encrypted)\n    }\n}\n\n// Or use EncryptedSharedPreferences\nval encryptedPrefs = EncryptedSharedPreferences.create(\n    context,\n    \"secure_prefs\",\n    masterKey,\n    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n)\n```\n\n**❌ BAD - Plaintext or weak encryption**:\n```kotlin\n// ❌ CRITICAL - Plaintext storage\nsharedPreferences.edit {\n    putString(\"pin\", userPin)  // Never store sensitive data in plaintext\n}\n\n// ❌ CRITICAL - Weak encryption\nval cipher = Cipher.getInstance(\"DES\")  // Use AES-256-GCM\n\n// ❌ CRITICAL - Hardcoded keys\nval key = \"my_secret_key_123\"  // Use Android Keystore\n```\n\n**Key Rules**:\n- Use Android Keystore for encryption keys\n- Use EncryptedSharedPreferences for simple key-value storage\n- Use AES-256-GCM for encryption\n- Never store sensitive data in plaintext\n- Never hardcode encryption keys\n\nReference: `docs/ARCHITECTURE.md#security`\n\n---\n\n## Logging Sensitive Data\n\n**✅ GOOD - No sensitive data**:\n```kotlin\nLog.d(TAG, \"Authentication attempt for user\")\nLog.d(TAG, \"Vault sync completed with ${items.size} items\")\n```\n\n**❌ BAD - Logs sensitive data**:\n```kotlin\n// ❌ CRITICAL\nLog.d(TAG, \"Password: $password\")\nLog.d(TAG, \"Auth token: $token\")\nLog.d(TAG, \"PIN: $pin\")\nLog.d(TAG, \"Encryption key: ${key.encoded}\")\n```\n\n**Key Rules**:\n- Never log passwords, PINs, tokens, keys\n- Never log encryption keys or sensitive data\n- Be careful with error messages (don't include sensitive context)\n\n---\n\n## Quick Checklist\n\n### Security\n- [ ] Sensitive data encrypted with Keystore?\n- [ ] No plaintext passwords/keys?\n- [ ] No sensitive data in logs?\n- [ ] Using AES-256-GCM for encryption?\n- [ ] No hardcoded encryption keys?\n\n---\n\nFor comprehensive security details, always refer to:\n- `docs/ARCHITECTURE.md#security` - Complete security architecture and zero-knowledge principles\n",".claude/skills/reviewing-changes/reference/style-patterns.md":"# Style Patterns Quick Reference\n\nProject-specific Kotlin style rules to catch during code review. These supplement (not replace) `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## `when` branches with wrapped right-hand side require curly braces\n\nWhen a `when` branch's expression is too long to fit on the same line as `->` and is wrapped to its own line, the body must be wrapped in `{ }`. A bare `->` followed by an indented expression on the next line should be flagged.\n\n**Flag this:**\n\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT ->\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n}\n```\n\n**Accept this:**\n\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT -> {\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    }\n}\n```\n\nSingle-line branches (body fits alongside `->`) do **not** require braces.\n\n**Suggested classification:** SUGGESTED (style consistency, not correctness).\n",".claude/skills/reviewing-changes/reference/testing-patterns.md":"# Testing Patterns Quick Reference\n\nQuick reference for Bitwarden Android testing patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## ViewModel Tests\n\n**✅ GOOD - Tests behavior**:\n```kotlin\n@Test\nfun `when login succeeds then state updates to success`() = runTest {\n    // Arrange\n    val viewModel = LoginViewModel(mockRepository)\n    coEvery { mockRepository.login(any(), any()) } returns Result.success(User())\n\n    // Act\n    viewModel.onLoginClicked(\"user@example.com\", \"password\")\n\n    // Assert\n    viewModel.state.test {\n        assertEquals(LoginState.Loading, awaitItem())\n        assertEquals(LoginState.Success, awaitItem())\n    }\n}\n```\n\n**❌ BAD - Tests implementation**:\n```kotlin\n@Test\nfun `repository is called with correct parameters`() {\n    // ❌ This tests implementation details, not behavior\n    viewModel.onLoginClicked(\"user\", \"pass\")\n    coVerify { mockRepository.login(\"user\", \"pass\") }\n}\n```\n\n**Key Rules**:\n- Test behavior, not implementation\n- Use `runTest` for coroutine tests\n- Use Turbine for Flow testing\n- Use MockK for mocking\n\n---\n\n## Repository Tests\n\n**✅ GOOD - Tests data transformations**:\n```kotlin\n@Test\nfun `fetchItems maps API response to domain model`() = runTest {\n    // Arrange\n    val apiResponse = listOf(ApiItem(id = \"1\", name = \"Test\"))\n    coEvery { apiService.getItems() } returns apiResponse\n\n    // Act\n    val result = repository.fetchItems()\n\n    // Assert\n    assertTrue(result.isSuccess)\n    assertEquals(\n        listOf(DomainItem(id = \"1\", name = \"Test\")),\n        result.getOrThrow()\n    )\n}\n```\n\n**Key Rules**:\n- Test data transformations\n- Test error handling (network failures, API errors)\n- Test caching behavior if applicable\n- Mock API services and databases\n\nReference: Project uses JUnit 5, MockK, Turbine, kotlinx-coroutines-test\n\n---\n\n## Null Safety\n\n**✅ GOOD - Safe handling**:\n```kotlin\n// Safe call with elvis operator\nval result = apiService.getData() ?: return State.Error(\"No data\")\n\n// Let with safe call\nintent?.getStringExtra(\"key\")?.let { value ->\n    processValue(value)\n}\n\n// Require with message\nval data = requireNotNull(response.data) { \"Response data must not be null\" }\n```\n\n**❌ BAD - Unsafe assertions**:\n```kotlin\n// ❌ Unsafe - can crash\nval result = apiService.getData()!!\n\n// ❌ Platform type unchecked\nval intent: Intent = getIntent()  // Could be null from Java\nval value = intent.getStringExtra(\"key\")  // Potential NPE\n```\n\n**Key Rules**:\n- Avoid `!!` unless safety is guaranteed (rare)\n- Handle platform types with explicit nullability\n- Use safe calls (`?.`), elvis operator (`?:`), or explicit checks\n- Use `requireNotNull` with descriptive message if crash is acceptable\n\n---\n\n## Quick Checklist\n\n### Testing\n- [ ] ViewModels have unit tests?\n- [ ] Tests verify behavior, not implementation?\n- [ ] Edge cases covered?\n- [ ] Error scenarios tested?\n\n### Code Quality\n- [ ] Null safety handled properly (no `!!` without guarantee)?\n- [ ] Public APIs have KDoc?\n- [ ] Following naming conventions?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n",".claude/skills/reviewing-changes/reference/ui-patterns.md":"# Compose UI Patterns Quick Reference\n\nQuick reference for Bitwarden Android Compose UI patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## Component Reuse\n\n**✅ GOOD - Uses existing components**:\n```kotlin\nBitwardenButton(\n    text = \"Submit\",\n    onClick = onSubmit\n)\n\nBitwardenTextField(\n    value = text,\n    onValueChange = onTextChange,\n    label = \"Email\"\n)\n```\n\n**❌ BAD - Duplicates existing components**:\n```kotlin\n// ❌ Recreating BitwardenButton\nButton(\n    onClick = onSubmit,\n    colors = ButtonDefaults.buttonColors(\n        containerColor = BitwardenTheme.colorScheme.primary\n    )\n) {\n    Text(\"Submit\")\n}\n```\n\n**Key Rules**:\n- Check `:ui` module for existing components before creating custom ones\n- Use BitwardenButton, BitwardenTextField, etc. for consistency\n- Place new reusable components in `:ui` module\n\n---\n\n## Theme Usage\n\n**✅ GOOD - Uses theme**:\n```kotlin\nText(\n    text = \"Title\",\n    style = BitwardenTheme.typography.titleLarge,\n    color = BitwardenTheme.colorScheme.primary\n)\n\nSpacer(modifier = Modifier.height(16.dp))  // Standard spacing\n```\n\n**❌ BAD - Hardcoded values**:\n```kotlin\nText(\n    text = \"Title\",\n    style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold),  // Use theme\n    color = Color(0xFF0066FF)  // Use theme color\n)\n\nSpacer(modifier = Modifier.height(17.dp))  // Non-standard spacing\n```\n\n**Key Rules**:\n- Use `BitwardenTheme.colorScheme` for colors\n- Use `BitwardenTheme.typography` for text styles\n- Use standard spacing (4.dp, 8.dp, 16.dp, 24.dp)\n\n---\n\n## Quick Checklist\n\n### UI Patterns\n- [ ] Using existing Bitwarden components from `:ui` module?\n- [ ] Using BitwardenTheme for colors and typography?\n- [ ] Using standard spacing values (4, 8, 16, 24 dp)?\n- [ ] No hardcoded colors or text styles?\n- [ ] UI is stateless (observes state, doesn't modify)?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n",".claude/skills/testing-android-code/SKILL.md":"---\nname: testing-android-code\ndescription: This skill should be used when writing or reviewing tests for Android code in Bitwarden. Triggered by \"BaseViewModelTest\", \"BitwardenComposeTest\", \"BaseServiceTest\", \"stateEventFlow\", \"bufferedMutableSharedFlow\", \"FakeDispatcherManager\", \"expectNoEvents\", \"assertCoroutineThrows\", \"createMockCipher\", \"createMockSend\", \"asSuccess\", \"Why is my Bitwarden test failing?\", or testing questions about ViewModels, repositories, Compose screens, or data sources in Bitwarden.\nversion: 1.0.0\n---\n\n# Testing Android Code - Bitwarden Testing Patterns\n\n**This skill provides tactical testing guidance for Bitwarden-specific patterns.** For comprehensive architecture and testing philosophy, consult `docs/ARCHITECTURE.md`.\n\n## Test Framework Configuration\n\n**Required Dependencies:**\n- **JUnit 5** (jupiter), **MockK**, **Turbine** (app.cash.turbine)\n- **kotlinx.coroutines.test**, **Robolectric**, **Compose Test**\n\n**Critical Note:** Tests run with en-US locale for consistency. Don't assume other locales.\n\n---\n\n## A. ViewModel Testing Patterns\n\n### Base Class: BaseViewModelTest\n\n**Always extend `BaseViewModelTest` for ViewModel tests.**\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`\n\n**Benefits:**\n- Automatically registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`\n- Provides `stateEventFlow()` helper for simultaneous StateFlow/EventFlow testing\n\n**Pattern:**\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n    private val mockRepository: ExampleRepository = mockk()\n    private val savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to INITIAL_STATE))\n\n    @Test\n    fun `ButtonClick should fetch data and update state`() = runTest {\n        coEvery { mockRepository.fetchData(any()) } returns Result.success(\"data\")\n\n        val viewModel = ExampleViewModel(savedStateHandle, mockRepository)\n\n        viewModel.stateFlow.test {\n            assertEquals(INITIAL_STATE, awaitItem())\n            viewModel.trySendAction(ExampleAction.ButtonClick)\n            assertEquals(INITIAL_STATE.copy(data = \"data\"), awaitItem())\n        }\n\n        coVerify { mockRepository.fetchData(any()) }\n    }\n}\n```\n\n**For complete examples:** See `references/test-base-classes.md`\n\n### StateFlow vs EventFlow (Critical Distinction)\n\n| Flow Type | Replay | First Action | Pattern |\n|-----------|--------|--------------|---------|\n| StateFlow | Yes (1) | `awaitItem()` gets current state | Expect initial → trigger → expect new |\n| EventFlow | No | `expectNoEvents()` first | expectNoEvents → trigger → expect event |\n\n**For detailed patterns:** See `references/flow-testing-patterns.md`\n\n---\n\n## B. Compose UI Testing Patterns\n\n### Base Class: BitwardenComposeTest\n\n**Always extend `BitwardenComposeTest` for Compose screen tests.**\n\n**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`\n\n**Benefits:**\n- Pre-configures all Bitwarden managers (FeatureFlags, AuthTab, Biometrics, etc.)\n- Wraps content in `BitwardenTheme` and `LocalManagerProvider`\n- Provides fixed Clock for deterministic time-based tests\n\n**Pattern:**\n```kotlin\nclass ExampleScreenTest : BitwardenComposeTest() {\n    private var haveCalledNavigateBack = false\n    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        setContent {\n            ExampleScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                viewModel = viewModel,\n            )\n        }\n    }\n\n    @Test\n    fun `on back click should send BackClick action`() {\n        composeTestRule.onNodeWithContentDescription(\"Back\").performClick()\n        verify { viewModel.trySendAction(ExampleAction.BackClick) }\n    }\n}\n```\n\n**Note:** Use `bufferedMutableSharedFlow` for event testing in Compose tests. Default replay is 0; pass `replay = 1` if needed.\n\n**For complete base class details:** See `references/test-base-classes.md`\n\n---\n\n## C. Repository and Service Testing\n\n### Service Testing with MockWebServer\n\n**Base Class:** `BaseServiceTest` (`network/src/testFixtures/`)\n\n```kotlin\nclass ExampleServiceTest : BaseServiceTest() {\n    private val api: ExampleApi = retrofit.create()\n    private val service = ExampleServiceImpl(api)\n\n    @Test\n    fun `getConfig should return success when API succeeds`() = runTest {\n        server.enqueue(MockResponse().setBody(EXPECTED_JSON))\n        val result = service.getConfig()\n        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)\n    }\n}\n```\n\n### Repository Testing Pattern\n\n```kotlin\nclass ExampleRepositoryTest {\n    private val fixedClock: Clock = Clock.fixed(\n        Instant.parse(\"2023-10-27T12:00:00Z\"),\n        ZoneOffset.UTC,\n    )\n    private val dispatcherManager = FakeDispatcherManager()\n    private val mockDiskSource: ExampleDiskSource = mockk()\n    private val mockService: ExampleService = mockk()\n\n    private val repository = ExampleRepositoryImpl(\n        clock = fixedClock,\n        exampleDiskSource = mockDiskSource,\n        exampleService = mockService,\n        dispatcherManager = dispatcherManager,\n    )\n\n    @Test\n    fun `fetchData should return success when service succeeds`() = runTest {\n        coEvery { mockService.getData(any()) } returns expectedData.asSuccess()\n        val result = repository.fetchData(userId)\n        assertTrue(result.isSuccess)\n    }\n}\n```\n\n**Key patterns:** Use `FakeDispatcherManager`, fixed Clock, and `.asSuccess()` helpers.\n\n---\n\n## D. Test Data Builders\n\n### Builder Pattern with Number Parameter\n\n**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/model/`\n\n```kotlin\nfun createMockCipher(\n    number: Int,\n    id: String = \"mockId-$number\",\n    name: String? = \"mockName-$number\",\n    // ... more parameters with defaults\n): SyncResponseJson.Cipher\n\n// Usage:\nval cipher1 = createMockCipher(number = 1)  // mockId-1, mockName-1\nval cipher2 = createMockCipher(number = 2)  // mockId-2, mockName-2\nval custom = createMockCipher(number = 3, name = \"Custom\")\n```\n\n**Available Builders (35+):**\n- **Cipher:** `createMockCipher()`, `createMockLogin()`, `createMockCard()`, `createMockIdentity()`, `createMockSecureNote()`, `createMockSshKey()`, `createMockField()`, `createMockUri()`, `createMockFido2Credential()`, `createMockPasswordHistory()`, `createMockCipherPermissions()`\n- **Sync:** `createMockSyncResponse()`, `createMockFolder()`, `createMockCollection()`, `createMockPolicy()`, `createMockDomains()`\n- **Send:** `createMockSend()`, `createMockFile()`, `createMockText()`, `createMockSendJsonRequest()`\n- **Profile:** `createMockProfile()`, `createMockOrganization()`, `createMockProvider()`, `createMockPermissions()`\n- **Attachments:** `createMockAttachment()`, `createMockAttachmentJsonRequest()`, `createMockAttachmentResponse()`\n\nSee `network/src/testFixtures/kotlin/com/bitwarden/network/model/` for full list.\n\n---\n\n## E. Result Type Testing\n\n**Locations:**\n- `.asSuccess()`, `.asFailure()`: `core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.kt`\n- `assertCoroutineThrows`: `core/src/testFixtures/kotlin/com/bitwarden/core/data/util/TestHelpers.kt`\n\n```kotlin\n// Create results\n\"data\".asSuccess()              // Result.success(\"data\")\nthrowable.asFailure()           // Result.failure<T>(throwable)\n\n// Assertions\nassertTrue(result.isSuccess)\nassertEquals(expectedValue, result.getOrNull())\n```\n\n---\n\n## F. Test Utilities and Helpers\n\n### Fake Implementations\n\n| Fake | Location | Purpose |\n|------|----------|---------|\n| `FakeDispatcherManager` | `core/src/testFixtures/` | Deterministic coroutine execution |\n| `FakeConfigDiskSource` | `data/src/testFixtures/` | In-memory config storage |\n| `FakeSharedPreferences` | `data/src/testFixtures/` | Memory-backed SharedPreferences |\n\n### Exception Testing (CRITICAL)\n\n```kotlin\n// CORRECT - Call directly, NOT inside runTest\n@Test\nfun `test exception`() {\n    assertCoroutineThrows<IllegalStateException> {\n        repository.throwingFunction()\n    }\n}\n```\n\n**Why:** `runTest` catches exceptions and rethrows them, breaking the assertion pattern.\n\n---\n\n## G. Critical Gotchas\n\nCommon testing mistakes in Bitwarden. **For complete details and examples:** See `references/critical-gotchas.md`\n\n**Core Patterns:**\n- **assertCoroutineThrows + runTest** - Never wrap in `runTest`; call directly\n- **Static mock cleanup** - Always `unmockkStatic()` in `@After`\n- **StateFlow vs EventFlow** - StateFlow: `awaitItem()` first; EventFlow: `expectNoEvents()` first\n- **FakeDispatcherManager** - Always use instead of real `DispatcherManagerImpl`\n- **Coroutine test wrapper** - Use `runTest { }` for all Flow/coroutine tests\n\n**Assertion Patterns:**\n- **Complete state assertions** - Assert entire state objects, not individual fields\n- **JUnit over Kotlin** - Use `assertTrue()`, not Kotlin's `assert()`\n- **Use Result extensions** - Use `asSuccess()` and `asFailure()` for Result type assertions\n\n**Test Design:**\n- **Fake vs Mock strategy** - Use Fakes for happy paths, Mocks for error paths\n- **DI over static mocking** - Extract interfaces (like UuidManager) instead of mockkStatic\n- **Null stream testing** - Test null returns from ContentResolver operations\n- **bufferedMutableSharedFlow** - Use with `.onSubscription { emit(state) }` in Fakes\n- **Test factory methods** - Accept domain state types, not SavedStateHandle\n\n---\n\n## H. Test File Organization\n\n### Directory Structure\n\n```\nmodule/src/test/kotlin/com/bitwarden/.../\n├── ui/*ScreenTest.kt, *ViewModelTest.kt\n├── data/repository/*RepositoryTest.kt\n└── network/service/*ServiceTest.kt\n\nmodule/src/testFixtures/kotlin/com/bitwarden/.../\n├── util/TestHelpers.kt\n├── base/Base*Test.kt\n└── model/*Util.kt\n```\n\n### Test Constants Placement\n\nDeclare test constants as top-level `private const val` at the **bottom** of the file, after the class closing brace. Do NOT use `companion object` for test constants.\n\n### Test Naming\n\n- Classes: `*Test.kt`, `*ScreenTest.kt`, `*ViewModelTest.kt`\n- Functions: `` `given state when action should result` ``\n\n---\n\n## Summary\n\nKey Bitwarden-specific testing patterns:\n\n1. **BaseViewModelTest** - Automatic dispatcher setup with `stateEventFlow()` helper\n2. **BitwardenComposeTest** - Pre-configured with all managers and theme\n3. **BaseServiceTest** - MockWebServer setup for network testing\n4. **Turbine Flow Testing** - StateFlow (replay) vs EventFlow (no replay)\n5. **Test Data Builders** - Consistent `number: Int` parameter pattern\n6. **Fake Implementations** - FakeDispatcherManager, FakeConfigDiskSource\n7. **Result Type Testing** - `.asSuccess()`, `.asFailure()`\n\n**Always consult:** `docs/ARCHITECTURE.md` and existing test files for reference implementations.\n\n---\n\n## Reference Documentation\n\nFor detailed information, see:\n\n- `references/test-base-classes.md` - Detailed base class documentation and usage patterns\n- `references/flow-testing-patterns.md` - Complete Turbine patterns for StateFlow/EventFlow\n- `references/critical-gotchas.md` - Full anti-pattern reference and debugging tips\n\n**Complete Examples:**\n- `examples/viewmodel-test-example.md` - Full ViewModel test with StateFlow/EventFlow\n- `examples/compose-screen-test-example.md` - Full Compose screen test\n- `examples/repository-test-example.md` - Full repository test with mocks and fakes\n",".claude/skills/testing-android-code/examples/compose-screen-test-example.md":"/**\n * Complete Compose Screen Test Example\n *\n * Key patterns demonstrated:\n * - Extending BitwardenComposeTest\n * - Mocking ViewModel with flows\n * - Testing UI interactions\n * - Testing navigation callbacks\n * - Using bufferedMutableSharedFlow for events\n * - Testing dialogs with isDialog() and hasAnyAncestor()\n */\npackage com.bitwarden.example.feature\n\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.filterToOne\nimport androidx.compose.ui.test.hasAnyAncestor\nimport androidx.compose.ui.test.isDialog\nimport androidx.compose.ui.test.onAllNodesWithText\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow\nimport com.bitwarden.ui.util.assertNoDialogExists\nimport com.bitwarden.ui.util.isProgressBar\nimport com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.verify\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport junit.framework.TestCase.assertTrue\nimport org.junit.Before\nimport org.junit.Test\n\nclass ExampleScreenTest : BitwardenComposeTest() {\n\n    // Track navigation callbacks\n    private var haveCalledNavigateBack = false\n    private var haveCalledNavigateToNext = false\n\n    // Use bufferedMutableSharedFlow for events (default replay = 0)\n    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n\n    // Mock ViewModel with relaxed = true\n    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        haveCalledNavigateBack = false\n        haveCalledNavigateToNext = false\n\n        setContent {\n            ExampleScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                onNavigateToNext = { haveCalledNavigateToNext = true },\n                viewModel = viewModel,\n            )\n        }\n    }\n\n    /**\n     * Test: Back button sends action to ViewModel\n     */\n    @Test\n    fun `on back click should send BackClick action`() {\n        composeTestRule\n            .onNodeWithContentDescription(\"Back\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.BackClick) }\n    }\n\n    /**\n     * Test: Submit button sends action to ViewModel\n     */\n    @Test\n    fun `on submit click should send SubmitClick action`() {\n        composeTestRule\n            .onNodeWithText(\"Submit\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.SubmitClick) }\n    }\n\n    /**\n     * Test: Loading state shows progress indicator\n     */\n    @Test\n    fun `loading state should display progress indicator`() {\n        mutableStateFlow.update { it.copy(isLoading = true) }\n\n        composeTestRule\n            .onNode(isProgressBar)\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Data state shows content\n     */\n    @Test\n    fun `data state should display content`() {\n        mutableStateFlow.update { it.copy(data = \"Test Data\") }\n\n        composeTestRule\n            .onNodeWithText(\"Test Data\")\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Error state shows error message\n     */\n    @Test\n    fun `error state should display error message`() {\n        mutableStateFlow.update { it.copy(errorMessage = \"Something went wrong\") }\n\n        composeTestRule\n            .onNodeWithText(\"Something went wrong\")\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: NavigateBack event triggers navigation callback\n     */\n    @Test\n    fun `NavigateBack event should call onNavigateBack`() {\n        mutableEventFlow.tryEmit(ExampleEvent.NavigateBack)\n\n        assertTrue(haveCalledNavigateBack)\n    }\n\n    /**\n     * Test: NavigateToNext event triggers navigation callback\n     */\n    @Test\n    fun `NavigateToNext event should call onNavigateToNext`() {\n        mutableEventFlow.tryEmit(ExampleEvent.NavigateToNext)\n\n        assertTrue(haveCalledNavigateToNext)\n    }\n\n    /**\n     * Test: Item in list can be clicked\n     */\n    @Test\n    fun `on item click should send ItemClick action`() {\n        val itemId = \"item-123\"\n        mutableStateFlow.update {\n            it.copy(items = listOf(ExampleItem(id = itemId, name = \"Test Item\")))\n        }\n\n        composeTestRule\n            .onNodeWithText(\"Test Item\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.ItemClick(itemId)) }\n    }\n\n    // ==================== DIALOG TESTS ====================\n\n    /**\n     * Test: No dialog exists when dialogState is null\n     */\n    @Test\n    fun `no dialog should exist when dialogState is null`() {\n        mutableStateFlow.update { it.copy(dialogState = null) }\n\n        composeTestRule.assertNoDialogExists()\n    }\n\n    /**\n     * Test: Loading dialog displays when state updates\n     * PATTERN: Use isDialog() to check dialog exists\n     */\n    @Test\n    fun `loading dialog should display when dialogState is Loading`() {\n        mutableStateFlow.update {\n            it.copy(dialogState = ExampleState.DialogState.Loading(\"Please wait...\"))\n        }\n\n        composeTestRule\n            .onNode(isDialog())\n            .assertIsDisplayed()\n\n        // Verify loading text within dialog using hasAnyAncestor(isDialog())\n        composeTestRule\n            .onAllNodesWithText(\"Please wait...\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Error dialog displays title and message\n     * PATTERN: Use filterToOne(hasAnyAncestor(isDialog())) to find text within dialogs\n     */\n    @Test\n    fun `error dialog should display title and message`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Error(\n                    title = \"An error has occurred\",\n                    message = \"Something went wrong. Please try again.\",\n                ),\n            )\n        }\n\n        // Verify dialog exists\n        composeTestRule\n            .onNode(isDialog())\n            .assertIsDisplayed()\n\n        // Verify title within dialog\n        composeTestRule\n            .onAllNodesWithText(\"An error has occurred\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n\n        // Verify message within dialog\n        composeTestRule\n            .onAllNodesWithText(\"Something went wrong. Please try again.\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Dialog button click sends action\n     * PATTERN: Find button with hasAnyAncestor(isDialog()) then performClick()\n     */\n    @Test\n    fun `error dialog dismiss button should send DismissDialog action`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Error(\n                    title = \"Error\",\n                    message = \"An error occurred\",\n                ),\n            )\n        }\n\n        // Click dismiss button within dialog\n        composeTestRule\n            .onAllNodesWithText(\"Ok\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.DismissDialog) }\n    }\n\n    /**\n     * Test: Confirmation dialog with multiple buttons\n     * PATTERN: Test both confirm and cancel actions\n     */\n    @Test\n    fun `confirmation dialog confirm button should send ConfirmAction`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Confirmation(\n                    title = \"Confirm Action\",\n                    message = \"Are you sure you want to proceed?\",\n                ),\n            )\n        }\n\n        // Click confirm button\n        composeTestRule\n            .onAllNodesWithText(\"Confirm\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.ConfirmAction) }\n    }\n\n    @Test\n    fun `confirmation dialog cancel button should send DismissDialog action`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Confirmation(\n                    title = \"Confirm Action\",\n                    message = \"Are you sure?\",\n                ),\n            )\n        }\n\n        // Click cancel button\n        composeTestRule\n            .onAllNodesWithText(\"Cancel\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.DismissDialog) }\n    }\n}\n\nprivate val DEFAULT_STATE = ExampleState(\n    isLoading = false,\n    data = null,\n    errorMessage = null,\n    items = emptyList(),\n    dialogState = null,\n)\n\n// Example types (normally in separate files)\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    val errorMessage: String? = null,\n    val items: List<ExampleItem> = emptyList(),\n    val dialogState: DialogState? = null,\n) {\n    /**\n     * PATTERN: Nested sealed class for dialog states.\n     * Common dialog types: Loading, Error, Confirmation\n     */\n    sealed class DialogState {\n        data class Loading(val message: String) : DialogState()\n        data class Error(val title: String, val message: String) : DialogState()\n        data class Confirmation(val title: String, val message: String) : DialogState()\n    }\n}\n\ndata class ExampleItem(val id: String, val name: String)\n\nsealed class ExampleAction {\n    data object BackClick : ExampleAction()\n    data object SubmitClick : ExampleAction()\n    data class ItemClick(val itemId: String) : ExampleAction()\n    data object DismissDialog : ExampleAction()\n    data object ConfirmAction : ExampleAction()\n}\n\nsealed class ExampleEvent {\n    data object NavigateBack : ExampleEvent()\n    data object NavigateToNext : ExampleEvent()\n}\n",".claude/skills/testing-android-code/examples/repository-test-example.md":"/**\n * Complete Repository Test Example\n *\n * Key patterns demonstrated:\n * - Fake for disk sources, Mock for network services\n * - Using FakeDispatcherManager for deterministic coroutines\n * - Using fixed Clock for deterministic time\n * - Testing Result types with .asSuccess() / .asFailure()\n * - Asserting actual objects (not isSuccess/isFailure) for better diagnostics\n * - Testing Flow emissions with Turbine\n */\npackage com.bitwarden.example.data.repository\n\nimport app.cash.turbine.test\nimport com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager\nimport com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow\nimport com.bitwarden.core.data.util.asFailure\nimport com.bitwarden.core.data.util.asSuccess\nimport io.mockk.coEvery\nimport io.mockk.coVerify\nimport io.mockk.mockk\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.onSubscription\nimport kotlinx.coroutines.test.runTest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNull\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport java.time.Clock\nimport java.time.Instant\nimport java.time.ZoneOffset\n\nclass ExampleRepositoryTest {\n\n    // Fixed clock for deterministic time-based tests\n    private val fixedClock: Clock = Clock.fixed(\n        Instant.parse(\"2023-10-27T12:00:00Z\"),\n        ZoneOffset.UTC,\n    )\n\n    // Use FakeDispatcherManager for deterministic coroutine execution\n    private val dispatcherManager = FakeDispatcherManager()\n\n    // Mock service (network layer is always mocked)\n    private val mockService: ExampleService = mockk()\n\n    /**\n     * PATTERN: Use Fake for disk source in happy path tests.\n     * This is the Bitwarden convention for repository testing.\n     */\n    private val fakeDiskSource = FakeExampleDiskSource()\n\n    private lateinit var repository: ExampleRepositoryImpl\n\n    @BeforeEach\n    fun setup() {\n        repository = ExampleRepositoryImpl(\n            clock = fixedClock,\n            service = mockService,\n            diskSource = fakeDiskSource,\n            dispatcherManager = dispatcherManager,\n        )\n    }\n\n    // ==================== HAPPY PATH TESTS (use Fake) ====================\n\n    /**\n     * Test: Successful fetch returns data and saves to disk\n     */\n    @Test\n    fun `fetchData should return success and save to disk when service succeeds`() = runTest {\n        val expectedData = ExampleData(id = \"1\", name = \"Test\", updatedAt = fixedClock.instant())\n        coEvery { mockService.getData() } returns expectedData.asSuccess()\n\n        val result = repository.fetchData()\n\n        assertEquals(expectedData, result.getOrThrow())\n        // Fake automatically stores the data - verify it's there\n        assertEquals(expectedData, fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Service failure returns failure without saving\n     */\n    @Test\n    fun `fetchData should return failure when service fails`() = runTest {\n        val exception = Exception(\"Network error\")\n        coEvery { mockService.getData() } returns exception.asFailure()\n\n        val result = repository.fetchData()\n\n        assertEquals(exception, result.exceptionOrNull())\n        // Fake was not updated\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Repository flow emits when disk source updates\n     */\n    @Test\n    fun `dataFlow should emit when disk source updates`() = runTest {\n        val data1 = ExampleData(id = \"1\", name = \"First\", updatedAt = fixedClock.instant())\n        val data2 = ExampleData(id = \"2\", name = \"Second\", updatedAt = fixedClock.instant())\n\n        repository.dataFlow.test {\n            // Initial null value from Fake\n            assertNull(awaitItem())\n\n            // Update via Fake property setter (triggers emission)\n            fakeDiskSource.storedData = data1\n            assertEquals(data1, awaitItem())\n\n            // Another update\n            fakeDiskSource.storedData = data2\n            assertEquals(data2, awaitItem())\n        }\n    }\n\n    /**\n     * Test: Refresh fetches and saves new data\n     */\n    @Test\n    fun `refresh should fetch new data and update disk source`() = runTest {\n        val newData = ExampleData(id = \"new\", name = \"Fresh\", updatedAt = fixedClock.instant())\n        coEvery { mockService.getData() } returns newData.asSuccess()\n\n        val result = repository.refresh()\n\n        assertEquals(Unit, result.getOrThrow())\n        coVerify { mockService.getData() }\n        assertEquals(newData, fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Delete clears data from disk\n     */\n    @Test\n    fun `deleteData should clear disk source`() = runTest {\n        // Pre-populate the fake\n        fakeDiskSource.storedData = ExampleData(id = \"1\", name = \"Test\", updatedAt = fixedClock.instant())\n\n        repository.deleteData()\n\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Cached data returns from disk when available\n     */\n    @Test\n    fun `getCachedData should return disk data without network call`() = runTest {\n        val cachedData = ExampleData(\n            id = \"cached\",\n            name = \"Cached\",\n            updatedAt = fixedClock.instant(),\n        )\n        fakeDiskSource.storedData = cachedData\n\n        val result = repository.getCachedData()\n\n        assertEquals(cachedData, result)\n        coVerify(exactly = 0) { mockService.getData() }\n    }\n\n    // ==================== ERROR PATH TESTS ====================\n\n    /**\n     * PATTERN: For error paths, reconfigure the class-level mock per-test.\n     * Use coEvery to change mock behavior for each specific test case.\n     */\n    @Test\n    fun `fetchData should return failure when service returns error`() = runTest {\n        val exception = Exception(\"Server unavailable\")\n        coEvery { mockService.getData() } returns exception.asFailure()\n\n        val result = repository.fetchData()\n\n        assertEquals(exception, result.exceptionOrNull())\n        // Fake state unchanged on failure\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    @Test\n    fun `refresh should return failure and preserve cached data when service fails`() = runTest {\n        // Pre-populate cache via Fake\n        val cachedData = ExampleData(id = \"cached\", name = \"Old\", updatedAt = fixedClock.instant())\n        fakeDiskSource.storedData = cachedData\n\n        // Reconfigure mock to return failure\n        coEvery { mockService.getData() } returns Exception(\"Network error\").asFailure()\n\n        val result = repository.refresh()\n\n        assertTrue(result.isFailure)\n        // Cached data preserved on failure\n        assertEquals(cachedData, fakeDiskSource.storedData)\n    }\n}\n\n// Example types (normally in separate files)\ndata class ExampleData(\n    val id: String,\n    val name: String,\n    val updatedAt: Instant,\n)\n\ninterface ExampleService {\n    suspend fun getData(): Result<ExampleData>\n}\n\ninterface ExampleDiskSource {\n    val dataFlow: kotlinx.coroutines.flow.Flow<ExampleData?>\n    fun getData(): ExampleData?\n    fun saveData(data: ExampleData)\n    fun clearData()\n}\n\n/**\n * PATTERN: Fake implementation for happy path testing.\n *\n * Key characteristics:\n * - Uses bufferedMutableSharedFlow(replay = 1) for proper replay behavior\n * - Uses .onSubscription { emit(state) } for immediate state emission\n * - Private storage with override property setter that emits to flow\n * - Test assertions done via the override property getter\n */\nclass FakeExampleDiskSource : ExampleDiskSource {\n    private var storedDataValue: ExampleData? = null\n    private val mutableDataFlow = bufferedMutableSharedFlow<ExampleData?>(replay = 1)\n\n    /**\n     * Override property with getter/setter. Setter emits to flow automatically.\n     * Tests can read this property for assertions and write to trigger emissions.\n     */\n    var storedData: ExampleData?\n        get() = storedDataValue\n        set(value) {\n            storedDataValue = value\n            mutableDataFlow.tryEmit(value)\n        }\n\n    override val dataFlow: Flow<ExampleData?>\n        get() = mutableDataFlow.onSubscription { emit(storedData) }\n\n    override fun getData(): ExampleData? = storedData\n\n    override fun saveData(data: ExampleData) {\n        storedData = data\n    }\n\n    override fun clearData() {\n        storedData = null\n    }\n}\n",".claude/skills/testing-android-code/examples/viewmodel-test-example.md":"/**\n * Complete ViewModel Test Example\n *\n * Key patterns demonstrated:\n * - Extending BaseViewModelTest\n * - Testing StateFlow with Turbine\n * - Testing EventFlow with Turbine\n * - Using stateEventFlow() for simultaneous testing\n * - MockK mocking patterns\n * - Test factory method design (accepts domain state, not SavedStateHandle)\n * - Complete state assertions (assert entire state objects)\n */\npackage com.bitwarden.example.feature\n\nimport androidx.lifecycle.SavedStateHandle\nimport app.cash.turbine.test\nimport com.bitwarden.ui.platform.base.BaseViewModelTest\nimport io.mockk.coEvery\nimport io.mockk.coVerify\nimport io.mockk.every\nimport io.mockk.mockk\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.test.runTest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Test\n\nclass ExampleViewModelTest : BaseViewModelTest() {\n\n    // Mock dependencies\n    private val mockRepository: ExampleRepository = mockk()\n    private val mockAuthDiskSource: AuthDiskSource = mockk {\n        every { userStateFlow } returns MutableStateFlow(null)\n    }\n\n    /**\n     * StateFlow has replay=1, so first awaitItem() returns current state\n     */\n    @Test\n    fun `initial state should be default state`() = runTest {\n        val viewModel = createViewModel()\n\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n        }\n    }\n\n    /**\n     * Test state transitions: initial -> loading -> success\n     */\n    @Test\n    fun `LoadData action should update state from idle to loading to success`() = runTest {\n        val expectedData = \"loaded data\"\n        coEvery { mockRepository.fetchData(any()) } returns Result.success(expectedData)\n\n        val viewModel = createViewModel()\n\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.LoadData)\n\n            assertEquals(DEFAULT_STATE.copy(isLoading = true), awaitItem())\n            assertEquals(DEFAULT_STATE.copy(isLoading = false, data = expectedData), awaitItem())\n        }\n\n        coVerify { mockRepository.fetchData(any()) }\n    }\n\n    /**\n     * EventFlow has no replay - MUST call expectNoEvents() first\n     */\n    @Test\n    fun `SubmitClick action should emit NavigateToNext event`() = runTest {\n        coEvery { mockRepository.submitData(any()) } returns Result.success(Unit)\n\n        val viewModel = createViewModel()\n\n        viewModel.eventFlow.test {\n            expectNoEvents() // CRITICAL for EventFlow\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n            assertEquals(ExampleEvent.NavigateToNext, awaitItem())\n        }\n    }\n\n    /**\n     * Use stateEventFlow() helper for simultaneous testing\n     */\n    @Test\n    fun `complex action should update state and emit event`() = runTest {\n        coEvery { mockRepository.complexOperation(any()) } returns Result.success(\"result\")\n\n        val viewModel = createViewModel()\n\n        viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n            assertEquals(DEFAULT_STATE, stateFlow.awaitItem())\n            eventFlow.expectNoEvents()\n\n            viewModel.trySendAction(ExampleAction.ComplexAction)\n\n            assertEquals(DEFAULT_STATE.copy(isLoading = true), stateFlow.awaitItem())\n            assertEquals(DEFAULT_STATE.copy(data = \"result\"), stateFlow.awaitItem())\n            assertEquals(ExampleEvent.ShowToast(\"Success!\"), eventFlow.awaitItem())\n        }\n    }\n\n    /**\n     * Test state restoration from saved state.\n     * Note: Use initialState parameter, NOT SavedStateHandle directly.\n     */\n    @Test\n    fun `initial state from saved state should be preserved`() = runTest {\n        // Build complete expected state - always assert full objects\n        val savedState = ExampleState(\n            isLoading = false,\n            data = \"restored data\",\n            errorMessage = null,\n        )\n\n        val viewModel = createViewModel(initialState = savedState)\n\n        viewModel.stateFlow.test {\n            assertEquals(savedState, awaitItem())\n        }\n    }\n\n    /**\n     * Factory method accepts domain state, NOT SavedStateHandle.\n     * This hides Android framework details from test logic.\n     */\n    private fun createViewModel(\n        initialState: ExampleState? = null,\n    ): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle().apply { set(\"state\", initialState) },\n        repository = mockRepository,\n        authDiskSource = mockAuthDiskSource,\n    )\n}\n\nprivate val DEFAULT_STATE = ExampleState(\n    isLoading = false,\n    data = null,\n    errorMessage = null,\n)\n\n// Example types (normally in separate files)\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    val errorMessage: String? = null,\n)\n\nsealed class ExampleAction {\n    data object LoadData : ExampleAction()\n    data object SubmitClick : ExampleAction()\n    data object ComplexAction : ExampleAction()\n}\n\nsealed class ExampleEvent {\n    data object NavigateToNext : ExampleEvent()\n    data class ShowToast(val message: String) : ExampleEvent()\n}\n",".claude/skills/testing-android-code/references/critical-gotchas.md":"# Critical Gotchas and Anti-Patterns\n\nCommon mistakes and pitfalls when writing tests in the Bitwarden Android codebase.\n\n## ❌ NEVER wrap assertCoroutineThrows in runTest\n\n### The Problem\n\n`runTest` catches exceptions and rethrows them, which breaks the `assertCoroutineThrows` assertion pattern.\n\n### Wrong\n\n```kotlin\n@Test\nfun `test exception`() = runTest {\n    assertCoroutineThrows<Exception> {\n        repository.throwingFunction()\n    }  // Won't work - exception is caught by runTest!\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `test exception`() {\n    assertCoroutineThrows<Exception> {\n        repository.throwingFunction()\n    }  // Works correctly\n}\n```\n\n### Why This Happens\n\n`runTest` provides a coroutine scope and catches exceptions to provide better error messages. However, `assertCoroutineThrows` needs to catch the exception itself to verify it was thrown. When wrapped in `runTest`, the exception is caught twice, breaking the assertion.\n\n## ❌ ALWAYS unmock static functions\n\n### The Problem\n\nMockK's static mocking persists across tests. Forgetting to clean up causes mysterious failures in subsequent tests.\n\n### Wrong\n\n```kotlin\n@Before\nfun setup() {\n    mockkStatic(::isBuildVersionAtLeast)\n    every { isBuildVersionAtLeast(any()) } returns true\n}\n\n// Forgot @After - subsequent tests will fail mysteriously!\n```\n\n### Correct\n\n```kotlin\n@Before\nfun setup() {\n    mockkStatic(::isBuildVersionAtLeast)\n    every { isBuildVersionAtLeast(any()) } returns true\n}\n\n@After\nfun tearDown() {\n    unmockkStatic(::isBuildVersionAtLeast)  // CRITICAL\n}\n```\n\n### Common Static Functions to Watch\n\n```kotlin\n// Platform version checks\nmockkStatic(::isBuildVersionAtLeast)\nunmockkStatic(::isBuildVersionAtLeast)\n\n// URI parsing\nmockkStatic(Uri::class)\nunmockkStatic(Uri::class)\n\n// Static utility functions\nmockkStatic(MyUtilClass::class)\nunmockkStatic(MyUtilClass::class)\n```\n\n### Debugging Tip\n\nIf tests pass individually but fail when run together, suspect static mocking cleanup issues.\n\n## ❌ Don't confuse StateFlow and EventFlow testing\n\n### StateFlow (replay = 1)\n\n```kotlin\n// CORRECT - StateFlow always has current value\nviewModel.stateFlow.test {\n    val initial = awaitItem()  // Gets current state immediately\n    viewModel.trySendAction(action)\n    val updated = awaitItem()  // Gets new state\n}\n```\n\n### EventFlow (no replay)\n\n```kotlin\n// CORRECT - EventFlow has no initial value\nviewModel.eventFlow.test {\n    expectNoEvents()  // MUST do this first\n    viewModel.trySendAction(action)\n    val event = awaitItem()  // Gets emitted event\n}\n```\n\n### Common Mistake\n\n```kotlin\n// WRONG - Forgetting expectNoEvents() on EventFlow\nviewModel.eventFlow.test {\n    viewModel.trySendAction(action)  // May cause flaky tests\n    assertEquals(event, awaitItem())\n}\n```\n\n## ❌ Don't mix real and test dispatchers\n\n### Wrong\n\n```kotlin\nprivate val repository = ExampleRepositoryImpl(\n    dispatcherManager = DispatcherManagerImpl(),  // Real dispatcher!\n)\n\n@Test\nfun `test repository`() = runTest {\n    // Test will have timing issues - real dispatcher != test dispatcher\n}\n```\n\n### Correct\n\n```kotlin\nprivate val repository = ExampleRepositoryImpl(\n    dispatcherManager = FakeDispatcherManager(),  // Test dispatcher\n)\n\n@Test\nfun `test repository`() = runTest {\n    // Test runs deterministically\n}\n```\n\n### Why This Matters\n\nReal dispatchers use actual thread pools and delays. Test dispatchers (UnconfinedTestDispatcher) execute immediately and deterministically. Mixing them causes:\n- Non-deterministic test failures\n- Real delays in tests (slow test suite)\n- Race conditions\n\n### Always Use\n\n- `FakeDispatcherManager()` for repositories\n- `UnconfinedTestDispatcher()` when manually creating dispatchers\n- `runTest` for coroutine tests (provides TestDispatcher automatically)\n\n## ❌ Don't forget to use runTest for coroutine tests\n\n### Wrong\n\n```kotlin\n@Test\nfun `test coroutine`() {\n    viewModel.stateFlow.test { /* ... */ }  // Missing runTest!\n}\n```\n\nThis causes:\n- Test completes before coroutines finish\n- False positives (test passes but assertions never run)\n- Mysterious failures\n\n### Correct\n\n```kotlin\n@Test\nfun `test coroutine`() = runTest {\n    viewModel.stateFlow.test { /* ... */ }\n}\n```\n\n### When runTest is Required\n\n- Testing ViewModels (they use `viewModelScope`)\n- Testing Flows with Turbine `.test {}`\n- Testing repositories with suspend functions\n- Any test calling suspend functions\n\n### Exception: assertCoroutineThrows\n\nAs noted above, `assertCoroutineThrows` should NOT be wrapped in `runTest`.\n\n## ❌ Don't forget relaxed = true for complex mocks\n\n### Without relaxed\n\n```kotlin\nprivate val viewModel = mockk<ExampleViewModel>()  // Must mock every method!\n\n// Error: \"no answer found for: stateFlow\"\n```\n\n### With relaxed\n\n```kotlin\nprivate val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n    // Only mock what you care about\n    every { stateFlow } returns mutableStateFlow\n    every { eventFlow } returns mutableEventFlow\n}\n```\n\n### When to Use relaxed\n\n- Mocking ViewModels in Compose tests\n- Mocking complex objects with many methods\n- When you only care about specific method calls\n\n### When NOT to Use relaxed\n\n- Mocking repository interfaces (be explicit about behavior)\n- When you want to verify NO unexpected calls\n- Testing error paths (want test to fail if unexpected method called)\n\n## ❌ Don't assert individual fields when complete state is available\n\n### The Problem\n\nAsserting individual state fields can miss unintended side effects on other fields.\n\n### Wrong\n\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    viewModel.trySendAction(SomeAction.DoThing)\n\n    val state = viewModel.stateFlow.value\n    assertEquals(null, state.dialog)  // Only checks one field!\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    viewModel.trySendAction(SomeAction.DoThing)\n\n    val expected = SomeState(\n        isLoading = false,\n        data = \"result\",\n        dialog = null,\n    )\n    assertEquals(expected, viewModel.stateFlow.value)  // Checks all fields\n}\n```\n\n### Why This Matters\n\n- Catches unintended mutations to other state fields\n- Makes expected state explicit and readable\n- Prevents silent regressions when state structure changes\n\n---\n\n## ❌ Don't use Kotlin assert() for boolean checks\n\n### The Problem\n\nKotlin's `assert()` doesn't follow JUnit conventions and provides poor failure messages.\n\n### Wrong\n\n```kotlin\n@Test\nfun `event should trigger callback`() {\n    mutableEventFlow.tryEmit(SomeEvent.Navigate)\n\n    assert(onNavigateCalled)  // Kotlin assert - bad failure messages\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `event should trigger callback`() {\n    mutableEventFlow.tryEmit(SomeEvent.Navigate)\n\n    assertTrue(onNavigateCalled)  // JUnit assertTrue - proper assertion\n}\n```\n\n### Always Use JUnit Assertions\n\n- `assertTrue()` / `assertFalse()` for booleans\n- `assertEquals()` for value comparisons\n- `assertNotNull()` / `assertNull()` for nullability\n- `assertThrows<T>()` for exceptions\n\n---\n\n## ❌ Don't pass SavedStateHandle to test factory methods\n\n### The Problem\n\nExposing `SavedStateHandle` in test factory methods leaks Android framework details into test logic.\n\n### Wrong\n\n```kotlin\nprivate fun createViewModel(\n    savedStateHandle: SavedStateHandle = SavedStateHandle(),  // Framework type exposed\n): MyViewModel = MyViewModel(\n    savedStateHandle = savedStateHandle,\n    repository = mockRepository,\n)\n\n@Test\nfun `initial state from saved state`() = runTest {\n    val savedState = MyState(isLoading = true)\n    val savedStateHandle = SavedStateHandle(mapOf(\"state\" to savedState))\n\n    val viewModel = createViewModel(savedStateHandle = savedStateHandle)\n    // ...\n}\n```\n\n### Correct\n\n```kotlin\nprivate fun createViewModel(\n    initialState: MyState? = null,  // Domain type only\n): MyViewModel = MyViewModel(\n    savedStateHandle = SavedStateHandle().apply { set(\"state\", initialState) },\n    repository = mockRepository,\n)\n\n@Test\nfun `initial state from saved state`() = runTest {\n    val savedState = MyState(isLoading = true)\n\n    val viewModel = createViewModel(initialState = savedState)\n    // ...\n}\n```\n\n### Why This Matters\n\n- Cleaner, more intuitive test code\n- Hides SavedStateHandle implementation details\n- Follows Bitwarden conventions\n\n---\n\n## ❌ Don't test SavedStateHandle persistence in unit tests\n\n### The Problem\n\nTesting whether state persists to SavedStateHandle is testing Android framework behavior, not your business logic.\n\n### Wrong\n\n```kotlin\n@Test\nfun `state should persist to SavedStateHandle`() = runTest {\n    val savedStateHandle = SavedStateHandle()\n    val viewModel = createViewModel(savedStateHandle = savedStateHandle)\n\n    viewModel.trySendAction(SomeAction)\n\n    val savedState = savedStateHandle.get<MyState>(\"state\")\n    assertEquals(expectedState, savedState)  // Testing framework, not logic!\n}\n```\n\n### Correct\n\nFocus on testing business logic and state transformations:\n\n```kotlin\n@Test\nfun `action should update state correctly`() = runTest {\n    val viewModel = createViewModel()\n\n    viewModel.trySendAction(SomeAction)\n\n    assertEquals(expectedState, viewModel.stateFlow.value)  // Test observable state\n}\n```\n\n---\n\n## ❌ Don't use static mocking when DI pattern is available\n\n### The Problem\n\nStatic mocking (`mockkStatic`) is harder to maintain and less testable than dependency injection.\n\n### Wrong\n\n```kotlin\nclass ParserTest {\n    @BeforeEach\n    fun setup() {\n        mockkStatic(UUID::class)\n        every { UUID.randomUUID() } returns mockk {\n            every { toString() } returns \"fixed-uuid\"\n        }\n    }\n\n    @AfterEach\n    fun tearDown() {\n        unmockkStatic(UUID::class)\n    }\n}\n```\n\n### Correct\n\nExtract an interface and inject it:\n\n```kotlin\n// Production code\ninterface UuidManager {\n    fun generateUuid(): String\n}\n\nclass UuidManagerImpl : UuidManager {\n    override fun generateUuid(): String = UUID.randomUUID().toString()\n}\n\nclass Parser(private val uuidManager: UuidManager) { ... }\n\n// Test code\nclass ParserTest {\n    private val mockUuidManager = mockk<UuidManager>()\n\n    @BeforeEach\n    fun setup() {\n        every { mockUuidManager.generateUuid() } returns \"fixed-uuid\"\n    }\n\n    // No tearDown needed - no static mocking!\n}\n```\n\n### When to Use This Pattern\n\n- UUID generation\n- Timestamp/Clock operations\n- System property access\n- Any static function that needs deterministic testing\n\n---\n\n## ❌ Don't forget to test null stream returns from Android APIs\n\n### The Problem\n\nAndroid's `ContentResolver.openOutputStream()` and `openInputStream()` can return null, not just throw exceptions.\n\n### Wrong\n\n```kotlin\nclass FileManagerTest {\n    @Test\n    fun `stringToUri with exception should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } throws IOException()\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n\n        assertFalse(result)\n    }\n    // Missing: test for null return!\n}\n```\n\n### Correct\n\n```kotlin\nclass FileManagerTest {\n    @Test\n    fun `stringToUri with exception should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } throws IOException()\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n        assertFalse(result)\n    }\n\n    @Test\n    fun `stringToUri with null stream should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } returns null\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n        assertFalse(result)  // CRITICAL: must handle null!\n    }\n}\n```\n\n### Common Android APIs That Return Null\n\n- `ContentResolver.openOutputStream()` / `openInputStream()`\n- `Context.getExternalFilesDir()`\n- `PackageManager.getApplicationInfo()` (can throw)\n\n---\n\n## Bitwarden Mocking Guidelines\n\n**Mock at architectural boundaries:**\n- Repository → ViewModel (mock repository)\n- Service → Repository (mock service)\n- API → Service (use MockWebServer, not mocks)\n- DiskSource → Repository (mock disk source)\n\n**Fake vs Mock Strategy (IMPORTANT):**\n- **Happy paths**: Use Fake implementations (`FakeAuthenticatorDiskSource`, `FakeVaultDiskSource`)\n- **Error paths**: Use MockK with isolated repository instances\n\n```kotlin\n// Happy path - use Fake\nprivate val fakeDiskSource = FakeAuthenticatorDiskSource()\n\n@Test\nfun `createItem should return Success`() = runTest {\n    val result = repository.createItem(mockItem)\n    assertEquals(CreateItemResult.Success, result)\n}\n\n// Error path - use isolated Mock\n@Test\nfun `createItem with exception should return Error`() = runTest {\n    val mockDiskSource = mockk<AuthenticatorDiskSource> {\n        coEvery { saveItem(any()) } throws RuntimeException()\n    }\n    val repository = RepositoryImpl(diskSource = mockDiskSource)\n\n    val result = repository.createItem(mockItem)\n    assertEquals(CreateItemResult.Error, result)\n}\n```\n\n**Use Fakes for:**\n- `FakeDispatcherManager` - deterministic coroutines\n- `FakeConfigDiskSource` - in-memory config storage\n- `FakeSharedPreferences` - memory-backed preferences\n- `FakeAuthenticatorDiskSource` - in-memory authenticator storage\n\n**Create real instances for:**\n- Data classes, value objects (User, Config, CipherView)\n- Test data builders (`createMockCipher(number = 1)`)\n\n## ❌ Don't forget bufferedMutableSharedFlow with onSubscription for Fakes\n\n### The Problem\n\nFake data sources using `MutableSharedFlow` won't emit cached state to new subscribers without explicit handling.\n\n### Wrong\n\n```kotlin\nclass FakeDataSource : DataSource {\n    private val mutableFlow = MutableSharedFlow<List<Item>>()\n    private val storedItems = mutableListOf<Item>()\n\n    override fun getItems(): Flow<List<Item>> = mutableFlow\n\n    override suspend fun saveItem(item: Item) {\n        storedItems.add(item)\n        mutableFlow.emit(storedItems)\n    }\n}\n\n// Test: Initial collection gets nothing!\nrepository.dataFlow.test {\n    // Hangs or fails - no initial emission\n}\n```\n\n### Correct\n\n```kotlin\nclass FakeDataSource : DataSource {\n    private val mutableFlow = bufferedMutableSharedFlow<List<Item>>()\n    private val storedItems = mutableListOf<Item>()\n\n    override fun getItems(): Flow<List<Item>> = mutableFlow\n        .onSubscription { emit(storedItems.toList()) }\n\n    override suspend fun saveItem(item: Item) {\n        storedItems.add(item)\n        mutableFlow.emit(storedItems.toList())\n    }\n}\n\n// Test: Initial collection receives current state\nrepository.dataFlow.test {\n    assertEquals(emptyList(), awaitItem())  // Works!\n}\n```\n\n### Key Points\n\n- Use `bufferedMutableSharedFlow()` from `core/data/repository/util/`\n- Add `.onSubscription { emit(currentState) }` for immediate state emission\n- This ensures new collectors receive the current cached state\n\n---\n\n## ✅ Use Result extension functions for assertions\n\n### The Pattern\n\nUse `asSuccess()` and `asFailure()` extensions from `com.bitwarden.core.data.util` for cleaner Result assertions.\n\n### Success Path\n\n```kotlin\n@Test\nfun `getData should return success`() = runTest {\n    val result = repository.getData()\n    val expected = expectedData.asSuccess()\n\n    assertEquals(expected.getOrNull(), result.getOrNull())\n}\n```\n\n### Failure Path\n\n```kotlin\n@Test\nfun `getData with error should return failure`() = runTest {\n    val exception = IOException(\"Network error\")\n    coEvery { mockService.getData() } returns exception.asFailure()\n\n    val result = repository.getData()\n\n    assertTrue(result.isFailure)\n    assertEquals(exception, result.exceptionOrNull())\n}\n```\n\n### Avoid Redundant Assertions\n\n```kotlin\n// WRONG - redundant success checks\nassertTrue(result.isSuccess)\nassertTrue(expected.isSuccess)\nassertArrayEquals(expected.getOrNull(), result.getOrNull())\n\n// CORRECT - final assertion is sufficient\nassertArrayEquals(expected.getOrNull(), result.getOrNull())\n```\n\n---\n\n## Summary Checklist\n\nBefore submitting tests, verify:\n\n**Core Patterns:**\n- [ ] No `assertCoroutineThrows` inside `runTest`\n- [ ] All static mocks have `unmockk` in `@After`\n- [ ] EventFlow tests start with `expectNoEvents()`\n- [ ] Using FakeDispatcherManager, not real dispatchers\n- [ ] All coroutine tests use `runTest`\n\n**Assertion Patterns:**\n- [ ] Assert complete state objects, not individual fields\n- [ ] Use JUnit `assertTrue()`, not Kotlin `assert()`\n- [ ] Use `asSuccess()` for Result type assertions\n- [ ] Avoid redundant assertion patterns\n\n**Test Design:**\n- [ ] Test factory methods accept domain types, not SavedStateHandle\n- [ ] Use Fakes for happy paths, Mocks for error paths\n- [ ] Prefer DI patterns over static mocking\n- [ ] Test null returns from Android APIs (streams, files)\n- [ ] Fakes use `bufferedMutableSharedFlow()` with `.onSubscription`\n\n**General:**\n- [ ] Tests don't depend on execution order\n- [ ] Complex mocks use `relaxed = true`\n- [ ] Test data is created fresh for each test\n- [ ] Mocking behavior, not value objects\n- [ ] Testing observable behavior, not implementation\n\nWhen tests fail mysteriously, check these gotchas first.\n",".claude/skills/testing-android-code/references/flow-testing-patterns.md":"# Flow Testing with Turbine\n\nBitwarden Android uses Turbine for testing Kotlin Flows, including the critical distinction between StateFlow and EventFlow patterns.\n\n## StateFlow vs EventFlow\n\n### StateFlow (Replayed)\n\n**Characteristics:**\n- `replay = 1` - Always emits current value to new collectors\n- First `awaitItem()` returns the current/initial state\n- Survives configuration changes\n- Used for UI state that needs to be immediately available\n\n**Test Pattern:**\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateFlow.test {\n        // First awaitItem() gets CURRENT state\n        assertEquals(INITIAL_STATE, awaitItem())\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.LoadData)\n\n        // Next awaitItem() gets UPDATED state\n        assertEquals(LOADING_STATE, awaitItem())\n        assertEquals(SUCCESS_STATE, awaitItem())\n    }\n}\n```\n\n### EventFlow (No Replay)\n\n**Characteristics:**\n- `replay = 0` - Only emits new events after subscription\n- No initial value emission\n- One-time events (navigation, toasts, dialogs)\n- Does not survive configuration changes\n\n**Test Pattern:**\n```kotlin\n@Test\nfun `action should emit event`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.eventFlow.test {\n        // MUST call expectNoEvents() first - nothing emitted yet\n        expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.Submit)\n\n        // Now expect the event\n        assertEquals(MyEvent.NavigateToNext, awaitItem())\n    }\n}\n```\n\n**Critical:** Always call `expectNoEvents()` before triggering actions on EventFlow. Forgetting this causes flaky tests.\n\n## Testing State and Events Simultaneously\n\nUse the `stateEventFlow()` helper from `BaseViewModelTest`:\n\n```kotlin\n@Test\nfun `complex action should update state and emit event`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        // Initial state\n        assertEquals(INITIAL_STATE, stateFlow.awaitItem())\n\n        // No events yet\n        eventFlow.expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.ComplexAction)\n\n        // Verify state progression\n        assertEquals(LOADING_STATE, stateFlow.awaitItem())\n        assertEquals(SUCCESS_STATE, stateFlow.awaitItem())\n\n        // Verify event emission\n        assertEquals(MyEvent.ShowToast, eventFlow.awaitItem())\n    }\n}\n```\n\n## Repository Flow Testing\n\n### Testing Database Flows\n\n```kotlin\n@Test\nfun `dataFlow should emit when database updates`() = runTest {\n    val dataFlow = MutableStateFlow(initialData)\n    every { mockDiskSource.dataFlow } returns dataFlow\n\n    repository.dataFlow.test {\n        // Initial value\n        assertEquals(initialData, awaitItem())\n\n        // Update disk source\n        dataFlow.value = updatedData\n\n        // Verify emission\n        assertEquals(updatedData, awaitItem())\n    }\n}\n```\n\n### Testing Transformed Flows\n\n```kotlin\n@Test\nfun `flow transformation should map correctly`() = runTest {\n    val sourceFlow = MutableStateFlow(UserEntity(id = \"1\", name = \"John\"))\n    every { mockDao.observeUser() } returns sourceFlow\n\n    // Repository transforms entity to domain model\n    repository.userFlow.test {\n        val expectedUser = User(id = \"1\", name = \"John\")\n        assertEquals(expectedUser, awaitItem())\n    }\n}\n```\n\n## Common Patterns\n\n### Pattern 1: Testing Initial State + Action\n\n```kotlin\n@Test\nfun `load data should update from idle to loading to success`() = runTest {\n    coEvery { repository.getData() } returns \"data\".asSuccess()\n\n    viewModel.stateFlow.test {\n        assertEquals(DEFAULT_STATE, awaitItem())\n\n        viewModel.loadData()\n\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Success), awaitItem())\n    }\n}\n```\n\n### Pattern 2: Testing Error States\n\n```kotlin\n@Test\nfun `load data with error should emit failure state`() = runTest {\n    val error = Exception(\"Network error\")\n    coEvery { repository.getData() } returns error.asFailure()\n\n    viewModel.stateFlow.test {\n        assertEquals(DEFAULT_STATE, awaitItem())\n\n        viewModel.loadData()\n\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())\n        assertEquals(\n            DEFAULT_STATE.copy(loadingState = LoadingState.Error(\"Network error\")),\n            awaitItem(),\n        )\n    }\n}\n```\n\n### Pattern 3: Testing Event Sequences\n\n```kotlin\n@Test\nfun `submit should emit validation then navigation events`() = runTest {\n    viewModel.eventFlow.test {\n        expectNoEvents()\n\n        viewModel.trySendAction(MyAction.Submit)\n\n        assertEquals(MyEvent.ShowValidation, awaitItem())\n        assertEquals(MyEvent.NavigateToNext, awaitItem())\n    }\n}\n```\n\n### Pattern 4: Testing Cancellation\n\n```kotlin\n@Test\nfun `cancelling collection should stop emissions`() = runTest {\n    val flow = flow {\n        repeat(100) {\n            emit(it)\n            delay(100)\n        }\n    }\n\n    flow.test {\n        assertEquals(0, awaitItem())\n        assertEquals(1, awaitItem())\n\n        // Cancel after 2 items\n        cancel()\n\n        // No more items received\n    }\n}\n```\n\n## Anti-Patterns\n\n### ❌ Forgetting expectNoEvents() on EventFlow\n\n```kotlin\n// WRONG\nviewModel.eventFlow.test {\n    viewModel.trySendAction(action)  // May fail - no initial expectNoEvents\n    assertEquals(event, awaitItem())\n}\n\n// CORRECT\nviewModel.eventFlow.test {\n    expectNoEvents()  // ALWAYS do this first\n    viewModel.trySendAction(action)\n    assertEquals(event, awaitItem())\n}\n```\n\n### ❌ Not Using runTest\n\n```kotlin\n// WRONG - Missing runTest\n@Test\nfun `test flow`() {\n    flow.test { /* ... */ }\n}\n\n// CORRECT\n@Test\nfun `test flow`() = runTest {\n    flow.test { /* ... */ }\n}\n```\n\n### ❌ Mixing StateFlow and EventFlow Patterns\n\n```kotlin\n// WRONG - Treating StateFlow like EventFlow\nstateFlow.test {\n    expectNoEvents()  // Unnecessary - StateFlow always has value\n    /* ... */\n}\n\n// WRONG - Treating EventFlow like StateFlow\neventFlow.test {\n    val item = awaitItem()  // Will hang - no initial value!\n    /* ... */\n}\n```\n\n## Reference Implementations\n\n**ViewModel with StateFlow and EventFlow:**\n`app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n**Repository Flow Testing:**\n`data/src/test/kotlin/com/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt`\n\n**Complex Flow Transformations:**\n`data/src/test/kotlin/com/bitwarden/data/vault/repository/VaultRepositoryTest.kt`\n",".claude/skills/testing-android-code/references/test-base-classes.md":"# Test Base Classes Reference\n\nBitwarden Android provides specialized base classes that configure test environments and provide helper utilities.\n\n## BaseViewModelTest\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`\n\n### Purpose\nProvides essential setup for testing ViewModels with proper coroutine dispatcher configuration and Flow testing helpers.\n\n### Automatic Configuration\n- Registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`\n- Ensures deterministic coroutine execution in tests\n- All coroutines complete immediately without real delays\n\n### Key Feature: stateEventFlow() Helper\n\n**Use Case:** When you need to test both StateFlow and EventFlow simultaneously.\n\n```kotlin\n@Test\nfun `complex action should update state and emit event`() = runTest {\n    val viewModel = ExampleViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        // Verify initial state\n        assertEquals(INITIAL_STATE, stateFlow.awaitItem())\n\n        // No events yet\n        eventFlow.expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(ExampleAction.ComplexAction)\n\n        // Verify state updated\n        assertEquals(LOADING_STATE, stateFlow.awaitItem())\n\n        // Verify event emitted\n        assertEquals(ExampleEvent.ShowToast, eventFlow.awaitItem())\n    }\n}\n```\n\n### Usage Pattern\n\n```kotlin\nclass MyViewModelTest : BaseViewModelTest() {\n    private val mockRepository: MyRepository = mockk()\n    private val savedStateHandle = SavedStateHandle(\n        mapOf(KEY_STATE to INITIAL_STATE)\n    )\n\n    @Test\n    fun `test action`() = runTest {\n        val viewModel = MyViewModel(\n            savedStateHandle = savedStateHandle,\n            repository = mockRepository\n        )\n\n        // Test with automatic dispatcher setup\n        viewModel.stateFlow.test {\n            assertEquals(INITIAL_STATE, awaitItem())\n        }\n    }\n}\n```\n\n## BitwardenComposeTest\n\n**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`\n\n### Purpose\nPre-configured test class for Compose UI tests with all Bitwarden managers and theme setup.\n\n### Automatic Configuration\n- All Bitwarden managers pre-configured (FeatureFlags, AuthTab, Biometrics, etc.)\n- Wraps content in `BitwardenTheme` and `LocalManagerProvider`\n- Provides fixed `Clock` for deterministic time-based tests\n- Extends `BaseComposeTest` for Robolectric and dispatcher setup\n\n### Key Features\n\n**Pre-configured Managers:**\n- `FeatureFlagManager` - Controls feature flag behavior\n- `AuthTabManager` - Manages auth tab state\n- `BiometricsManager` - Handles biometric authentication\n- `ClipboardManager` - Clipboard operations\n- `NotificationManager` - Notification display\n\n**Fixed Clock:**\nAll tests use a fixed clock for deterministic time-based testing:\n```kotlin\n// Tests use consistent time: 2023-10-27T12:00:00Z\nval fixedClock: Clock\n```\n\n### Usage Pattern\n\n```kotlin\nclass MyScreenTest : BitwardenComposeTest() {\n    private var haveCalledNavigateBack = false\n    private val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n    private val viewModel = mockk<MyViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        setContent {\n            MyScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                viewModel = viewModel\n            )\n        }\n    }\n\n    @Test\n    fun `on back click should send action`() {\n        composeTestRule.onNodeWithContentDescription(\"Back\").performClick()\n        verify { viewModel.trySendAction(MyAction.BackClick) }\n    }\n\n    @Test\n    fun `loading state should show progress`() {\n        mutableStateFlow.value = DEFAULT_STATE.copy(isLoading = true)\n        composeTestRule.onNode(isProgressBar).assertIsDisplayed()\n    }\n}\n```\n\n### Important: bufferedMutableSharedFlow for Events\n\nIn Compose tests, use `bufferedMutableSharedFlow` instead of regular `MutableSharedFlow` (default replay is 0):\n\n```kotlin\n// Correct for Compose tests\nprivate val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()\n\n// This allows triggering events and having the UI react\nmutableEventFlow.tryEmit(MyEvent.NavigateBack)\n```\n\n## BaseServiceTest\n\n**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt`\n\n### Purpose\nProvides MockWebServer setup for testing API service implementations.\n\n### Automatic Configuration\n- `server: MockWebServer` - Auto-started before each test, stopped after\n- `retrofit: Retrofit` - Pre-configured with:\n  - JSON converter (kotlinx.serialization)\n  - NetworkResultCallAdapter for Result<T> responses\n  - Base URL pointing to MockWebServer\n- `json: Json` - kotlinx.serialization JSON instance\n\n### Usage Pattern\n\n```kotlin\nclass MyServiceTest : BaseServiceTest() {\n    private val api: MyApi = retrofit.create()\n    private val service = MyServiceImpl(api)\n\n    @Test\n    fun `getConfig should return success when API succeeds`() = runTest {\n        // Enqueue mock response\n        server.enqueue(MockResponse().setBody(EXPECTED_JSON))\n\n        // Call service\n        val result = service.getConfig()\n\n        // Verify result\n        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)\n    }\n\n    @Test\n    fun `getConfig should return failure when API fails`() = runTest {\n        // Enqueue error response\n        server.enqueue(MockResponse().setResponseCode(500))\n\n        // Call service\n        val result = service.getConfig()\n\n        // Verify failure\n        assertTrue(result.isFailure)\n    }\n}\n```\n\n### MockWebServer Patterns\n\n**Enqueue successful response:**\n```kotlin\nserver.enqueue(MockResponse().setBody(\"\"\"{\"key\": \"value\"}\"\"\"))\n```\n\n**Enqueue error response:**\n```kotlin\nserver.enqueue(MockResponse().setResponseCode(404))\nserver.enqueue(MockResponse().setResponseCode(500))\n```\n\n**Enqueue delayed response:**\n```kotlin\nserver.enqueue(\n    MockResponse()\n        .setBody(\"\"\"{\"key\": \"value\"}\"\"\")\n        .setBodyDelay(1000, TimeUnit.MILLISECONDS)\n)\n```\n\n**Verify request details:**\n```kotlin\nval request = server.takeRequest()\nassertEquals(\"/api/config\", request.path)\nassertEquals(\"GET\", request.method)\nassertEquals(\"Bearer token\", request.getHeader(\"Authorization\"))\n```\n\n## BaseComposeTest\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseComposeTest.kt`\n\n### Purpose\nBase class for Compose tests that extends `BaseRobolectricTest` and provides `setTestContent()` helper.\n\n### Features\n- Robolectric configuration for Compose\n- Proper dispatcher setup\n- `composeTestRule` for UI testing\n- `setTestContent()` helper wraps content in theme\n\n### Usage\nTypically you'll extend `BitwardenComposeTest` which extends this class. Use `BaseComposeTest` directly only for tests that don't need Bitwarden-specific manager configuration.\n\n## When to Use Each Base Class\n\n| Test Type | Base Class | Use When |\n|-----------|------------|----------|\n| ViewModel tests | `BaseViewModelTest` | Testing ViewModel state and events |\n| Compose screen tests | `BitwardenComposeTest` | Testing Compose UI with Bitwarden components |\n| API service tests | `BaseServiceTest` | Testing network layer with MockWebServer |\n| Repository tests | None (manual setup) | Testing repository logic with mocked dependencies |\n| Utility/helper tests | None (manual setup) | Testing pure functions or utilities |\n\n## Complete Examples\n\n**ViewModel Test:**\n`../examples/viewmodel-test-example.md`\n\n**Compose Screen Test:**\n`../examples/compose-screen-test-example.md`\n\n**Repository Test:**\n`../examples/repository-test-example.md`\n","docs/ARCHITECTURE.md":"# Architecture\n\n- [Overview](#overview)\n- [Application Modules](#application-modules)\n  - [Core Library Modules](#core-library-modules)\n  - [Specialized Library Modules](#specialized-library-modules)\n  - [Supporting Modules (Source Sets and Test Fixtures)](#supporting-modules-source-sets-and-test-fixtures)\n- [Data Layer](#data-layer)\n  - [Data Sources](#data-sources)\n  - [Managers](#managers)\n  - [Repositories](#repositories)\n  - [Note on Dependency Injection](#note-on-dependency-injection)\n- [UI Layer](#ui-layer)\n  - [ViewModels / MVVM](#viewmodels--mvvm)\n    - [Example](#example)\n  - [Screens / Compose](#screens--compose)\n    - [State Hoisting](#state-hoisting)\n    - [Example](#example-1)\n  - [Navigation](#navigation)\n    - [State-based Navigation](#state-based-navigation)\n    - [Event-based Navigation](#event-based-navigation)\n    - [Navigation Implementation](#navigation-implementation)\n    - [Example](#example-2)\n\n## Overview\n\nThe Bitwarden Android project employs a multi-module architecture to enhance separation of concerns, maintainability, and build efficiency. This architecture distinguishes between application modules, which are user-facing, and library modules, which provide shared functionalities.\n\n### Application modules\nThese modules represent the entry points and primary user interfaces for the applications. They are structured with standard Android source sets to organize code for different build types and testing:\n\n* **`Bitwarden.app`**: This module constitutes the Bitwarden Password Manager application. It integrates various library modules to deliver password management and secure data storage features. It is responsible for the application's top-level setup, primary navigation graph, and dependency injection rooting for password manager functionalities. The `app` module utilizes product flavors to generate different versions of the application:\n    * `src/standard/`: Contains code and resources specific to the 'standard' product flavor. Builds produced from this flavor are intended for distribution via the Google Play Store and include functionality dependent on Google Play Services.\n    * `src/fdroid/`: Contains code and resources specific to the 'fdroid' product flavor. Builds produced from this flavor are intended for distribution via F-Droid. This flavor is stripped of all Google Play Services libraries and, as such, does not contain features like push notifications for real-time syncing.\n    * `src/debug/`: Contains code and resources specific to the 'debug' build type (e.g., for enabling debug-specific features, configurations, or alternative API endpoints), which can be combined with any product flavor.\n    * `src/release/`: Contains code and resources specific to the 'release' build type (e.g., for ProGuard/R8 configurations, release-specific API keys, or analytics configurations), which can be combined with any product flavor.\n* **`Bitwarden.authenticator`**: This module implements the Bitwarden Authenticator application. Its purpose is to store and generate two-factor authentication (2FA) codes, such as Time-based One-Time Passwords (TOTP), directly on the user's device. It provides the user interface and logic for managing authenticator accounts and displaying codes, functioning as a standalone authenticator application.\n    * `src/debug/`: Contains code and resources specific to the 'debug' build type of the Authenticator.\n    * `src/release/`: Contains code and resources specific to the 'release' build type of the Authenticator.\n\n### Core Library Modules\n\nThese libraries provide foundational capabilities utilized by application modules and other feature-specific libraries:\n\n* **`Bitwarden.core`**: A fundamental library containing shared business logic, essential data models (entities, value objects), domain-specific utilities, and common interfaces. It is designed with minimal or no direct Android framework dependencies to ensure high reusability and testability.\n* **`Bitwarden.data`**: Manages the data layer for the applications. Responsibilities include defining repositories, data sources (for network APIs and local persistence like Room), data transformation logic (mappers), and managing data synchronization. It abstracts data origin and storage details from other parts of the system.\n* **`Bitwarden.network`**: Encapsulates all network communication logic. This includes API client setup (e.g., using Retrofit or Ktor), defining request/response models for network operations, and handling network interceptors and authentication.\n* **`Bitwarden.ui`**: A library module providing common UI elements, theming resources (colors, typography, shapes), custom Jetpack Compose components, base UI classes, and UI-related utility functions. It ensures a consistent user experience across different features and applications.\n\n### Specialized Library Modules\n\nThese libraries offer more targeted functionalities:\n\n* **`Bitwarden.authenticatorbridge`**: Serves as an interface or bridge facilitating communication and integration between the `Bitwarden.authenticator` module's functionalities and the`Bitwarden.app` module. This allows for controlled interaction while maintaining modular decoupling.\n* **`Bitwarden.annotation`**: Contains custom Java/Kotlin annotations used throughout the project. These annotations support compile-time code generation (e.g., for dependency injection with Dagger Hilt, or database schema generation with Room) and provide metadata for other development tools.\n\n### Supporting Modules (Source Sets and Test Fixtures)\n\nThe project structure includes conventional Gradle source sets and test fixture modules that support\ndevelopment and testing:\n\n* **`src/main/`**: The primary source set of a module, containing its functional code.\n* **`src/test/`**: Contains local unit tests (JVM-based) for the associated module.\n* **`src/testFixtures/`**: Provides shared testing utilities, mock objects, sample data, or helper\n  functions reusable across various test modules.\n\n## Data Layer\n\nThe data layer is where all the UI-independent data is stored and retrieved. It consists of both raw data sources as well as higher-level \"repository\" and \"manager\" classes.\n\nNote that any functions exposed by a data layer class that must perform asynchronous work do so by exposing **suspending functions** that may run inside [coroutines](https://kotlinlang.org/docs/coroutines-guide.html) while any streaming sources of data are handled by exposing [Flows](https://kotlinlang.org/docs/flow.html).\n\n### Data Sources\n\nThe lowest level of the data layer are the \"data source\" classes. These are the raw sources of data that include data persisted in [Room](https://developer.android.com/jetpack/androidx/releases/room3) / [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences), data retrieved from network requests using [Retrofit](https://github.com/square/retrofit), and data retrieved via interactions with the [Bitwarden SDK](https://github.com/bitwarden/sdk).\n\nNote that these data sources are constructed in a manner that adheres to a very important principle of the app: **that function calls should not throw exceptions** (see the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--kotlin) for more details.) In the case of data sources, this tends to mean that suspending functions like those representing network requests or Bitwarden SDK calls should return a [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/) type. This is an important responsibility of the data layer as a wrapper around other third party libraries, as dependencies like Retrofit and the Bitwarden SDK tend to throw exceptions to indicate errors instead.\n\n### Managers\n\nManager classes represent something of a middle level of the data layer. While some manager classes like [VaultLockManager](../app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt) depend on the the lower-level data sources, others are wrappers around OS-level classes (ex: [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt)) while others have no dependencies at all (ex: [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt)). The commonality amongst the manager classes is that they tend to have a single discrete responsibility. These classes may also exist solely in the data layer for use inside a repository or manager class, like [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt), or may be exposed directly to the UI layer, like [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt).\n\n### Repositories\n\nRepository classes represent the outermost level of the data layer. They can take data sources, managers, and in rare cases even other repositories as dependencies and are meant to be exposed directly to the UI layer. They synthesize data from multiple sources and combine various asynchronous requests as necessary in order to expose data to the UI layer in a more appropriate form. These classes tend to have broad responsibilities that generally cover a major domain of the app, such as authentication ([AuthRepository](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt)) or vault access ([VaultRepository](../app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt)).\n\nRepository classes also feature functions that do not throw exceptions, but unlike the lower levels of the data layer the `Result` type should be avoided in favor of custom sealed classes that represent the various success/error cases in a more processed form. Returning raw `Throwable`/`Exception` instances as part of \"error\" states should be avoided when possible.\n\nIn some cases a source of data may be continuously observed and in these cases a repository may choose to expose a [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) that emits data updates using the [DataState](../app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/DataState.kt) wrapper.\n\n### Note on Dependency Injection\n\nNearly all classes in the data layer consist of interfaces representing exposed behavior and a corresponding `...Impl` class implementing that interface (ex: [AuthDiskSource](../app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt) / [AuthDiskSourceImpl](../app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt)). All `...Impl` classes are intended to be manually constructed while their associated interfaces are provided for dependency injection via a [Hilt Module](https://dagger.dev/hilt/modules.html) (ex: [PlatformNetworkModule](../app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt)). This prevents the `...Impl` classes from being injected by accident and allows the interfaces to be easily mocked/faked in tests.\n\n## UI Layer\n\nThe UI layer adheres to the concept of [unidirectional data flow](https://developer.android.com/develop/ui/compose/architecture#udf) and makes use of the MVVM design pattern. Both concepts are in line what Google currently recommends as the best approach for building the UI-layer of a modern Android application and this allows us to make use of all the available tooling Google provides as part of the [Jetpack suite of libraries](https://developer.android.com/jetpack). The MVVM implementation is built around the Android [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) class and the UI itself is constructed using the [Jetpack Compose](https://developer.android.com/develop/ui/compose), a declarative UI framework specifically built around the unidirectional data flow approach.\n\nEach screen in the app is associated with at least the following three classes/files:\n\n- A `...ViewModel` class responsible for managing the data and state for the screen.\n- A `...Screen` class that contains the Compose implementation of the UI.\n- A `...Navigation` file containing the details for how to add the screen to the overall navigation graph and how navigate to it within the graph.\n\n### ViewModels / MVVM\n\nThe app's approach to MVVM is based around the handling of \"state\", \"actions\", and \"events\" and is encoded in the [BaseViewModel](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/BaseViewModel.kt) class.\n\n- **State:** The \"state\" represents the complete internal and external **total state** of the ViewModel (VM). Any and all information needed to configure the UI that is associated with the VM should be included in this state and is exposed via the `BaseViewModel.stateFlow` property as `StateFlow<S>` (where `S` represents the unique type of state associated with the VM). This state is typically a combination of state from the data layer (such as account information) and state from the UI layer (such as data entered into a text field).\n\n  There should be no additional `StateFlow` exposed from a VM that would represent some other kind of state; all state should be represented by `S`. Additionally, any internal state not directly needed by the UI but which influences the behavior of the VM should be included as well in order to keep all state managed by the VM in a single place.\n\n- **Actions:** The \"actions\" represent interactions with the VM in some way that could potentially cause an update to that total state. These can be external actions coming from the user's interaction with the UI, like click events, or internal actions coming from some asynchronous process internal to the VM itself, like the result of some suspending functions. Actions are sent by interacting directly with `BaseViewModel.actionChannel` or by using the `BaseViewModel.sendAction` and `BaseViewModel.trySendAction` helpers. All actions are then processed synchronously in a queue in the `handleAction` function.\n\n  It is worth emphasizing that state should never be updated inside a coroutine in a VM; all asynchronous work that results in a state update should do so by posting an internal action. This ensures that the only place that state changes can occur is synchronously inside the `handleAction` function. This makes the process of finding and reasoning about state changes easier and simplifies debugging.\n\n- **Events:** The \"events\" represent discrete, one-shot side-effects and are typically associated with navigation events triggered by some user action. They are sent internally using `BaseViewModel.sendEvent` and may be consumed by the UI layer via the `BaseViewModel.eventFlow` property. An [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) should typically be used to simplify the consumption of these events.\n\nVMs are typically injected using [Dagger Hilt](https://developer.android.com/training/dependency-injection/hilt-android) by annotating them with `@HiltViewModel`. This allows them to be constructed, retrieved, and cached in the Compose UI layer by calling `hiltViewModel()` with the appropriate type. Dependencies passed into the VM constructor will typically be singletons of the graph, such as [AuthRepository](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt). In cases where the VM needs to initialized with some specific data (like an ID) that is sent from the previous screen, this data should be retrieved by injecting the [SavedStateHandle](https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate) and pulling out data using a type-safe wrapper (ex: [LoginArgs](../app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt)).\n\n#### Example\n\nThe following is an example that demonstrates many of the above principles and best practices when using `BaseViewModel` to implement a VM. It has the following features:\n\n- It is injected with a repository (`ExampleRepository`) that provides streaming `ExampleData` and a `SavedStateHandle` in order to pull initial data using a `ExampleArgs` wrapper class.\n- It has state that manages several properties of interest to the UI: `exampleData`, `isToggledEnabled`, and `dialogState`.\n- It receives external actions from the UI (`ContinueButtonClick` and `ToggleValueUpdate`) as well as internal actions launch from inside different coroutines coroutines (`Internal.ExampleDataReceive`, `Internal.ToggleValueSync`). These actions result in state updates or the emission of an event (`NavigateToNextScreen`).\n- It saves the current state to the `SavedStateHandle` in order to restore it later after process death and restoration. This ensures the user's actions and associated state will not be lost if the app goes into the background and is temporarily killed by the OS to conserve memory.\n\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\nprivate const val KEY_STATE = \"state\"\n\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    private val exampleRepository: ExampleRepository,\n    private val savedStateHandle: SavedStateHandle,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    // If previously saved state data is present in the SavedStateHandle, use that as the initial\n    // state, otherwise initialize from repository and navigation data.\n    initialState = savedStateHandle[KEY_STATE] ?: ExampleState(\n        exampleData = exampleRepository.exampleDataStateFlow.value,\n        isToggleEnabled = ExampleArgs(savedStateHandle).isToggleEnabledInitialValue,\n        dialogState = null,\n    )\n) {\n    init {\n        // As the state updates, write to saved state handle for retrieval after process death and\n        // restoration.\n        stateFlow\n            .onEach { savedStateHandle[KEY_STATE] = it }\n            .launchIn(viewModelScope)\n\n        exampleRepository\n            .exampleDataStateFlow\n            // Asynchronously received data is converted to an internal action and sent in order to\n            // be handled by `handleAction`.\n            .map { ExampleAction.Internal.ExampleDataReceive(it) }\n            .onEach(::sendAction)\n            .launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        when (action) {\n            is ExampleAction.ContinueButtonClick -> handleContinueButtonClick()\n            is ExampleAction.ToggleValueUpdate -> handleToggleValueUpdate(action)\n            is ExampleAction.Internal -> handleInternalAction(action)\n        }\n    }\n\n    private fun handleContinueButtonClick() {\n        // Update the state to show a dialog\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Loading,\n            )\n        }\n\n        // Run a suspending call in a coroutine to fetch data and post the result back as an\n        // internal action for further processing so that all state changes and event emissions\n        // happen synchronously in `handleAction`.\n        viewModelScope.launch {\n            val completionData = exampleRepository\n                .fetchCompletionData(isToggleEnabled = state.isToggleEnabled)\n\n            sendAction(\n                ExampleAction.Internal.CompletionDataReceive(\n                    completionData = completionData,\n                )\n            )\n        }\n    }\n\n    private fun handleToggleValueUpdate(action: ExampleAction.ToggleValueUpdate) {\n        // Update the state\n        mutableStateFlow.update {\n            it.copy(\n                isToggleEnabled = action.isToggleEnabled,\n            )\n        }\n    }\n\n    private fun handleInternalAction(action: ExampleAction.Internal) {\n        when (action) {\n          is ExampleAction.Internal.CompletionDataReceive -> handleCompletionDataReceive(action)\n          is ExampleAction.Internal.ExampleDataReceive -> handleExampleDataReceive(action)\n        }\n    }\n\n    private fun handleCompletionDataReceive(action: ExampleAction.Internal.CompletionDataReceive) {\n        // Update the state to clear the dialog\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = null,\n            )\n        }\n\n        // Send event with data from the action to navigate\n        sendEvent(\n            ExampleEvent.NavigateToNextScreen(\n                completionData = action.completionData,\n            )\n        )\n    }\n\n    private fun handleExampleDataReceive(action: ExampleAction.Internal.ExampleDataReceive) {\n        // Update the state\n        mutableStateFlow.update {\n            it.copy(\n                exampleData = action.exampleData,\n            )\n        }\n    }\n}\n\n@Parcelize\ndata class ExampleState(\n    val exampleData: String,\n    val isToggleEnabled: Boolean,\n    val dialogState: DialogState?,\n) : Parcelable {\n\n    sealed class DialogState : Parcelable {\n        @Parcelize\n        data object Loading : DialogState()\n    }\n}\n\nsealed class ExampleEvent {\n    data class NavigateToNextScreen(\n        val completionData: CompletionData,\n    ) : ExampleEvent()\n}\n\nsealed class ExampleAction {\n    data object ContinueButtonClick : ExampleAction()\n\n    data class ToggleValueUpdate(\n        val isToggleEnabled: Boolean,\n    ) : ExampleAction()\n\n    sealed class Internal : ExampleAction() {\n        data class CompletionDataReceive(\n          val completionData: CompletionData,\n        ) : Internal()\n\n        data class ExampleDataReceive(\n            val exampleData: String,\n        ) : Internal()\n    }\n}\n```\n</details>\n\n\n### Screens / Compose\n\nEach unique screen destination is represented by a composable `...Screen` function annotated with `@Composable`. The responsibilities of this layer include:\n\n- Receiving \"state\" data from the associated `ViewModel` and rendering the UI accordingly.\n- Receiving \"events\" from the associated `ViewModel` and taking the corresponding action, which is typically to trigger a passed-in callback function for navigation purposes. An [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) should be used to simplify this process.\n- Sending \"actions\" to the `ViewModel` indicating user interactions or UI-layer events as necessary.\n- Interacting with purely UI-layer \"manager\" classes that are not appropriate or possible to be injected into the `ViewModel` (such as those concerning permissions or camera access).\n\nIn order to both provide consistency to the app and to simplify the development of new screens, an extensive system of reusable composable components has been developed and may be found in the [components package](../app/src/main/java/com/x8bit/bitwarden/ui/platform/components). These tend to bear the prefix `Bitwarden...` to easily distinguish them from similar OS-level components. If there is any new unique UI component that is added, it should always be considered if it should be made a shareable component.\n\nRefer to the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--jetpack-compose) for additional information on best practices when using Compose.\n\n#### State-hoisting\n\nJetpack Compose is built around the idea that the state required to render any given composable function can be \"hoisted\" to higher levels that may need access to that state. This means that the responsibility for keeping track of the current state of a component may not typically reside with the component itself. It is important, therefore, to understand where the best place is to manage any given piece of UI state.\n\nThis is discussed in detail in [Google's state-hoisting documentation](https://developer.android.com/develop/ui/compose/state-hoisting) and can be summarized roughly as follows: **state should be hoisted as high as is necessary to perform any relevant logic and no higher.** This is a pattern that is followed in the Bitwarden app and tends to lead to the following:\n\n- Any state that will eventually need to be used by the VM should be hoisted to the VM. For example, any text input, toggle values, etc. that may be updated by user interactions should be completely controlled by the VM. These state changes are communicated via the \"actions\" that the Compose layer sends, which trigger corresponding updates the VM's total \"state\".\n- Any UI state that will not be used by logic inside the VM should remain in the UI layer. For example, visibility toggle states for password inputs should typically remain out of the VM.\n\nThese rules can lead to some notable differences in how certain dialogs are handled. For example, loading dialogs are controlled by events that occur in the VM, such as when a network request is started and when it completes. This requires the VM to be in charge of managing the visibility state of the dialog. However, some dialogs appear as the result of clicking on an item in the UI in order to simply display information or to ask for the user's confirmation before some action is taken. Because there is no logic in the VM that depends on the visibility of these particular dialogs, their visibility state can be controlled by the UI. Note, however, that any user interaction that results in a navigation to a new screen should always be routed to the VM first, as the VM should always be in charge of triggering changes at the per-screen level.\n\n#### Example\n\nThe following shows off the basic structure of a composable `...Screen` implementation. Note that:\n\n- The VM is \"injected\" using the `hiltViewModel()` helper function. This will correctly create, cache, and scope the VM in production code while making it easier to pass in mock/fake implementations in test code.\n- An `onNavigateToNextScreen` function is also passed in, which can be called to trigger a navigation via a call to a [NavController](https://developer.android.com/reference/androidx/navigation/NavController) in the outer layers of the navigation graph. Passing in a function rather than the `NavController` itself decouples the screen code from the navigation framework and greatly simplifies the testing of screens.\n- The VM \"state\" is consumed using `viewModel.stateFlow.collectAsStateWithLifecycle()`. This will cause the composable to \"recompose\" and update whenever updates are pushed to the `viewModel.stateFlow`.\n- The VM \"events\" are consumed using an [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) and demonstrate how the `onNavigateToNextScreen` may be triggered.\n- The current state of the text, switch, and button are hoisted to the VM and pulled out from the `state`. User interactions with the switch and button result in \"actions\" being sent to the VM using `viewModel.trySendAction`.\n- Reusable components (`BitwardenLoadingDialog`, `BitwardenSwitch`, and `BitwardenFilledButton`) are used where possible in order to build the screen using the correct theming and reduce code duplication. When this is not possible (such as when rending the `Text` composable) all colors and styles are pulled from the BitwardenTheme object.\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\n@Composable\nfun ExampleScreen(\n    onNavigateToNextScreen: (CompletionData) -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    // Collect state\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    // Handle events\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            is ExampleEvent.NavigateToNextScreen -> {\n                onNavigateToNextScreen(event.completionData)\n            }\n        }\n    }\n\n    // Show state-based dialogs if necessary\n    when (state.dialogState) {\n        ExampleState.DialogState.Loading -> {\n            BitwardenLoadingDialog(\n                visibilityState = LoadingDialogState.Shown(\n                    text = R.string.loading.asText(),\n                )\n            )\n        }\n\n        else -> Unit\n    }\n\n    // Render the remaining state\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = Modifier\n            .verticalScroll(rememberScrollState()),\n    ) {\n        Text(\n            text = state.exampleData,\n            textAlign = TextAlign.Center,\n            style = BitwardenTheme.typography.headlineSmall,\n            color = BitwardenTheme.colorScheme.textColors.primary,\n            modifier = Modifier\n                .padding(horizontal = 24.dp)\n                .wrapContentHeight(),\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        BitwardenSwitch(\n            label = stringResource(id = R.string.toggle_label),\n            isChecked = state.isToggleEnabled,\n            onCheckedChange = { viewModel.trySendAction(ExampleAction.ToggleValueUpdate(it)) },\n            modifier = Modifier\n                .padding(horizontal = 16.dp)\n                .fillMaxWidth(),\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        BitwardenFilledButton(\n            label = stringResource(id = R.string.continue_text),\n            onClick = { viewModel.trySendAction(ExampleAction.ContinueButtonClick) },\n            modifier = Modifier\n                .padding(horizontal = 16.dp)\n                .fillMaxWidth(),\n        )\n    }\n}\n```\n</details>\n\n### Navigation\n\nNavigation in the app is achieved via the [Compose Navigation](https://developer.android.com/develop/ui/compose/navigation) component. This involves specifying \"destinations\" within a [NavHost](https://developer.android.com/reference/androidx/navigation/NavHost) and using a [NavController](https://developer.android.com/reference/androidx/navigation/NavController) to trigger actual navigation events. The app uses a mix of state-based navigation at the highest level (in order to determine the overall app flow the user is in) and event-based navigation (to handle navigation within a flow based on user interactions).\n\n#### State-based Navigation\n\nState-based navigation is handled by the [RootNavViewModel](../app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt) and [RootNavScreen](../app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt). The current navigation state is determined within the `RootNavViewModel` by monitoring the overall [UserState](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt) along with any [SpecialCircumstance](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt). The `UserState` encodes whether there are any user accounts, whether they are logged in, etc., while the `SpecialCircumstance` describes a particular circumstance under which the app may have been launched (such as being opened in order to manually select an autofill value). The `RootNavState` calculated by the `RootNavViewModel` is then used by the `RootNavScreen` in order to trigger a navigation only if the overall state has changed.\n\nThe benefit of state-based navigation at this level is that it allows for a variety of state changes that may happen deep within the data layer to automatically trigger navigation to the correct top-level flow without the rest of the UI needing to be concerned with these details. For example, when logging out the current user, the data layer can decide the new state and the `RootNavViewModel` can decide where to go next, rather than having the screen that triggered the logout action choose the next navigation state (which would then need to be redundantly handled in multiple places in the app).\n\nState-based navigation should be limited to specific cases that affect the overall flow of the app and are driven by data emitted from the data layer. All other navigation should use event-based navigation.\n\n#### Event-based Navigation\n\nEvent-based navigation is ultimately the result of some \"action\" sent to a VM, whether that comes directly from a user-interaction with the current screen or from some internal action of the VM sent as the result of some asynchronous call. The VM will send an \"event\" to the Compose layer, which will trigger a callback that will eventually make a call to a [NavController](https://developer.android.com/reference/androidx/navigation/NavController).\n\n#### Navigation Implementation\n\nAt its most basic level, Compose Navigation relies on constructing String-based URIs with a root path to specify a destination and both path and query parameters to handle the passing of required and optional parameters. This process can typically be very error prone. The Bitwarden app borrows a pattern best exemplified by the [Now In Android sample project](https://github.com/android/nowinandroid) in order to make this process more type-safe. This consists of:\n\n- A type-safe `...Destination` extension function on [NavGraphBuilder](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) for any single screen destination or a type-safe `...Graph` extension function on [NavGraphBuilder](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) for any nested graph destination.\n- A type-safe `navigateTo...` extension function on [NavController](https://developer.android.com/reference/androidx/navigation/NavController) for any destination that is not simply the root of a graph.\n- A type-safe `...Args` class that pulls data from a `SavedStateHandle` for consumption by a VM.\n\nThese are all then grouped into a single `...Navigation.kt` file where all of the raw String-based details can be hidden and only the exposed type-safe functions must be used by the rest of the app.\n\n#### Example\n\nThe following example demonstrates a sample `ExampleNavigation.kt` file with:\n\n- An `ExampleArgs` class that wraps `SavedStateHandle` and allows for the `isToggleEnabledInitialValue` to be extracted in a type-safe way.\n- A `NavGraphBuilder.exampleDestination` extension function that can be used to safely add the `ExampleScreen` to the navigation graph.\n- A `NavController.navigateToExample` extension function that can be called to navigate to the `ExampleScreen` in a type-safe way.\n\nNote in particular how consumers of the above **do not need to know the details of the actual route or path parameter**.\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\n@Serializable\ndata class ExampleRoute(\n    val isToggleEnabled: Boolean,\n)\n\ndata class ExampleArgs(\n    val isToggleEnabledInitialValue: Boolean,\n)\n\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(isToggleEnabledInitialValue = route.isToggleEnabled)\n}\n\nfun NavGraphBuilder.exampleDestination(\n    onNavigateToNextScreen: (CompletionData) -> Unit,\n) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(onNavigateToNextScreen = onNavigateToNextScreen)\n    }\n}\n\nfun NavController.navigateToExample(\n    isToggleEnabled: Boolean,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(\n        route = ExampleRoute(isToggleEnabled = isToggleEnabled),\n        navOptions = navOptions,\n    )\n}\n```\n\n</details>\n",".cursorrules (android-jetpack-compose-cursorrules-prompt-file.mdc)":"---\ndescription: \"Cursor rules for Android development with Jetpack Compose integration.\"\nglobs: **/*\nalwaysApply: false\n---\n// Android Jetpack Compose .cursorrules\n\n// Flexibility Notice\n\n// Note: This is a recommended project structure, but be flexible and adapt to existing project structures.\n// Do not enforce these structural patterns if the project follows a different organization.\n// Focus on maintaining consistency with the existing project architecture while applying Jetpack Compose best practices.\n\n// Project Architecture and Best Practices\n\nconst androidJetpackComposeBestPractices = [\n    \"Adapt to existing project architecture while maintaining clean code principles\",\n    \"Follow Material Design 3 guidelines and components\",\n    \"Implement clean architecture with domain, data, and presentation layers\",\n    \"Use Kotlin coroutines and Flow for asynchronous operations\",\n    \"Implement dependency injection using Hilt\",\n    \"Follow unidirectional data flow with ViewModel and UI State\",\n    \"Use Compose navigation for screen management\",\n    \"Implement proper state hoisting and composition\",\n];\n\n// Folder Structure\n\n// Note: This is a reference structure. Adapt to the project's existing organization\n\nconst projectStructure = `\napp/\n  src/\n    main/\n      java/com/package/\n        data/\n          repository/\n          datasource/\n          models/\n        domain/\n          usecases/\n          models/\n          repository/\n        presentation/\n          screens/\n          components/\n          theme/\n          viewmodels/\n        di/\n        utils/\n      res/\n        values/\n        drawable/\n        mipmap/\n    test/\n    androidTest/\n`;\n\n// Compose UI Guidelines\n\nconst composeGuidelines = `\n1. Use remember and derivedStateOf appropriately\n2. Implement proper recomposition optimization\n3. Use proper Compose modifiers ordering\n4. Follow composable function naming conventions\n5. Implement proper preview annotations\n6. Use proper state management with MutableState\n7. Implement proper error handling and loading states\n8. Use proper theming with MaterialTheme\n9. Follow accessibility guidelines\n10. Implement proper animation patterns\n`;\n\n// Testing Guidelines\n\nconst testingGuidelines = `\n1. Write unit tests for ViewModels and UseCases\n2. Implement UI tests using Compose testing framework\n3. Use fake repositories for testing\n4. Implement proper test coverage\n5. Use proper testing coroutine dispatchers\n`;\n\n// Performance Guidelines\n\nconst performanceGuidelines = `\n1. Minimize recomposition using proper keys\n2. Use proper lazy loading with LazyColumn and LazyRow\n3. Implement efficient image loading\n4. Use proper state management to prevent unnecessary updates\n5. Follow proper lifecycle awareness\n6. Implement proper memory management\n7. Use proper background processing\n`;\n"},"items":[{"name":"CLAUDE.md","path":".claude/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/CLAUDE.md","title":".claude - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# Bitwarden Android - Claude Code Configuration\n\nOfficial Android application for Bitwarden Password Manager and Bitwarden Authenticator, providing secure password management, two-factor authentication, and credential autofill services with zero-knowledge encryption.\n\n## Overview\n\n- Multi-module Android application: `:app` (Password Manager), `:authenticator` (2FA TOTP generator)\n- Zero-knowledge architecture: encryption/decryption happens client-side via Bitwarden SDK\n- Target users: End-users via Google Play Store and F-Droid\n\n### Key Concepts\n\n- **Zero-Knowledge Architecture**: Server never has access to unencrypted vault data or encryption keys\n- **Bitwarden SDK**: Rust-based cryptographic SDK handling all encryption/decryption operations\n- **DataState**: Wrapper for streaming data states (Loading, Loaded, Pending, Error, NoNetwork)\n- **Result Types**: Custom sealed classes for operation results (never throw exceptions from data layer)\n- **UDF (Unidirectional Data Flow)**: State flows down, actions flow up through ViewModels\n\n---\n\n## Architecture\n\n```\nUser Request (UI Action)\n         |\n    Screen (Compose)\n         |\n    ViewModel (State/Action/Event)\n         |\n    Repository (Business Logic)\n         |\n    +----+----+----+\n    |    |    |    |\n  Disk  Network  SDK\n   |      |      |\n Room  Retrofit  Bitwarden\n  DB    APIs     Rust SDK\n```\n\n### Key Principles\n\n1. **No Exceptions from Data Layer**: All suspending functions return `Result<T>` or custom sealed classes\n2. **State Hoisting to ViewModel**: All state that affects behavior must live in the ViewModel's state\n3. **Interface-Based DI**: All implementations use interface/`...Impl` pairs with Hilt injection\n4. **Encryption by Default**: All sensitive data encrypted via SDK before storage\n\n### Core Patterns\n\n- **BaseViewModel**: Enforces UDF with State/Action/Event pattern. See `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt`.\n- **Repository Result Pattern**: Type-safe error handling using custom sealed classes for discrete operations and `DataState<T>` wrapper for streaming data.\n- **Common Patterns**: Flow collection via `Internal` actions, error handling via `when` branches, `DataState` streaming with `.map { }` and `.stateIn()`.\n\n> For complete architecture patterns, code templates, and module organization, see `docs/ARCHITECTURE.md`.\n\n---\n\n## Development Guide\n\n### Workflow Skills\n\n> **Quick start**: Use the `bitwarden-tech-lead:bitwarden-tech-lead` agent (or `/plan-android-work <task>`) to refine\n> requirements and plan,\n> then the `bitwarden-software-engineer:bitwarden-software-engineer` agent (or `/work-on-android <task>`) for implementation,\n> then `/review-android <PR#>` to review the result.\n\n## Skills & Commands\n\n| Skill | Triggers |\n|-------|---------|\n| `build-test-verify` | \"build\", \"run tests\", \"lint\", \"format\", \"verify build\" |\n| `implementing-android-code` | \"implement\", \"write code\", \"add screen\", \"create feature\" |\n| `planning-android-implementation` | \"plan implementation\", \"architecture design\", \"phased task breakdown\" |\n| `refining-android-requirements` | \"refine requirements\", \"analyze ticket\", \"gap analysis\" |\n| `reviewing-changes` | \"review\", \"code review\", \"check PR\" |\n| `testing-android-code` | \"write tests\", \"add test coverage\", \"unit test\" |\n\n| Command | Usage |\n|---------|-------|\n| `/plan-android-work <task>` | Fetch ticket → refine requirements → design implementation approach |\n| `/work-on-android <task>` | Full workflow: implement → test → verify → preflight → commit → review → PR |\n| `/review-android <PR#>` | Full review workflow: PR context gathering → Android checklist → output |\n\n---\n\n## Security Rules\n\n**MANDATORY - These rules have no exceptions:**\n\n1. **Zero-Knowledge Architecture**: Never transmit unencrypted vault data or master passwords to the server. All encryption happens client-side via the Bitwarden SDK.\n2. **No Plaintext Key Storage**: Encryption keys must be stored using Android Keystore (biometric unlock) or encrypted with PIN/master password.\n3. **Sensitive Data Cleanup**: On logout, all sensitive data must be cleared from memory and storage via `UserLogoutManager.logout()`.\n4. **Input Validation**: Validate all user inputs before processing, especially URLs and credentials.\n5. **SDK Isolation**: Use scoped SDK sources (`ScopedVaultSdkSource`) to prevent cross-user crypto context leakage.\n\n---\n\n## Code Style & Standards\n\n- **Formatter**: Android Studio with `bitwarden-style.xml` | **Line Limit**: 100 chars | **Detekt**: Enabled\n- **Naming**: `camelCase` (vars/fns), `PascalCase` (classes), `SCREAMING_SNAKE_CASE` (constants), `...Impl` (implementations)\n- **KDoc**: Required for all public APIs\n- **String Resources**: Add new strings to `:ui` module (`ui/src/main/res/values/strings.xml`). Use typographic quotes/apostrophes (`\"` `\"` `'`) not escaped ASCII (`\\\"` `\\'`). Name each resource from its own text content in `snake_case` — not with generic suffixes (`_message`, `_title`). E.g., `one_or_more_email_addresses_are_incorrect`, not `invalid_email_addresses_message`.\n\n> For complete style rules (imports, formatting, documentation, Compose conventions), see `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n---\n\n## Anti-Patterns\n\nIn addition to the Key Principles above, follow these rules:\n\n### DO\n- Map async results to internal actions before updating state\n- Inject `Clock` for time-dependent operations\n- Return early to reduce nesting\n\n### DON'T\n- Update state directly inside coroutines (use internal actions)\n- Use `any` types or suppress null safety\n- Catch generic `Exception` (catch specific types)\n- Use `e.printStackTrace()` (use Timber logging)\n- Create new patterns when established ones exist\n- Skip KDoc for public APIs\n\n---\n\n## Quick Reference\n\n- **Code style**: Full rules: `docs/STYLE_AND_BEST_PRACTICES.md`\n- **Before writing code**: Use `implementing-android-code` skill for Bitwarden-specific patterns, gotchas, and templates\n- **Before writing tests**: Use `testing-android-code` skill for test patterns and templates\n- **Building/testing**: Use `build-test-verify` skill | App tests: `./gradlew app:testStandardDebugUnitTest`\n- **Before committing**: Use `bitwarden-delivery-tools:perform-preflight` skill, then `bitwarden-delivery-tools:committing-changes` skill for message format\n- **Code review**: Use `/review-android` for the full review workflow; `reviewing-changes` skill for checklist-only\n- **Creating PRs**: Use `bitwarden-delivery-tools:creating-pull-request` skill for PR workflow and templates\n- **Troubleshooting**: See `docs/TROUBLESHOOTING.md`\n- **Architecture**: `docs/ARCHITECTURE.md` | [Bitwarden SDK](https://github.com/bitwarden/sdk) | [Jetpack Compose](https://developer.android.com/jetpack/compose) | [Hilt DI](https://dagger.dev/hilt/)\n","isInternal":false,"tokens":1595,"sizeBytes":6819},{"name":"SKILL.md","path":".claude/skills/build-test-verify/SKILL.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/build-test-verify/SKILL.md","title":"build-test-verify","category":"anthropic-skill","format":"markdown","content":"---\nname: build-test-verify\nversion: 0.1.0\ndescription: Build, test, lint, and deploy commands for the Bitwarden Android project. Use when running tests, building APKs/AABs, running lint/detekt, deploying, using fastlane, or discovering codebase structure. Triggered by \"run tests\", \"build\", \"gradle\", \"lint\", \"detekt\", \"deploy\", \"fastlane\", \"assemble\", \"verify\", \"coverage\".\n---\n\n# Build, Test & Verify\n\n## Environment Setup\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `GITHUB_TOKEN` | Yes (CI) | GitHub Packages auth for SDK (`read:packages` scope) |\n| Build flavors | - | `standard` (Play Store), `fdroid` (no Google services) |\n| Build types | - | `debug`, `beta`, `release` |\n\nIf builds fail resolving the Bitwarden SDK, verify `GITHUB_TOKEN` in `user.properties` or environment and check connectivity to `maven.pkg.github.com`.\n\n---\n\n## Building\n\n```bash\n# Debug builds\n./gradlew app:assembleDebug\n./gradlew authenticator:assembleDebug\n\n# Release builds (requires signing keys)\n./gradlew app:assembleStandardRelease\n./gradlew app:bundleStandardRelease\n\n# F-Droid builds\n./gradlew app:assembleFdroidRelease\n```\n\n---\n\n## Running Tests\n\n**IMPORTANT**: The app module uses the `standard` flavor. Always use `testStandardDebugUnitTest`, NOT `testDebugUnitTest`.\n\n**IMPORTANT**: Always pipe test output through a filter that captures failures on the first run. Gradle suppresses detailed failure output by default, so use `2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30` to see pass/fail results and assertion details without needing a second run.\n\n```bash\n# App module tests (correct flavor!)\n./gradlew app:testStandardDebugUnitTest 2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30\n\n# Run specific test classes\n./gradlew app:testStandardDebugUnitTest --tests \"com.x8bit.bitwarden.SomeTest\" 2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30\n\n# Run all unit tests across all modules\n./gradlew test\n\n# Individual shared modules (no flavor needed)\n./gradlew :core:test\n./gradlew :data:test\n./gradlew :network:test\n./gradlew :ui:test\n\n# Authenticator module\n./gradlew authenticator:testStandardDebugUnitTest\n```\n\n### Reading Test Reports\n\nIf you need full failure details beyond what grep captures, check the HTML test report:\n\n```bash\n# After a test run, open the report at:\n# app/build/reports/tests/testStandardDebugUnitTest/index.html\n# Or read individual failure XML:\nfind app/build/test-results -name \"*.xml\" -exec grep -l \"failure\" {} \\;\n```\n\n### Test Structure\n\n```\napp/src/test/                    # App unit tests\napp/src/testFixtures/            # App test utilities\ncore/src/testFixtures/           # Core test utilities (FakeDispatcherManager)\ndata/src/testFixtures/           # Data test utilities (FakeSharedPreferences)\nnetwork/src/testFixtures/        # Network test utilities (BaseServiceTest)\nui/src/testFixtures/             # UI test utilities (BaseViewModelTest, BaseComposeTest)\n```\n\n### Test Quick Reference\n\n- **Dispatcher Control**: `FakeDispatcherManager` from `:core:testFixtures`\n- **MockK**: `mockk<T> { every { } returns }`, `coEvery { }` for suspend\n- **Flow Testing**: Turbine with `stateEventFlow()` helper from `BaseViewModelTest`\n- **Time Control**: Inject `Clock` for deterministic time testing\n\n---\n\n## Lint & Static Analysis\n\n**IMPORTANT**: Prefer running detekt on modified files only — a full project scan is slow and unnecessary during development. The project supports a `-Pprecommit=true` flag that limits detekt to staged files.\n\n**IMPORTANT**: Always pipe detekt output through a filter to capture errors on the first run. Detekt prints violation details to stderr/stdout but Gradle can obscure them. Use the grep pattern below to see violations immediately.\n\n```bash\n# Detekt on staged files only (preferred during development)\ngit add -u && ./gradlew -Pprecommit=true detekt 2>&1 | grep -E \"FAILED|BUILD|Line |Rule |Signature|detekt\" | head -40\n\n# Detekt on all files (full scan, use sparingly)\n./gradlew detekt 2>&1 | grep -E \"FAILED|BUILD|Line |Rule |Signature|detekt\" | head -40\n\n# Android Lint\n./gradlew lint\n\n# Full validation suite (detekt + lint + tests + coverage)\n./fastlane check\n```\n\n### How `-Pprecommit=true` Works\n\nThe root `build.gradle.kts` configures detekt tasks to use `git diff --name-only --cached` when this property is set, limiting analysis to staged files only. This is the same mechanism used by the project's pre-commit hook. Stage your changes with `git add` before running.\n\n---\n\n## Codebase Discovery\n\n```bash\n# Find existing Bitwarden UI components\nfind ui/src/main/kotlin/com/bitwarden/ui/platform/components/ -name \"Bitwarden*.kt\" | sort\n\n# Find all ViewModels\ngrep -rl \"BaseViewModel<\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Find all Navigation files with @Serializable routes\nfind app/src/main/kotlin/ -name \"*Navigation.kt\" | sort\n\n# Find all Hilt modules\nfind app/src/main/kotlin/ -name \"*Module.kt\" -path \"*/di/*\" | sort\n\n# Find all repository interfaces\nfind app/src/main/kotlin/ -name \"*Repository.kt\" -not -name \"*Impl.kt\" -path \"*/repository/*\" | sort\n\n# Find encrypted disk source examples\ngrep -rl \"EncryptedPreferences\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Find Clock injection usage\ngrep -rl \"private val clock: Clock\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Search existing strings before adding new ones\ngrep -n \"search_term\" ui/src/main/res/values/strings.xml\n```\n\n---\n\n## Deployment & Versioning\n\n**Version location**: `gradle/libs.versions.toml`\n```toml\nappVersionCode = \"1\"\nappVersionName = \"2025.11.1\"\n```\nPattern: `YEAR.MONTH.PATCH`\n\n**Publishing channels**:\n- **Play Store**: GitHub Actions workflow with signed AAB\n- **F-Droid**: Dedicated workflow with F-Droid signing keys\n- **Firebase App Distribution**: Beta testing\n","frontmatter":{"name":"build-test-verify","version":"0.1.0","description":"Build, test, lint, and deploy commands for the Bitwarden Android project. Use when running tests, building APKs/AABs, running lint/detekt, deploying, using fastlane, or discovering codebase structure. Triggered by \"run tests\", \"build\", \"gradle\", \"lint\", \"detekt\", \"deploy\", \"fastlane\", \"assemble\", \"verify\", \"coverage\"."},"isInternal":false,"tokens":1484,"sizeBytes":5868},{"name":"SKILL.md","path":".claude/skills/evaluating-sdk-internal-updates/SKILL.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/evaluating-sdk-internal-updates/SKILL.md","title":"evaluating-sdk-internal-updates","category":"anthropic-skill","format":"markdown","content":"---\nname: evaluating-sdk-internal-updates\ndescription: Evaluates a bitwarden/android \"Update SDK to\" PR against the sdk-internal commit range for compile-time and runtime breaking changes, maps affected symbols to Android call sites, and applies clear in-scope fixes. Use when reviewing an SDK bump PR, a bitwardenSdk version change, or triaging sdk-internal breaking changes.\nallowed-tools:\n  - Bash(gh pr diff:*)\n  - Bash(git -C * log *)\n  - Bash(git -C * show *)\n  - Bash(git add:*)\n  - Bash(git commit:*)\n  - Bash(grep:*)\n  - Bash(./gradlew *)\n  - Read\n  - Grep\n  - Glob\n  - Edit\n  - Write\n  - Skill(implementing-android-code)\n  - Skill(bitwarden-delivery-tools:committing-changes)\n---\n\n# Evaluating sdk-internal Updates\n\n**Identify both compile-time and runtime breaks before fixing anything — fixing the first break found is not finishing.** Steps 3-7 always cover the entire commit range before step 8 starts, no matter how obvious or urgent an early compile-time break looks.\n\n## Identify\n\nBinding surface facts specific to this SDK: `#[uniffi::export]` / `derive(uniffi::...)` annotations are scattered across many crates, not just `crates/bitwarden-uniffi`; `crates/bitwarden-ffi` is unrelated (\"do not use\"). No `.udl` files. UniFFI emits one Kotlin package per crate (`com.bitwarden.core`, `com.bitwarden.vault`, `com.bitwarden.crypto`, etc.) — only the top-level `Client`/`AuthClient`/`GeneratorClients` actually live under `com.bitwarden.sdk`. Both `app` and `authenticator` depend on the SDK; neither is optional to search.\n\nA hunk that only touches a macro invocation (e.g. `state_bridge! { ... }`) doesn't show the binding surface — the expansion lives in the macro's definition, often in a different crate (e.g. `bitwarden-state-bridge-macro`). `#[uniffi::export(with_foreign)]` marks a callback interface: a trait Kotlin must implement, where adding a field/method is never additive-safe for the implementor.\n\n1. Locate the local `bitwarden/sdk-internal` clone (check sibling directories to this repo). If none exists, stop and tell the user it's a required prerequisite for this skill — do not clone it yourself.\n2. `gh pr diff <PR> -R bitwarden/android | grep bitwardenSdk` → old/new `bitwardenSdk` string. Everything after the second `-` is the git ref — a commit SHA or a branch name (`.dev` SDK builds use both); resolve a branch name as `origin/<branch>` in the clone.\n3. Attempt `./gradlew app:compileStandardDebugKotlin authenticator:compileDebugKotlin` at the current checkout before crawling sdk-internal. A failure confirms a compile-time break directly, with a more precise location than any git search — note it and continue to steps 4-7 for the full commit range; do not fix it yet. A clean build only rules out compile-time breaks, not runtime ones.\n4. `git -C <sdk-internal-path> log --oneline OLD..NEW -G'uniffi::export|derive\\(uniffi|#\\[uniffi' -- '*.rs'` → candidate binding-surface commits.\n5. Classify per hunk, not per commit — a commit with one additive headline change can still have a second, unrelated breaking hunk. If a hunk only touches a macro invocation, read the macro's definition before classifying. `git -C <sdk-internal-path> show <sha> -- '*.rs'`.\n6. For every distinct symbol/type touched (every hunk, not just the commit's headline change), grep the whole repo for the bare symbol name to find Android call sites — a fixed module list or a `com.bitwarden.sdk.<Symbol>` import-prefix check both miss real consumers.\n7. Report: compile-time breaks, runtime breaks, safe/no-call-site — each with commit, symbol, and call sites. Cite sdk-internal commits and PRs as `bitwarden/sdk-internal#<NNN>` or a full commit URL/SHA — never a bare `#<NNN>` copied from a commit subject line. A bare reference auto-links within whatever repo the report is posted to (e.g. `bitwarden/android`), tagging an unrelated issue or PR there.\n\n## Resolve\n\nResolve the findings from Step 7 by deciding on a fix (step 8), implementing and committing it directly (step 9), then verifying (step 10).\n\n8. Decide the fix for anything found, compile-time or runtime, whenever the correct behavior is clear and within scope. For a new required method, grep for the underlying concept, not the new method/type name (it won't exist yet) — no existing consumer means stub it: a `// no-op` comment or a null/default return that satisfies the compiler, not a behavioral decision. Never `TODO()` — it throws at runtime, which is a crash, not a stub. A sibling's structure (naming, placement, style) is a template; its behavior (storage, defaulting, error handling, side effects) is not evidence for yours. If unsure, report it instead of guessing, along with anything needing a product decision.\n9. Implement every fix from step 8 directly: invoke `Skill(implementing-android-code)` first if the fix isn't purely mechanical, then commit with `Skill(bitwarden-delivery-tools:committing-changes)`.\n10. Verify with the same compile task used in step 3.\n","frontmatter":{"name":"evaluating-sdk-internal-updates","description":"Evaluates a bitwarden/android \"Update SDK to\" PR against the sdk-internal commit range for compile-time and runtime breaking changes, maps affected symbols to Android call sites, and applies clear in-scope fixes. Use when reviewing an SDK bump PR, a bitwardenSdk version change, or triaging sdk-internal breaking changes.","allowed-tools":["Bash(gh pr diff:*)","Bash(git -C * log *)","Bash(git -C * show *)","Bash(git add:*)","Bash(git commit:*)","Bash(grep:*)","Bash(./gradlew *)","Read","Grep","Glob","Edit","Write","Skill(implementing-android-code)","Skill(bitwarden-delivery-tools:committing-changes)"],"allowedTools":["Bash(gh pr diff:*)","Bash(git -C * log *)","Bash(git -C * show *)","Bash(git add:*)","Bash(git commit:*)","Bash(grep:*)","Bash(./gradlew *)","Read","Grep","Glob","Edit","Write","Skill(implementing-android-code)","Skill(bitwarden-delivery-tools:committing-changes)"]},"allowedTools":["Bash(gh pr diff:*)","Bash(git -C * log *)","Bash(git -C * show *)","Bash(git add:*)","Bash(git commit:*)","Bash(grep:*)","Bash(./gradlew *)","Read","Grep","Glob","Edit","Write","Skill(implementing-android-code)","Skill(bitwarden-delivery-tools:committing-changes)"],"isInternal":false,"tokens":1232,"sizeBytes":4997},{"name":"SKILL.md","path":".claude/skills/implementing-android-code/SKILL.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/implementing-android-code/SKILL.md","title":"implementing-android-code","category":"anthropic-skill","format":"markdown","content":"---\nname: implementing-android-code\nversion: 0.1.4\ndescription: This skill should be used when implementing Android code in Bitwarden. Covers critical patterns, gotchas, and anti-patterns unique to this codebase. Triggered by \"How do I implement a ViewModel?\", \"Create a new screen\", \"Add navigation\", \"Write a repository\", \"BaseViewModel pattern\", \"State-Action-Event\", \"type-safe navigation\", \"@Serializable route\", \"SavedStateHandle persistence\", \"process death recovery\", \"handleAction\", \"sendAction\", \"Hilt module\", \"Repository pattern\", \"implementing a screen\", \"adding a data source\", \"handling navigation\", \"encrypted storage\", \"security patterns\", \"Clock injection\", \"DataState\", or any questions about implementing features, screens, ViewModels, data sources, or navigation in the Bitwarden Android app.\n---\n\n# Implementing Android Code - Bitwarden Quick Reference\n\n**This skill provides tactical guidance for Bitwarden-specific patterns.** For comprehensive architecture decisions and complete code style rules, consult `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n---\n\n## Critical Patterns Reference\n\n### A. ViewModel Implementation (State-Action-Event Pattern)\n\nAll ViewModels follow the **State-Action-Event (SAE)** pattern via `BaseViewModel<State, Event, Action>`.\n\n**Key Requirements:**\n- Annotate with `@HiltViewModel`\n- State class MUST be `@Parcelize data class : Parcelable`\n- Implement `handleAction(action: A)` - MUST be synchronous\n- Post internal actions from coroutines using `sendAction()`\n- Save/restore state via `SavedStateHandle[KEY_STATE]`\n- Private action handlers: `private fun handle*` naming convention\n\n**Template**: See [ViewModel template](templates.md#viewmodel-template-state-action-event-pattern)\n\n**Pattern Summary:**\n```kotlin\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val repository: ExampleRepository,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    initialState = savedStateHandle[KEY_STATE] ?: ExampleState(),\n) {\n    init {\n        stateFlow.onEach { savedStateHandle[KEY_STATE] = it }.launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        // Synchronous dispatch only\n        when (action) {\n            is Action.Click -> handleClick()\n            is Action.Internal.DataReceived -> handleDataReceived(action)\n        }\n    }\n\n    private fun handleClick() {\n        viewModelScope.launch {\n            val result = repository.fetchData()\n            sendAction(Action.Internal.DataReceived(result))  // Post internal action\n        }\n    }\n\n    private fun handleDataReceived(action: Action.Internal.DataReceived) {\n        mutableStateFlow.update { it.copy(data = action.result) }\n    }\n}\n```\n\n**Reference:**\n- `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)\n- `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt` (see class declaration)\n\n**Critical Gotchas:**\n- ❌ **NEVER** update `mutableStateFlow` directly inside coroutines\n- ✅ **ALWAYS** post internal actions from coroutines, update state in `handleAction()`\n- ❌ **NEVER** forget `@IgnoredOnParcel` for sensitive data (causes security leak)\n- ✅ **ALWAYS** use `@Parcelize` on state classes for process death recovery\n- ✅ State restoration happens automatically if properly saved to `SavedStateHandle`\n\n---\n\n### B. Navigation Implementation (Type-Safe)\n\nAll navigation uses **type-safe routes** with kotlinx.serialization.\n\n**Pattern Structure:**\n1. `@Serializable` route data class with parameters\n2. `...Args` helper class for extracting from `SavedStateHandle`\n3. `NavGraphBuilder.{screen}Destination()` extension for adding screen to graph\n4. `NavController.navigateTo{Screen}()` extension for navigation calls\n\n**Template**: See [Navigation template](templates.md#navigation-template-type-safe-routes)\n\n**Pattern Summary:**\n```kotlin\n@Serializable\ndata class ExampleRoute(val userId: String, val isEditMode: Boolean = false)\n\ndata class ExampleArgs(val userId: String, val isEditMode: Boolean)\n\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(userId = route.userId, isEditMode = route.isEditMode)\n}\n\nfun NavController.navigateToExample(\n    userId: String,\n    isEditMode: Boolean = false,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(route = ExampleRoute(userId, isEditMode), navOptions = navOptions)\n}\n\nfun NavGraphBuilder.exampleDestination(onNavigateBack: () -> Unit) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(onNavigateBack = onNavigateBack)\n    }\n}\n```\n\n**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt` (see `LoginRoute` and extensions)\n\n**Key Benefits:**\n- ✅ Type safety: Compile-time errors for missing parameters\n- ✅ No string literals in navigation code\n- ✅ Automatic serialization/deserialization\n- ✅ Clear contract for screen dependencies\n\n---\n\n### C. Screen/Compose Implementation\n\nAll screens follow consistent Compose patterns.\n\n**Template**: See [Screen/Compose template](templates.md#screencompose-template)\n\n**Key Patterns:**\n```kotlin\n@Composable\nfun ExampleScreen(\n    onNavigateBack: () -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            ExampleEvent.NavigateBack -> onNavigateBack()\n        }\n    }\n\n    BitwardenScaffold(\n        topBar = {\n            BitwardenTopAppBar(\n                title = stringResource(R.string.title),\n                navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_back),\n                onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },\n            )\n        },\n    ) {\n        // UI content\n    }\n}\n```\n\n**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt` (see `LoginScreen` composable)\n\n**Essential Requirements:**\n- ✅ Use `hiltViewModel()` for dependency injection\n- ✅ Use `collectAsStateWithLifecycle()` for state (not `collectAsState()`)\n- ✅ Use `EventsEffect(viewModel)` for one-shot events\n- ✅ Use `Bitwarden*` prefixed components from `:ui` module\n\n**State Hoisting Rules:**\n- **ViewModel state**: Data that needs to survive process death or affects business logic\n- **UI-only state**: Temporary UI state (scroll position, text field focus) using `remember` or `rememberSaveable`\n\n---\n\n### D. Data Layer Implementation\n\nThe data layer follows strict patterns for repositories, managers, and data sources.\n\n**Interface + Implementation Separation (ALWAYS)**\n\n**Template**: See [Data Layer template](templates.md#data-layer-template-repository--hilt-module)\n\n**Pattern Summary:**\n```kotlin\n// Interface (injected via Hilt)\ninterface ExampleRepository {\n    suspend fun fetchData(id: String): ExampleResult\n    val dataFlow: StateFlow<DataState<ExampleData>>\n}\n\n// Implementation (NOT directly injected)\nclass ExampleRepositoryImpl(\n    private val exampleDiskSource: ExampleDiskSource,\n    private val exampleService: ExampleService,\n) : ExampleRepository {\n    override suspend fun fetchData(id: String): ExampleResult {\n        // NO exceptions thrown - return Result or sealed class\n        return exampleService.getData(id).fold(\n            onSuccess = { ExampleResult.Success(it.toModel()) },\n            onFailure = { ExampleResult.Error(it.message) },\n        )\n    }\n}\n\n// Sealed result class (domain-specific)\nsealed class ExampleResult {\n    data class Success(val data: ExampleData) : ExampleResult()\n    data class Error(val message: String?) : ExampleResult()\n}\n\n// Hilt Module\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleRepositoryModule {\n    @Provides\n    @Singleton\n    fun provideExampleRepository(\n        exampleDiskSource: ExampleDiskSource,\n        exampleService: ExampleService,\n    ): ExampleRepository = ExampleRepositoryImpl(exampleDiskSource, exampleService)\n}\n```\n\n**Reference:**\n- `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt`\n- `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`\n\n**Three-Layer Data Architecture:**\n1. **Data Sources** - Raw data access (network, disk, SDK). Return `Result<T>`, never throw.\n2. **Managers** - Single responsibility business logic. Wrap OS/external services.\n3. **Repositories** - Aggregate sources/managers. Return domain-specific sealed classes.\n\n**Critical Rules:**\n- ❌ **NEVER** throw exceptions in data layer\n- ✅ **ALWAYS** use interface + `...Impl` pattern\n- ✅ **ALWAYS** inject interfaces, never implementations\n- ✅ Data sources return `Result<T>`, repositories return domain sealed classes\n- ✅ Use `StateFlow` for continuously observed data\n\n**KDoc on Interfaces vs. Implementations**\n\nKDoc on an interface member describes the **contract** — what the caller can rely on. It must not describe **how** the implementation fulfills that contract. Implementation details (which data source is consulted, what is cached, what is logged, which manager is delegated to, ordering of internal calls, etc.) belong on the `...Impl` member — and only when the *why* is non-obvious from the code itself.\n\nThis applies to every interface in the codebase: repositories, managers, data sources, validators, and UI-layer interfaces alike. The rule is the same one CLAUDE.md states for comments in general — describe the contract or the non-obvious *why*, never the *what* a well-named identifier already conveys.\n\n❌ **Wrong** — interface KDoc leaks the implementation:\n```kotlin\ninterface ExampleRepository {\n    /**\n     * Fetches data by reading from [ExampleDiskSource] first, falling back to\n     * [ExampleService] on cache miss and writing the network result back to disk.\n     */\n    suspend fun fetchData(id: String): ExampleResult\n}\n```\n\n✅ **Right** — interface KDoc describes the contract; implementation details (if needed at all) live on the override:\n```kotlin\ninterface ExampleRepository {\n    /** Returns the [ExampleData] for [id], or an error result on failure. */\n    suspend fun fetchData(id: String): ExampleResult\n}\n\nclass ExampleRepositoryImpl(...) : ExampleRepository {\n    // No KDoc needed — the disk-then-network pattern is visible in the code.\n    override suspend fun fetchData(id: String): ExampleResult { ... }\n}\n```\n\nRed flags that an interface KDoc has drifted into implementation territory:\n- Names a concrete collaborator (`...DiskSource`, `...Service`, `...Manager`, `...Impl`)\n- Describes ordering of internal calls (\"first ... then ...\", \"falls back to ...\")\n- Mentions caching, retries, logging, or threading behavior that callers don't depend on\n- Restates the method signature in prose (\"Suspends until X returns Y\")\n\nIf callers genuinely depend on a behavior (e.g., \"this method is safe to call before vault unlock\", \"result is cached for the session\"), that *is* part of the contract and belongs on the interface. The test: would a different valid implementation be free to change this? If yes, it's implementation detail — strip it.\n\n---\n\n### E. UI Components\n\n**Use Existing Components First:**\n\nThe `:ui` module provides reusable `Bitwarden*` prefixed components. Search before creating new ones.\n\n**Common Components:**\n- `BitwardenFilledButton` - Primary action buttons\n- `BitwardenOutlinedButton` - Secondary action buttons\n- `BitwardenTextField` - Text input fields\n- `BitwardenPasswordField` - Password input with show/hide\n- `BitwardenSwitch` - Toggle switches\n- `BitwardenTopAppBar` - Toolbar/app bar\n- `BitwardenScaffold` - Screen container with scaffold\n- `BitwardenBasicDialog` - Simple dialogs\n- `BitwardenLoadingDialog` - Loading indicators\n\n**Component Discovery:**\nSearch `ui/src/main/kotlin/com/bitwarden/ui/platform/components/` for existing `Bitwarden*` components. For build, test, and codebase discovery commands, use the **`build-test-verify`** skill.\n\n**When to Create New Reusable Components:**\n- Component used in 3+ places\n- Component needs consistent theming across app\n- Component has semantic meaning (accessibility)\n- Component has complex state management\n\n**New Component Requirements:**\n- Prefix with `Bitwarden`\n- Accept themed colors/styles from `BitwardenTheme`\n- Include preview composables for testing\n- Support accessibility (content descriptions, semantics)\n\n**String Resources:**\n\nNew strings belong in the `:ui` module: `ui/src/main/res/values/strings.xml`\n\n- Use typographic apostrophes and quotes to avoid escape characters: `you’ll` not `you\\'ll`, `“word”` not `\\\"word\\\"`\n- Reference strings via generated `BitwardenString` resource IDs\n- Do not add strings to other modules unless explicitly instructed\n\n---\n\n### F. Security Patterns\n\n**Encrypted vs Unencrypted Storage:**\n\n**Template**: See [Security templates](templates.md#security-templates)\n\n**Pattern Summary:**\n```kotlin\nclass ExampleDiskSourceImpl(\n    @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,\n    @UnencryptedPreferences sharedPreferences: SharedPreferences,\n) : BaseEncryptedDiskSource(\n    encryptedSharedPreferences = encryptedSharedPreferences,\n    sharedPreferences = sharedPreferences,\n),\n    ExampleDiskSource {\n    fun storeAuthToken(token: String) {\n        putEncryptedString(KEY_TOKEN, token)  // Sensitive — uses base class method\n    }\n\n    fun storeThemePreference(isDark: Boolean) {\n        putBoolean(KEY_THEME, isDark)  // Non-sensitive — uses base class method\n    }\n}\n```\n\n**Android Keystore (Biometric Keys):**\n- User-scoped encryption keys: `BiometricsEncryptionManager`\n- Keys stored in Android Keystore (hardware-backed when available)\n- Integrity validation on biometric state changes\n\n**Input Validation:**\n```kotlin\n// Validation returns boolean, NEVER throws\ninterface RequestValidator {\n    fun validate(request: Request): Boolean\n}\n\n// Sanitization removes dangerous content\nfun String?.sanitizeTotpUri(issuer: String?, username: String?): String? {\n    if (this.isNullOrBlank()) return null\n    // Sanitize and return safe value\n}\n```\n\n**Security Checklist:**\n- ✅ Use `@EncryptedPreferences` for credentials, keys, tokens\n- ✅ Use `@UnencryptedPreferences` for UI state, preferences\n- ✅ Use `@IgnoredOnParcel` for sensitive ViewModel state\n- ❌ **NEVER** log sensitive data (passwords, tokens, vault items)\n- ✅ Validate all user input before processing\n- ✅ Use Timber for non-sensitive logging only\n\n---\n\n### G. Testing Patterns\n\n**ViewModel Testing:**\n\n**Template**: See [Testing templates](templates.md#testing-templates)\n\n**Pattern Summary:**\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n    private val mockRepository: ExampleRepository = mockk()\n\n    @Test\n    fun `ButtonClick should fetch data and update state`() = runTest {\n        val expectedResult = ExampleResult.Success(data = \"test\")\n        coEvery { mockRepository.fetchData(any()) } returns expectedResult\n\n        val viewModel = createViewModel()\n        viewModel.trySendAction(ExampleAction.ButtonClick)\n\n        viewModel.stateFlow.test {\n            assertEquals(EXPECTED_STATE.copy(data = \"test\"), awaitItem())\n        }\n    }\n\n    private fun createViewModel(): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to EXPECTED_STATE)),\n        repository = mockRepository,\n    )\n}\n```\n\n**Reference:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n**Key Testing Patterns:**\n- ✅ Extend `BaseViewModelTest` for proper dispatcher management\n- ✅ Use `runTest` from `kotlinx.coroutines.test`\n- ✅ Use Turbine's `.test { awaitItem() }` for Flow assertions\n- ✅ Use MockK: `coEvery` for suspend functions, `every` for sync\n- ✅ Test both state changes and event emissions\n- ✅ Test both success and failure Result paths\n\n**Flow Testing with Turbine:**\n```kotlin\n// Test state and events simultaneously\nviewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n    viewModel.trySendAction(ExampleAction.Submit)\n    assertEquals(ExpectedState.Loading, stateFlow.awaitItem())\n    assertEquals(ExampleEvent.ShowSuccess, eventFlow.awaitItem())\n}\n```\n\n**MockK Quick Reference:**\n```kotlin\ncoEvery { repository.fetchData(any()) } returns Result.success(\"data\")  // Suspend\nevery { diskSource.getData() } returns \"cached\"  // Sync\ncoVerify { repository.fetchData(\"123\") }  // Verify\n```\n\n---\n\n### H. Clock/Time Handling\n\nAll code needing current time must inject `Clock` for testability.\n\n**Key Requirements:**\n- ✅ Inject `Clock` via Hilt in ViewModels\n- ✅ Pass `Clock` as parameter in extension functions\n- ✅ Use `clock.instant()` to get current time\n- ❌ Never call `Instant.now()` or `DateTime.now()` directly\n- ❌ Never use `mockkStatic` for datetime classes in tests\n\n**Pattern Summary:**\n```kotlin\n// ViewModel with Clock\nclass MyViewModel @Inject constructor(\n    private val clock: Clock,\n) {\n    val timestamp = clock.instant()\n}\n\n// Extension function with Clock parameter\nfun State.getTimestamp(clock: Clock): Instant =\n    existingTime ?: clock.instant()\n\n// Test with fixed clock\nval FIXED_CLOCK = Clock.fixed(\n    Instant.parse(\"2023-10-27T12:00:00Z\"),\n    ZoneOffset.UTC,\n)\n```\n\n**Reference:**\n- `docs/STYLE_AND_BEST_PRACTICES.md` (see Time and Clock Handling section)\n- `core/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt` (see `provideClock` function)\n\n**Critical Gotchas:**\n- ❌ `Instant.now()` creates hidden dependency, non-testable\n- ❌ `mockkStatic(Instant::class)` is fragile, can leak between tests\n- ✅ `Clock.fixed(...)` provides deterministic test behavior\n\n---\n\n### I. Kotlin Style Rules\n\nProject-specific style conventions enforced in code review. These supplement (not replace) `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n**`when` branches with wrapped right-hand side require curly braces.**\n\nWhen a `when` branch's expression is too long to fit on the same line as the arrow and is wrapped to the next line, wrap the body in `{ }`. A bare `->` followed by an indented expression on its own line is rejected in review.\n\n❌ **Wrong** — wrapped body without braces:\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT ->\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    VaultItemCipherType.DRIVERS_LICENSE ->\n        VaultAddEditState.ViewState.Content.ItemType.DriversLicense()\n}\n```\n\n✅ **Right** — wrapped body with braces:\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT -> {\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    }\n    VaultItemCipherType.DRIVERS_LICENSE -> {\n        VaultAddEditState.ViewState.Content.ItemType.DriversLicense()\n    }\n}\n```\n\nSingle-line branches (body fits on the same line as `->`) do **not** need braces.\n\n---\n\n## Bitwarden-Specific Anti-Patterns\n\n**General anti-patterns are documented in CLAUDE.md.** This section covers violations specific to Bitwarden's State-Action-Event, navigation, and data layer patterns:\n\n❌ **NEVER update ViewModel state directly in coroutines**\n- Post internal actions, update state synchronously in `handleAction()`\n\n❌ **NEVER inject `...Impl` classes**\n- Only inject interfaces via Hilt\n\n❌ **NEVER create navigation without `@Serializable` routes**\n- No string-based navigation, always type-safe\n\n❌ **NEVER use raw `Result<T>` in repositories**\n- Use domain-specific sealed classes for better error handling\n\n❌ **NEVER make state classes without `@Parcelize`**\n- All ViewModel state must survive process death\n\n❌ **NEVER skip `SavedStateHandle` persistence for ViewModels**\n- Users lose form progress on process death\n\n❌ **NEVER forget `@IgnoredOnParcel` for passwords/tokens**\n- Causes security vulnerability (sensitive data in parcel)\n\n❌ **NEVER use generic `Exception` catching**\n- Catch specific exceptions only (`RemoteException`, `IOException`)\n\n❌ **NEVER call `Instant.now()` or `DateTime.now()` directly**\n- Inject `Clock` via Hilt, use `clock.instant()` for testability\n\n❌ **NEVER put implementation details in interface KDoc**\n- Interface KDoc describes the contract; concrete collaborators, call ordering, caching, and fallback behavior belong on the `...Impl` override (and only when the *why* is non-obvious). See section D for examples.\n\n---\n\n## Quick Reference\n\nFor build, test, and codebase discovery commands, use the **`build-test-verify`** skill.\n\n**File Reference Format:**\nWhen pointing to specific code, use: `file_path:line_number`\n\nExample: `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)\n","frontmatter":{"name":"implementing-android-code","version":"0.1.4","description":"This skill should be used when implementing Android code in Bitwarden. Covers critical patterns, gotchas, and anti-patterns unique to this codebase. Triggered by \"How do I implement a ViewModel?\", \"Create a new screen\", \"Add navigation\", \"Write a repository\", \"BaseViewModel pattern\", \"State-Action-Event\", \"type-safe navigation\", \"@Serializable route\", \"SavedStateHandle persistence\", \"process death recovery\", \"handleAction\", \"sendAction\", \"Hilt module\", \"Repository pattern\", \"implementing a screen\", \"adding a data source\", \"handling navigation\", \"encrypted storage\", \"security patterns\", \"Clock injection\", \"DataState\", or any questions about implementing features, screens, ViewModels, data sources, or navigation in the Bitwarden Android app."},"isInternal":false,"tokens":4751,"sizeBytes":20973},{"name":"templates.md","path":".claude/skills/implementing-android-code/templates.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/implementing-android-code/templates.md","title":"Implementing-android-code Skill","category":"anthropic-skill","format":"markdown","content":"# Code Templates - Bitwarden Android\n\nCopy-pasteable templates derived from actual codebase patterns. Replace `Example` with your feature name.\n\n---\n\n## ViewModel Template (State-Action-Event Pattern)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt`\n\n### State Class\n\n```kotlin\n@Parcelize\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    @IgnoredOnParcel val sensitiveInput: String = \"\", // Sensitive data excluded from parcel\n    val dialogState: DialogState? = null,\n) : Parcelable {\n\n    /**\n     * Dialog states for the Example screen.\n     */\n    sealed class DialogState : Parcelable {\n        @Parcelize\n        data class Error(\n            val title: Text? = null,\n            val message: Text,\n            val error: Throwable? = null,\n        ) : DialogState()\n\n        @Parcelize\n        data class Loading(val message: Text) : DialogState()\n    }\n}\n```\n\n### Event Sealed Class\n\n```kotlin\n/**\n * One-shot UI events for the Example screen.\n */\nsealed class ExampleEvent {\n    data object NavigateBack : ExampleEvent()\n\n    data class ShowToast(val message: Text) : ExampleEvent()\n}\n```\n\n### Action Sealed Class (with Internal)\n\n```kotlin\n/**\n * User and system actions for the Example screen.\n */\nsealed class ExampleAction {\n    data object BackClick : ExampleAction()\n\n    data object SubmitClick : ExampleAction()\n\n    data class InputChanged(val input: String) : ExampleAction()\n\n    data object ErrorDialogDismiss : ExampleAction()\n\n    /**\n     * Internal actions dispatched by the ViewModel from coroutines.\n     */\n    sealed class Internal : ExampleAction() {\n        data class ReceiveDataState(\n            val dataState: DataState<ExampleData>,\n        ) : Internal()\n\n        data class ReceiveDataResult(\n            val result: ExampleResult,\n        ) : Internal()\n    }\n}\n```\n\n### ViewModel\n\n```kotlin\nprivate const val KEY_STATE = \"state\"\n\n/**\n * ViewModel for the Example screen.\n */\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val exampleRepository: ExampleRepository,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    initialState = savedStateHandle[KEY_STATE]\n        ?: run {\n            val args = savedStateHandle.toExampleArgs()\n            ExampleState(\n                data = args.itemId,\n            )\n        },\n) {\n\n    init {\n        // Persist state for process death recovery\n        stateFlow\n            .onEach { savedStateHandle[KEY_STATE] = it }\n            .launchIn(viewModelScope)\n\n        // Collect repository flows as internal actions\n        exampleRepository.dataFlow\n            .map { ExampleAction.Internal.ReceiveDataState(it) }\n            .onEach(::sendAction)\n            .launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        when (action) {\n            ExampleAction.BackClick -> handleBackClick()\n            ExampleAction.SubmitClick -> handleSubmitClick()\n            ExampleAction.ErrorDialogDismiss -> handleErrorDialogDismiss()\n            is ExampleAction.InputChanged -> handleInputChanged(action)\n            is ExampleAction.Internal.ReceiveDataState -> {\n                handleReceiveDataState(action)\n            }\n            is ExampleAction.Internal.ReceiveDataResult -> {\n                handleReceiveDataResult(action)\n            }\n        }\n    }\n\n    private fun handleBackClick() {\n        sendEvent(ExampleEvent.NavigateBack)\n    }\n\n    private fun handleErrorDialogDismiss() {\n        mutableStateFlow.update { it.copy(dialogState = null) }\n    }\n\n    private fun handleSubmitClick() {\n        viewModelScope.launch {\n            val result = exampleRepository.submitData(state.data.orEmpty())\n            sendAction(ExampleAction.Internal.ReceiveDataResult(result))\n        }\n    }\n\n    private fun handleInputChanged(action: ExampleAction.InputChanged) {\n        mutableStateFlow.update { it.copy(sensitiveInput = action.input) }\n    }\n\n    private fun handleReceiveDataState(\n        action: ExampleAction.Internal.ReceiveDataState,\n    ) {\n        when (action.dataState) {\n            is DataState.Loaded -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        data = action.dataState.data.toString(),\n                    )\n                }\n            }\n\n            is DataState.Loading -> {\n                mutableStateFlow.update { it.copy(isLoading = true) }\n            }\n\n            is DataState.Error -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        dialogState = ExampleState.DialogState.Error(\n                            message = BitwardenString.generic_error_message.asText(),\n                            error = action.dataState.error,\n                        ),\n                    )\n                }\n            }\n\n            else -> Unit\n        }\n    }\n\n    private fun handleReceiveDataResult(\n        action: ExampleAction.Internal.ReceiveDataResult,\n    ) {\n        when (val result = action.result) {\n            is ExampleResult.Success -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        data = result.data,\n                    )\n                }\n            }\n\n            is ExampleResult.Error -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        dialogState = ExampleState.DialogState.Error(\n                            message = result.message?.asText()\n                                ?: BitwardenString.generic_error_message.asText(),\n                        ),\n                    )\n                }\n            }\n        }\n    }\n}\n```\n\n---\n\n## Navigation Template (Type-Safe Routes)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt`\n\n```kotlin\n@file:OmitFromCoverage\n\npackage com.x8bit.bitwarden.ui.feature.example\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.navigation.NavController\nimport androidx.navigation.NavGraphBuilder\nimport androidx.navigation.NavOptions\nimport androidx.navigation.toRoute\nimport com.bitwarden.annotation.OmitFromCoverage\nimport com.bitwarden.ui.platform.base.util.composableWithSlideTransitions\nimport kotlinx.serialization.Serializable\n\n/**\n * Route for the Example screen.\n */\n@Serializable\n@OmitFromCoverage\ndata class ExampleRoute(\n    val itemId: String,\n    val isEditMode: Boolean = false,\n)\n\n/**\n * Args extracted from [SavedStateHandle] for the Example screen.\n */\n@OmitFromCoverage\ndata class ExampleArgs(\n    val itemId: String,\n    val isEditMode: Boolean,\n)\n\n/**\n * Extracts [ExampleArgs] from the [SavedStateHandle].\n */\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(\n        itemId = route.itemId,\n        isEditMode = route.isEditMode,\n    )\n}\n\n/**\n * Navigate to the Example screen.\n */\nfun NavController.navigateToExample(\n    itemId: String,\n    isEditMode: Boolean = false,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(\n        route = ExampleRoute(\n            itemId = itemId,\n            isEditMode = isEditMode,\n        ),\n        navOptions = navOptions,\n    )\n}\n\n/**\n * Add the Example screen destination to the navigation graph.\n */\nfun NavGraphBuilder.exampleDestination(\n    onNavigateBack: () -> Unit,\n) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(\n            onNavigateBack = onNavigateBack,\n        )\n    }\n}\n```\n\n---\n\n## Screen/Compose Template\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt`\n\n```kotlin\npackage com.x8bit.bitwarden.ui.feature.example\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.bitwarden.ui.platform.base.util.EventsEffect\nimport com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar\nimport com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold\nimport com.bitwarden.ui.platform.components.util.rememberVectorPainter\nimport com.bitwarden.ui.platform.resource.BitwardenDrawable\nimport com.bitwarden.ui.platform.resource.BitwardenString\n\n/**\n * The Example screen.\n */\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ExampleScreen(\n    onNavigateBack: () -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            ExampleEvent.NavigateBack -> onNavigateBack()\n            is ExampleEvent.ShowToast -> {\n                // Handle toast\n            }\n        }\n    }\n\n    // Dialogs\n    ExampleDialogs(\n        dialogState = state.dialogState,\n        onDismissRequest = { viewModel.trySendAction(ExampleAction.ErrorDialogDismiss) },\n    )\n\n    val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())\n    BitwardenScaffold(\n        modifier = Modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            BitwardenTopAppBar(\n                title = stringResource(id = BitwardenString.example),\n                scrollBehavior = scrollBehavior,\n                navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),\n                onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },\n            )\n        },\n    ) {\n        ExampleScreenContent(\n            state = state,\n            onInputChanged = { viewModel.trySendAction(ExampleAction.InputChanged(it)) },\n            onSubmitClick = { viewModel.trySendAction(ExampleAction.SubmitClick) },\n            modifier = Modifier\n                .fillMaxSize(),\n        )\n    }\n}\n```\n\n---\n\n## Data Layer Template (Repository + Hilt Module)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`\n\n### Interface\n\n```kotlin\n/**\n * Provides data operations for the Example feature.\n */\ninterface ExampleRepository {\n    /**\n     * Submits data and returns a typed result.\n     */\n    suspend fun submitData(input: String): ExampleResult\n\n    /**\n     * Continuously observed data stream.\n     */\n    val dataFlow: StateFlow<DataState<ExampleData>>\n}\n```\n\n### Sealed Result Class\n\n```kotlin\n/**\n * Domain-specific result for Example operations.\n */\nsealed class ExampleResult {\n    data class Success(val data: String) : ExampleResult()\n    data class Error(val message: String?) : ExampleResult()\n}\n```\n\n### Implementation\n\n```kotlin\n/**\n * Default implementation of [ExampleRepository].\n */\nclass ExampleRepositoryImpl(\n    private val exampleDiskSource: ExampleDiskSource,\n    private val exampleService: ExampleService,\n    private val dispatcherManager: DispatcherManager,\n) : ExampleRepository {\n\n    override val dataFlow: StateFlow<DataState<ExampleData>>\n        get() = // ...\n\n    override suspend fun submitData(input: String): ExampleResult {\n        return exampleService\n            .postData(input)\n            .fold(\n                onSuccess = { ExampleResult.Success(it.toModel()) },\n                onFailure = { ExampleResult.Error(it.message) },\n            )\n    }\n}\n```\n\n### Hilt Module\n\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleRepositoryModule {\n\n    @Provides\n    @Singleton\n    fun provideExampleRepository(\n        exampleDiskSource: ExampleDiskSource,\n        exampleService: ExampleService,\n        dispatcherManager: DispatcherManager,\n    ): ExampleRepository = ExampleRepositoryImpl(\n        exampleDiskSource = exampleDiskSource,\n        exampleService = exampleService,\n        dispatcherManager = dispatcherManager,\n    )\n}\n```\n\n---\n\n## Security Templates\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt` and `AuthDiskSourceImpl.kt`\n\n### Encrypted Disk Source (Module)\n\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleDiskModule {\n\n    @Provides\n    @Singleton\n    fun provideExampleDiskSource(\n        @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,\n        @UnencryptedPreferences sharedPreferences: SharedPreferences,\n        json: Json,\n    ): ExampleDiskSource = ExampleDiskSourceImpl(\n        encryptedSharedPreferences = encryptedSharedPreferences,\n        sharedPreferences = sharedPreferences,\n        json = json,\n    )\n}\n```\n\n### Encrypted Disk Source (Implementation)\n\n```kotlin\n/**\n * Disk source for Example data using encrypted and unencrypted storage.\n */\nclass ExampleDiskSourceImpl(\n    encryptedSharedPreferences: SharedPreferences,\n    sharedPreferences: SharedPreferences,\n    private val json: Json,\n) : BaseEncryptedDiskSource(\n    encryptedSharedPreferences = encryptedSharedPreferences,\n    sharedPreferences = sharedPreferences,\n),\n    ExampleDiskSource {\n\n    private companion object {\n        const val ENCRYPTED_TOKEN_KEY = \"exampleToken\"\n        const val UNENCRYPTED_PREF_KEY = \"examplePreference\"\n    }\n\n    override var authToken: String?\n        get() = getEncryptedString(ENCRYPTED_TOKEN_KEY)\n        set(value) { putEncryptedString(ENCRYPTED_TOKEN_KEY, value) }\n\n    override var uiPreference: Boolean\n        get() = getBoolean(UNENCRYPTED_PREF_KEY) ?: false\n        set(value) { putBoolean(UNENCRYPTED_PREF_KEY, value) }\n}\n```\n\n---\n\n## Testing Templates\n\n**Based on**: `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n### ViewModel Test\n\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n\n    // Mock dependencies\n    private val mockRepository = mockk<ExampleRepository>()\n    private val mutableDataFlow = MutableStateFlow<DataState<ExampleData>>(DataState.Loading)\n\n    @BeforeEach\n    fun setup() {\n        every { mockRepository.dataFlow } returns mutableDataFlow\n    }\n\n    @Test\n    fun `initial state should be correct when there is no saved state`() {\n        val viewModel = createViewModel(state = null)\n        assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)\n    }\n\n    @Test\n    fun `initial state should be correct when there is a saved state`() {\n        val savedState = DEFAULT_STATE.copy(data = \"saved\")\n        val viewModel = createViewModel(state = savedState)\n        assertEquals(savedState, viewModel.stateFlow.value)\n    }\n\n    @Test\n    fun `SubmitClick should call repository and update state on success`() = runTest {\n        val expected = ExampleResult.Success(data = \"result\")\n        coEvery { mockRepository.submitData(any()) } returns expected\n\n        val viewModel = createViewModel()\n        viewModel.stateFlow.test {\n            // Initial state\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n\n            // Updated state after result\n            assertEquals(\n                DEFAULT_STATE.copy(data = \"result\", isLoading = false),\n                awaitItem(),\n            )\n        }\n    }\n\n    @Test\n    fun `SubmitClick should show error dialog on failure`() = runTest {\n        val expected = ExampleResult.Error(message = \"Network error\")\n        coEvery { mockRepository.submitData(any()) } returns expected\n\n        val viewModel = createViewModel()\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n\n            val errorState = awaitItem()\n            assertTrue(errorState.dialogState is ExampleState.DialogState.Error)\n        }\n    }\n\n    @Test\n    fun `BackClick should emit NavigateBack event`() = runTest {\n        val viewModel = createViewModel()\n        viewModel.eventFlow.test {\n            viewModel.trySendAction(ExampleAction.BackClick)\n            assertEquals(ExampleEvent.NavigateBack, awaitItem())\n        }\n    }\n\n    // Helper to create ViewModel with optional saved state\n    private fun createViewModel(\n        state: ExampleState? = DEFAULT_STATE,\n    ): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle(\n            mapOf(KEY_STATE to state),\n        ),\n        exampleRepository = mockRepository,\n    )\n\n    companion object {\n        private val DEFAULT_STATE = ExampleState(\n            isLoading = false,\n            data = null,\n        )\n    }\n}\n```\n\n### Flow Testing with stateEventFlow\n\n```kotlin\n@Test\nfun `SubmitClick should update state and emit event`() = runTest {\n    coEvery { mockRepository.submitData(any()) } returns ExampleResult.Success(\"data\")\n\n    val viewModel = createViewModel()\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        viewModel.trySendAction(ExampleAction.SubmitClick)\n\n        // Assert state change\n        assertEquals(\n            DEFAULT_STATE.copy(data = \"data\"),\n            stateFlow.awaitItem(),\n        )\n\n        // Assert event emission\n        assertEquals(\n            ExampleEvent.ShowToast(\"Success\".asText()),\n            eventFlow.awaitItem(),\n        )\n    }\n}\n```\n","isInternal":false,"tokens":3567,"sizeBytes":17835},{"name":"SKILL.md","path":".claude/skills/interacting-with-android-device/SKILL.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/interacting-with-android-device/SKILL.md","title":"interacting-with-android-device","category":"anthropic-skill","format":"markdown","content":"---\nname: interacting-with-android-device\ndescription: Instructions for capturing UI state, comparing with mocks, and interacting with an Android device using MCP tools backed by ADB.\nallowed-tools: mcp__android-device__capture, mcp__android-device__find_element, mcp__android-device__tap_at, mcp__android-device__tap_element, mcp__android-device__navigate, mcp__android-device__input_text, Bash(adb:*), Bash(sleep:*), Bash(./gradlew install*:*), Read, Glob\n---\n\n# Interacting with Android Device\n\n## Quick Start: MCP Tools\n\nThe `android-device` MCP server provides 6 tools for device interaction. These replace the previous shell scripts with proper XML parsing, structured dumpsys parsing, and native obstruction detection.\n\n**Available tools:**\n- `capture` — Capture UI hierarchy XML and/or screenshot. Params: `{ xml?: boolean, screenshot?: boolean }`. Default: both.\n- `find_element` — Find element by `text` or `content-desc`, return coordinates with **obstruction detection**. Params: `{ text: string }`. Returns JSON with coordinates, bounds, and obstruction status.\n- `tap_at` — Tap at specific coordinates, wait, capture screenshot. Params: `{ x, y, waitSeconds? }`.\n- `tap_element` — Find, tap, and capture in one call (recommended). Params: `{ text, waitSeconds? }`. Auto-adjusts coordinates when obstructed.\n- `navigate` — Navigation actions: home, back, app-drawer. Params: `{ action, waitSeconds? }`. Captures screenshot after action.\n- `input_text` — Type text into the focused field. Params: `{ text, clear? }`. Set `clear: true` to erase existing content first.\n\n**Use these MCP tools instead of raw ADB commands** to save tokens, get structured results, and benefit from automatic obstruction detection.\n\n## 1. Capturing Current State\nTo understand what is currently on the device, use the `capture` tool:\n*   It saves `view.xml` (UI hierarchy) and `screen.png` (screenshot) to the working directory\n*   Read `view.xml` to find coordinates (`bounds`) and properties (like `text` or `resource-id`) of UI elements\n*   Use `screen.png` for visual verification against design mocks\n\n## 2. Interacting with the Device\n\n### Using MCP Tools (Recommended)\n\n*   **Find and tap an element by text** — use `tap_element`:\n    This finds the element, detects obstructions, taps (with adjusted coordinates if needed), and captures a screenshot — all in one call.\n\n*   **Tap at specific coordinates** — use `tap_at`:\n    When you already have coordinates from `find_element` or manual inspection.\n\n*   **Navigate (home, back, app-drawer)** — use `navigate`:\n    Performs the action and captures a screenshot.\n\n*   **Find element without tapping** — use `find_element`:\n    Returns coordinates and full element info. Useful when you need to inspect before acting.\n\n*   **Type text into a field** — use `input_text`:\n    Types text into the currently focused field. Set `clear: true` to erase existing content first. Tap the field before calling this if it isn't already focused.\n\n### Raw ADB Commands (When MCP Tools Aren't Sufficient)\n*   **Key Events**:\n    *   Back: `adb shell input keyevent 4`\n    *   Home: `adb shell input keyevent 3`\n    *   Enter: `adb shell input keyevent 66`\n*   **Scrolling/Swiping**: Use `adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms>` where:\n    *   `(x1, y1)` = starting point\n    *   `(x2, y2)` = ending point\n    *   `duration_ms` = duration in milliseconds (1000ms is typical; adjust for speed/distance)\n    *   **Note**: For expanding containers/drawers, use large distances (e.g., 2400->300 for a 2992px tall screen)\n\n## 3. Obstruction Detection\n\nThe `find_element` and `tap_element` tools automatically detect when another element would intercept the tap. This catches:\n*   **System overlays** (Layer 1): TalkBack floating menu, PiP windows, accessibility services — detected via `dumpsys window windows` touchable regions\n*   **In-app elements** (Layer 2): FABs, dialogs, bottom sheets, snackbars — detected by finding the topmost clickable element at the tap point in the UI hierarchy\n\nWhen obstruction is detected:\n*   Coordinates are **auto-adjusted** to the center of the largest unobstructed strip (top/bottom/left/right of the obstructor)\n*   The response includes the obstructor identity, bounds, and visible region info\n*   If fully obscured (no visible region), the original center is returned as best-effort\n*   **Compose parent wrapper** pattern (identical bounds) is recognized as non-obstruction\n\n## 4. Verification Workflow\nFollow these steps for a complete UI test:\n1.  **Build and Install**: Ensure the latest version of the app is running: `./gradlew installDebug`.\n2.  **Inspect**: Use `capture` to dump the UI hierarchy and take a screenshot.\n3.  **Compare**: Check the current UI against any mock image files in the project.\n4.  **Interact**: Use `tap_element` to tap a UI element by text. The tool handles coordinate calculation and obstruction detection automatically.\n5.  **Verify**: Use `capture` again to confirm the UI has updated as expected (e.g., a new screen is shown, or a success message appeared).\n\n## 5. Examples\n\n### Example: Navigate to Settings and Check for Updates\n```\n# Go to home screen\nnavigate({ action: \"home\" })\n\n# Open app drawer\nnavigate({ action: \"app-drawer\" })\n\n# Find and tap through settings\ntap_element({ text: \"Settings\", waitSeconds: 2 })\ntap_element({ text: \"System\", waitSeconds: 2 })\ntap_element({ text: \"Software updates\", waitSeconds: 2 })\ntap_element({ text: \"Check for update\", waitSeconds: 5 })\n```\n\n### Example: Swiping\nFor swipe gestures not covered by the navigate tool, use raw ADB:\n```bash\nadb shell input swipe 672 2800 672 500 1000 && sleep 1 && adb shell screencap -p /sdcard/screen.png && adb pull /sdcard/screen.png .\n```\n\n## 6. Best Practices\n\n### Coordinate Calculation\n*   Prefer `find_element` or `tap_element` over manual coordinate calculation — they handle bounds parsing, center computation, and obstruction detection automatically\n*   When multiple instances of an element exist (e.g., in prediction row and full list), check the `find_element` response to verify you're targeting the correct one\n\n### Navigation and State Evaluation\n*   **Verify after each interaction**: Don't assume an action succeeded — use `capture` after interactions to confirm the UI changed as expected\n*   **Check both visual and structural state**: Use screenshot for visual verification, XML dump for structural confirmation (element presence, text content, state changes)\n*   **Identify navigation failures early**: If a tap opened the wrong screen, use `navigate({ action: \"back\" })` to recover immediately\n\n### Interaction Patterns\n*   **Scrolling before interaction**: When looking for an element, check if it's visible on screen first. If not, scroll using swipe gestures to reveal it\n*   **Use consistent scroll direction**: For vertical scrolling in lists/settings, use downward swipes (higher Y -> lower Y) to scroll down\n*   **Handle app crashes gracefully**: Don't retry the same action — use back button and try an alternative approach\n*   **Check Accessibility**: Use the `content-desc` and `text` properties in the UI hierarchy to ensure the UI is accessible for screen readers\n\n## 7. Troubleshooting\n\n### Device Not Connected\nIf tools report ADB errors:\n*   Check USB connection or emulator status\n*   Enable USB debugging on the device (Settings > Developer Options > USB Debugging)\n*   Accept the RSA key prompt on the device if asked\n*   Restart the device or disconnect/reconnect the USB cable\n*   Run `adb devices` to verify the device is visible\n\n### MCP Server Not Available\nIf tools are not listed in `/mcp`:\n*   Ensure Node.js 18+ is installed\n*   The server auto-builds on first use via `.mcp.json` at the project root\n*   Check `.claude/mcp/android-device-server/` exists with `package.json`\n*   Try manual build: `cd .claude/mcp/android-device-server && npm install && npm run build`\n","frontmatter":{"name":"interacting-with-android-device","description":"Instructions for capturing UI state, comparing with mocks, and interacting with an Android device using MCP tools backed by ADB.","allowed-tools":"mcp__android-device__capture, mcp__android-device__find_element, mcp__android-device__tap_at, mcp__android-device__tap_element, mcp__android-device__navigate, mcp__android-device__input_text, Bash(adb:*), Bash(sleep:*), Bash(./gradlew install*:*), Read, Glob","allowedTools":["mcp__android-device__capture,","mcp__android-device__find_element,","mcp__android-device__tap_at,","mcp__android-device__tap_element,","mcp__android-device__navigate,","mcp__android-device__input_text,","Bash(adb:*),","Bash(sleep:*),","Bash(./gradlew","install*:*),","Read,","Glob"]},"allowedTools":["mcp__android-device__capture,","mcp__android-device__find_element,","mcp__android-device__tap_at,","mcp__android-device__tap_element,","mcp__android-device__navigate,","mcp__android-device__input_text,","Bash(adb:*),","Bash(sleep:*),","Bash(./gradlew","install*:*),","Read,","Glob"],"isInternal":false,"tokens":1889,"sizeBytes":7957},{"name":"SKILL.md","path":".claude/skills/planning-android-implementation/SKILL.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/planning-android-implementation/SKILL.md","title":"planning-android-implementation","category":"anthropic-skill","format":"markdown","content":"---\nname: planning-android-implementation\nversion: 0.1.0\ndescription: Architecture design and phased implementation planning for Bitwarden Android. Use when planning implementation, designing architecture, creating file inventories, or breaking features into phases. Triggered by \"plan implementation\", \"architecture design\", \"implementation plan\", \"break this into phases\", \"what files do I need\", \"design the architecture\".\n---\n\n# Implementation Planning\n\nThis skill takes a refined specification (ideally from the `refining-android-requirements` skill) and produces a phased implementation plan with architecture design, file inventory, and risk assessment.\n\n**Prerequisite**: A clear set of requirements. If requirements are vague or incomplete, invoke the `refining-android-requirements` skill first.\n\n---\n\n## Step 1: Classify Change\n\nDetermine the change type to guide scope and planning depth:\n\n| Type | Description | Typical Scope |\n|------|-------------|---------------|\n| **New Feature** | Entirely new functionality, screens, or flows | New files + modifications, multi-phase |\n| **Enhancement** | Extending existing feature with new capabilities | Mostly modifications, 1-2 phases |\n| **Bug Fix** | Correcting incorrect behavior | Targeted modifications, single phase |\n| **Refactoring** | Restructuring without behavior change | Modifications only, migration-aware |\n| **Infrastructure** | Build, CI, tooling, or dependency changes | Config files, minimal code changes |\n\nState the classification and rationale before proceeding.\n\n---\n\n## Step 2: Codebase Exploration\n\nSearch the codebase to find reference implementations and integration points. Use the discovery commands from the `build-test-verify` skill as needed.\n\n### Find Pattern Anchors\n\nIdentify 2-3 existing files that serve as templates for the planned work:\n\n```\n**Pattern Anchors:**\n1. [file path] — [why this is a good reference]\n2. [file path] — [why this is a good reference]\n3. [file path] — [why this is a good reference]\n```\n\n### Map Integration Points\n\nIdentify files that must be modified to integrate the new work:\n\n- **Navigation**: Nav graph registrations, route definitions\n- **Dependency Injection**: Hilt modules, `@Provides` / `@Binds` functions\n- **Data Layer**: Repository interfaces, data source interfaces, Room DAOs\n- **API Layer**: Retrofit service interfaces, request/response models\n- **Feature Flags**: Feature flag definitions and checks\n- **Managers**: Single-responsibility data layer classes (see `docs/ARCHITECTURE.md` Managers section)\n- **Test Fixtures**: Shared test utilities in `src/testFixtures/` directories\n- **Product Flavor Source Sets**: Code in `src/standard/` vs `src/main/` for Play Services dependencies\n\n### Document Existing Patterns\n\nNote the specific patterns used by the pattern anchors:\n- State class structure (sealed class, data class fields)\n- Action/Event naming conventions\n- Repository method signatures and return types\n- Test structure and assertion patterns\n\n---\n\n## Step 3: Architecture Design\n\nProduce an ASCII diagram showing component relationships for the planned work:\n\n```\n┌─────────────────┐\n│   Screen        │ ← Compose UI\n│  (Composable)   │\n└────────┬────────┘\n         │ State / Action / Event\n┌────────▼────────┐\n│   ViewModel     │ ← Business logic orchestration\n└────────┬────────┘\n         │ Repository calls\n┌────────▼────────┐\n│   Repository    │ ← Data coordination (sealed class results)\n└───┬────┬────┬───┘\n    │    │    │\n┌───▼───┐ │ ┌─▼──────┐\n│Manager│ │ │Manager │ ← Single-responsibility (optional)\n└───┬───┘ │ └─┬──────┘\n    │     │   │\n┌───▼─────▼───▼────┐\n│   Data Sources   │ ← Raw data (Result<T>, never throw)\n└─┬────┬────┬──────┘\n  │    │    │\n Room Retrofit SDK\n```\n\nAdapt the diagram to show the actual components planned. _Consult `docs/ARCHITECTURE.md` for full data layer patterns and conventions._\n\n### Design Decisions\n\nDocument key architectural decisions in a table:\n\n| Decision | Resolution | Rationale |\n|----------|-----------|-----------|\n| [What needed deciding] | [What was chosen] | [Why] |\n\n---\n\n## Step 4: File Inventory\n\n### Files to Create\n\n| File Path | Type | Pattern Reference |\n|-----------|------|-------------------|\n| [full path] | [ViewModel / Screen / Repository / etc.] | [pattern anchor file] |\n\n**Include in file inventory:**\n- `...Navigation.kt` files for new screens\n- `...Module.kt` Hilt module files for new DI bindings\n- Paired test files (`...Test.kt`) for each new class\n\n### Files to Modify\n\n| File Path | Change Description | Risk Level |\n|-----------|-------------------|------------|\n| [full path] | [what changes] | Low / Medium / High |\n\n**Risk levels:**\n- **Low**: Additive changes (new entries in nav graph, new bindings in Hilt module)\n- **Medium**: Modifying existing logic (adding parameters, new branches)\n- **High**: Changing interfaces, data models, or shared utilities\n\n---\n\n## Step 5: Implementation Phases\n\nBreak the work into sequential phases. Each phase should be independently testable and committable.\n\n**Phase ordering principle**: Foundation → SDK/Data → Network → UI (tests accompany each phase)\n\nFor each phase:\n\n```markdown\n### Phase N: [Name]\n\n**Goal**: [What this phase accomplishes]\n\n**Files**:\n- Create: [list]\n- Modify: [list]\n\n**Tasks**:\n1. [Specific implementation task]\n2. [Specific implementation task]\n3. ...\n\n**Verification**:\n- [Test command or manual verification step]\n\n**Skills**: [Which workflow skills apply — e.g., `implementing-android-code`, `testing-android-code`]\n```\n\n### Phase Guidelines\n\n- Each phase should be small enough to be independently testable and committable\n- Tests are written within the same phase as the code they verify (not deferred to a \"testing phase\")\n- UI phases come after their data dependencies are in place\n- If a phase has more than 5 tasks, consider splitting it\n\n---\n\n## Step 6: Risk & Verification\n\n### Risk Assessment\n\n| Risk | Likelihood | Impact | Mitigation |\n|------|-----------|--------|------------|\n| [What could go wrong] | Low/Med/High | Low/Med/High | [How to prevent or handle] |\n\n### Verification Plan\n\n**Automated Verification:**\n- Unit test commands (from `build-test-verify` skill)\n- Lint/detekt commands\n- Build verification\n\n**Manual Verification:**\n- [Specific manual test scenarios]\n- [Edge cases to manually verify]\n- Verify ViewModel state survives process death (test via `SavedStateHandle` persistence and `Don't keep activities` developer option)\n","frontmatter":{"name":"planning-android-implementation","version":"0.1.0","description":"Architecture design and phased implementation planning for Bitwarden Android. Use when planning implementation, designing architecture, creating file inventories, or breaking features into phases. Triggered by \"plan implementation\", \"architecture design\", \"implementation plan\", \"break this into phases\", \"what files do I need\", \"design the architecture\"."},"isInternal":false,"tokens":1543,"sizeBytes":6925},{"name":"SKILL.md","path":".claude/skills/refining-android-requirements/SKILL.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/refining-android-requirements/SKILL.md","title":"refining-android-requirements","category":"anthropic-skill","format":"markdown","content":"---\nname: refining-android-requirements\nversion: 0.1.0\ndescription: Requirements gap analysis and structured specification for Bitwarden Android. Use when refining requirements, analyzing specs, identifying gaps, or producing structured specifications from tickets or descriptions. Triggered by \"refine requirements\", \"gap analysis\", \"spec review\", \"requirements analysis\", \"what's missing from this spec\", \"analyze this ticket\".\n---\n\n# Requirements Refinement\n\nThis skill takes raw requirements (from Jira tickets, Confluence pages, or free-text descriptions) and produces a structured, implementation-ready specification through systematic gap analysis.\n\n**Key principle**: This skill identifies gaps and produces specifications. It does NOT propose solutions or architecture — that is the responsibility of the `planning-android-implementation` skill.\n\n---\n\n## Step 1: Source Consolidation\n\nCombine all input sources into a single working document. For each requirement, note its source:\n\n```\n- [Source: PM-12345] User must be able to configure timeout\n- [Source: Confluence] Timeout range is 1-60 minutes\n- [Source: User] Default timeout should be 15 minutes\n```\n\nFlag any contradictions between sources for immediate resolution.\n\n---\n\n## Step 2: Gap Analysis\n\nEvaluate the consolidated requirements against the following 5-category rubric. For each category, check every item and note whether it is **covered**, **partially covered**, or **missing**.\n\n### A. Functional Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| User actions defined? | What specific user actions trigger this feature? |\n| All states covered? (empty, loading, error, success) | What should the user see in [empty/loading/error] state? |\n| Edge cases identified? | What happens when [boundary condition]? |\n| Cancellation/back navigation flows? | Can the user cancel mid-flow? What happens to partial data? |\n| Input validation rules? | What are the valid ranges/formats for [input]? |\n| Success/failure criteria? | How does the user know the operation succeeded or failed? |\n| Offline behavior? | What happens if this is attempted offline? |\n\n### B. Technical Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Module scope identified? (`:app`, `:authenticator`, shared) | Which module(s) does this feature belong to? |\n| SDK dependencies? | Does this require Bitwarden SDK operations? Which ones? |\n| Data storage approach? (Room, DataStore, in-memory) | Where is the data for this feature persisted? |\n| Network API endpoints? | Which API endpoints are involved? Are they existing or new? |\n| Process death handling? | What state needs to survive process death? |\n| Migration requirements? | Does existing data need migration? |\n| Feature flag needed? | Should this be behind a feature flag for staged rollout? |\n| Product flavors (standard vs fdroid)? | Does this feature depend on Google Play Services? Available on F-Droid? |\n| Data layer tier? | Does this need a new Manager (single-responsibility) or only Repository/DataSource? Consult `docs/ARCHITECTURE.md` Data Layer section. |\n| Streaming vs discrete data? | Is data continuously observed (`DataState<T>` + `StateFlow`) or a one-shot operation (custom sealed class)? See `docs/ARCHITECTURE.md` Repositories section. |\n\n### C. Security Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Data sensitivity classified? | What sensitivity level does this data have? (vault-level, account-level, non-sensitive) |\n| Storage encryption required? | Must this data be encrypted at rest? Via SDK or Android Keystore? |\n| Logout cleanup behavior? | What must be cleared when the user logs out? |\n| Auth-gating? | Does accessing this feature require active authentication? |\n| Input sanitization? | Are there URL or credential inputs that need validation? |\n| Sensitive data in ViewModel state? | Will passwords, tokens, or keys appear in state? Must use `@IgnoredOnParcel`. See `implementing-android-code` skill Section F. |\n| SDK crypto context isolation? | Does this use vault encryption? Must use `ScopedVaultSdkSource` for multi-account safety. See CLAUDE.md Security Rules. |\n\n### D. UX/UI Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| UI copy/strings defined? | What text should appear for [label/button/message]? |\n| Error messages specified? | What should the error message say when [failure case]? |\n| Loading states designed? | Should loading show a spinner, skeleton, or shimmer? |\n| Navigation flow clear? | Where does the user go after [action]? Back stack behavior? |\n| Accessibility considerations? | Are there content descriptions or focus order requirements? |\n| Toast/snackbar/dialog for feedback? | What feedback mechanism for [action result]? |\n\n### E. Cross-Cutting Concerns\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Multi-account behavior? | How does this behave with multiple accounts? Per-account or global? |\n| Backwards compatibility? | Does this affect existing users? Migration path? |\n| Feature flag strategy? | Is this behind a server-side or local feature flag? |\n| Analytics/logging? | Are there analytics events to track? |\n| Bitwarden Authenticator impact? | Does this affect the `:authenticator` module? |\n| F-Droid compatibility? | Does this degrade gracefully without Google Play Services (no push notifications, no Play Integrity)? |\n\n---\n\n## Step 3: Present Gaps\n\nOrganize all identified gaps into two categories:\n\n### Blocking Questions\n\nQuestions that **must** be answered before implementation can begin because they change the architecture, data model, or core flow.\n\nFormat each question as:\n\n```\n**G[N]** ([Category]) — [Question text]\n  Context: [Why this matters / what depends on the answer]\n```\n\n### Non-Blocking Questions\n\nQuestions that have **reasonable defaults** and can be resolved during implementation. Note the assumed default.\n\nFormat each question as:\n\n```\n**G[N]** ([Category]) — [Question text]\n  Default assumption: [What we'll assume if not answered]\n  Context: [Why this matters]\n```\n\n---\n\n## Step 4: Produce Specification\n\nAfter the user answers blocking questions (and optionally non-blocking ones), produce a structured specification:\n\n```markdown\n## Overview\n\n[1-2 paragraph summary of the feature, its purpose, and scope]\n\n## Functional Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| FR1 | [requirement] | [source] | [any notes] |\n| FR2 | ... | ... | ... |\n\n## Technical Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| TR1 | [requirement] | [source] | [any notes] |\n| TR2 | ... | ... | ... |\n\n## Security Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| SR1 | [requirement] | [source] | [any notes] |\n\n## UX Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| UX1 | [requirement] | [source] | [any notes] |\n\n## Open Items\n\nNon-blocking items with assumed defaults that may be revisited:\n\n| ID | Question | Assumed Default | Category |\n|----|----------|----------------|----------|\n| G[N] | [question] | [default] | [category] |\n\n## Source Documentation\n\n| Source | Type | Link |\n|--------|------|------|\n| [name] | Jira / Confluence / User-provided | [link if available] |\n```\n\n### Output Guidelines\n\n- Requirements use numbered IDs (FR1, TR1, SR1, UX1) for traceability through implementation\n- Each requirement cites its source (ticket, page, or user-provided)\n- Technical requirements use table format for structured key/value data\n- Interface signatures are included as fenced code blocks when applicable\n- Open items preserve the gap ID (G[N]) for cross-referencing","frontmatter":{"name":"refining-android-requirements","version":"0.1.0","description":"Requirements gap analysis and structured specification for Bitwarden Android. Use when refining requirements, analyzing specs, identifying gaps, or producing structured specifications from tickets or descriptions. Triggered by \"refine requirements\", \"gap analysis\", \"spec review\", \"requirements analysis\", \"what's missing from this spec\", \"analyze this ticket\"."},"isInternal":false,"tokens":1728,"sizeBytes":7855},{"name":"bug-fix.md","path":".claude/skills/reviewing-changes/checklists/bug-fix.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/checklists/bug-fix.md","title":"Checklists Skill","category":"anthropic-skill","format":"markdown","content":"# Bug Fix Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Bug\n\n**1. Understand root cause:**\n- What was the broken behavior?\n- What caused it?\n- How does this fix address the root cause?\n\n**2. Assess scope:**\n- How many files changed?\n- Is this a targeted fix or broader refactoring?\n- Does this affect multiple features?\n\n**3. Check for side effects:**\n- Could this break other features?\n- Are there edge cases not considered?\n\n### Second Pass: Verify the Fix\n\n**4. Code changes:**\n- Does the fix make sense?\n- Is it the simplest solution?\n- Any unnecessary changes included?\n\n**5. Testing:**\n- Is there a regression test?\n- Does test verify the bug is fixed?\n- Are edge cases covered?\n\n**6. Related code:**\n- Same pattern in other places that might have same bug?\n- Should other similar code be fixed too?\n\n## What to CHECK\n\n✅ **Root Cause Analysis**\n- Does the fix address the root cause or just symptoms?\n- Is the explanation in PR/commit clear?\n\n✅ **Regression Testing**\n- Is there a new test that would fail without this fix?\n- Does test cover the reported bug scenario?\n- Are related edge cases tested?\n\n✅ **Side Effects**\n- Could this break existing functionality?\n- Are there similar code paths that need checking?\n- Does this change behavior in unexpected ways?\n\n✅ **Fix Scope**\n- Is the fix appropriately scoped (not too broad, not too narrow)?\n- Are all instances of the bug fixed?\n- Any related bugs discovered during investigation?\n\n## What to SKIP\n\n❌ **Full Architecture Review** - Unless fix reveals architectural problems\n❌ **Comprehensive Testing Review** - Focus on regression tests, not entire test suite\n❌ **Major Refactoring Suggestions** - Unless directly related to preventing similar bugs\n\n## Red Flags\n\n🚩 **No test for the bug** - How will we prevent regression?\n🚩 **Fix doesn't match root cause** - Is this fixing symptoms?\n🚩 **Broad changes beyond the bug** - Should this be split into separate PRs?\n🚩 **Similar patterns elsewhere** - Should those be fixed too?\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"Can we add a test that would fail without this fix?\"\n- \"I see this pattern in [other file] - does it have the same issue?\"\n- \"Is this fixing the root cause or masking the symptom?\"\n- \"Could this change affect [related feature]?\"\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n**Overall Assessment:** APPROVE\n\nSee inline comments for suggested improvements.\n```\n\n**Inline comment examples:**\n\n```\n**data/auth/BiometricRepository.kt:120** - SUGGESTED: Extract null handling\n\n<details>\n<summary>Details</summary>\n\nRoot cause analysis: BiometricPrompt result was nullable but code assumed non-null, causing crash on cancellation (PM-12345).\n\nConsider extracting null handling pattern:\n\n\\```kotlin\nprivate fun handleBiometricResult(result: BiometricPrompt.AuthenticationResult?): AuthResult {\n    return result?.let { AuthResult.Success(it) } ?: AuthResult.Cancelled\n}\n\\```\n\nThis pattern could be reused if we add other biometric auth points.\n</details>\n```\n\n```\n**app/auth/BiometricViewModel.kt:89** - SUGGESTED: Add regression test\n\n<details>\n<summary>Details</summary>\n\nAdd test for cancellation scenario to prevent regression:\n\n\\```kotlin\n@Test\nfun `when biometric cancelled then returns cancelled state`() = runTest {\n    coEvery { repository.authenticate() } returns Result.failure(CancelledException())\n    viewModel.onBiometricAuth()\n    assertEquals(AuthState.Cancelled, viewModel.state.value)\n}\n\\```\n\nThis prevents regression of the bug just fixed.\n</details>\n```\n","isInternal":false,"tokens":865,"sizeBytes":3817},{"name":"dependency-update.md","path":".claude/skills/reviewing-changes/checklists/dependency-update.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/checklists/dependency-update.md","title":"Checklists Skill","category":"anthropic-skill","format":"markdown","content":"# Dependency Update Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Identify and Assess\n\n**1. Identify the change:**\n- Which library? Old version → New version?\n- Major (X.0.0), Minor (0.X.0), or Patch (0.0.X) version change?\n- Single dependency or multiple?\n\n**2. Check compilation safety:**\n- Any imports in codebase that might break?\n- Any deprecated APIs we're currently using?\n- Check if this is a breaking change version\n\n### Second Pass: Deep Analysis\n\n**3. Review release notes** (if available):\n- Breaking changes mentioned?\n- Security fixes included?\n- New features we should know about?\n- Deprecations that affect our usage?\n\n**4. Verify consistency:**\n- If updating androidx library, are related libraries updated consistently?\n- BOM (Bill of Materials) consistency if applicable?\n- Test dependencies updated alongside main dependencies?\n\n## What to CHECK\n\n✅ **Compilation Safety**\n- Look for API deprecations in our codebase\n- Check if import statements still valid\n- Major version bumps require extra scrutiny\n- Beta/alpha versions need stability assessment\n\n✅ **Security Implications** (if applicable)\n- Security-related libraries (crypto, auth, networking)?\n- Check for CVEs addressed in release notes\n- Review security advisories for this library\n\n✅ **Testing Implications**\n- Does this affect test utilities?\n- Are there breaking changes in test APIs?\n- Do existing tests still cover the same scenarios?\n\n✅ **Changelog Review**\n- Read release notes for breaking changes\n- Note any behavioral changes\n- Check migration guides if major version\n\n## What to SKIP\n\n❌ **Full Architecture Review** - No code changed, patterns unchanged\n❌ **Code Style Review** - No code to review\n❌ **New Test Requirements** - Unless API changed significantly\n❌ **Security Deep-Dive** - Unless crypto/auth/networking library\n❌ **Performance Analysis** - Unless release notes mention performance changes\n\n## Red Flags (Escalate to Full Review)\n\n🚩 **Major version bump** (e.g., 1.x → 2.0) - Read `checklists/feature-addition.md`\n🚩 **Security/crypto library** - Read `reference/architectural-patterns.md` and `docs/ARCHITECTURE.md#security`\n🚩 **Breaking changes in release notes** - Read relevant code sections carefully\n🚩 **Multiple dependency updates at once** - Check for interaction risks\n🚩 **Beta/Alpha versions** - Assess stability concerns and rollback plan\n\nIf any red flags present, escalate to more comprehensive review using appropriate checklist.\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Reviews\n\n### Example 1: Simple Patch Version (No Critical Issues)\n\n```markdown\n**Overall Assessment:** APPROVE\n\nSee inline comments for all issue details.\n```\n\n**Inline comment example:**\n```\n**libs.versions.toml:45** - SUGGESTED: Beta version in production\n\n<details>\n<summary>Details</summary>\n\nandroidx.credentials updated from 1.5.0 to 1.6.0-beta03\n\nMonitor for stability issues - beta releases may have unexpected behavior in production.\n\nChangelog: Adds support for additional credential types, internal bug fixes.\n</details>\n```\n\n### Example 2: Major Version with Breaking Changes (With Critical Issues)\n\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Breaking API changes in Retrofit 3.0.0 (network/api/BitwardenApiService.kt)\n- Breaking API changes in Retrofit 3.0.0 (network/api/VaultApiService.kt)\n\nSee inline comments for migration details.\n```\n\n**Inline comment example:**\n```\n**network/api/BitwardenApiService.kt:15** - CRITICAL: Breaking API changes\n\n<details>\n<summary>Details and fix</summary>\n\nRetrofit 3.0.0 removes `Call<T>` return type. Migration required:\n\n\\```kotlin\n// Before\nfun getUser(): Call<UserResponse>\n\n// After\nsuspend fun getUser(): Response<UserResponse>\n\\```\n\nUpdate all API service interfaces to use suspend functions, update call sites to use coroutines instead of enqueue/execute, and update tests accordingly.\n\nConsider creating a separate PR for this migration due to scope.\n\nReference: https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-300\n</details>\n```\n","isInternal":false,"tokens":981,"sizeBytes":4294},{"name":"feature-addition.md","path":".claude/skills/reviewing-changes/checklists/feature-addition.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/checklists/feature-addition.md","title":"Checklists Skill","category":"anthropic-skill","format":"markdown","content":"# Feature Addition Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: High-Level Assessment\n\n**1. Understand the feature:**\n- Read PR description - what problem does this solve?\n- Identify user-facing changes vs internal changes\n- Note any security implications (auth, encryption, data handling)\n\n**2. Scan file structure:**\n- Which modules affected? (app, data, network, ui, core?)\n- Are files organized correctly per module structure?\n- Any new public APIs introduced?\n\n**3. Initial risk assessment:**\n- Does this touch sensitive data or security-critical paths?\n- Does this affect existing features or only add new ones?\n- Are there obvious compilation or null safety issues?\n\n### Second Pass: Architecture Deep-Dive\n\n**4. MVVM + UDF Pattern Compliance:**\n- ViewModels properly structured?\n- State management using StateFlow?\n- Business logic in correct layer?\n\n**5. Dependency Injection:**\n- Hilt DI used correctly?\n- Dependencies injected, not manually instantiated?\n- Proper scoping applied?\n\n**6. Module Organization:**\n- Code placed in correct modules?\n- No circular dependencies introduced?\n- Proper separation of concerns?\n\n**7. Error Handling:**\n- Using Result types, not exception-based handling?\n- Errors propagated correctly through layers?\n\n### Third Pass: Details and Quality\n\n**8. Testing:**\n- Unit tests for ViewModels and repositories?\n- Test coverage for edge cases and error scenarios?\n- Tests verify behavior, not implementation?\n\n**9. Code Quality:**\n- Null safety handled properly?\n- Public APIs have KDoc documentation?\n- Naming follows project conventions?\n\n**10. Security:**\n- Sensitive data encrypted properly?\n- Authentication/authorization handled correctly?\n- Zero-knowledge architecture preserved?\n\n## Architecture Review\n\nRead `reference/architectural-patterns.md` for full patterns and code examples.\n\n**Check these four areas:**\n- **MVVM/UDF**: ViewModel exposes `StateFlow` (not `MutableStateFlow`), business logic in Repository, UI is stateless\n- **Hilt DI**: `@HiltViewModel` + `@Inject constructor`, inject interfaces not implementations, no manual instantiation\n- **Module placement**: UI in `:ui`/`:app`, data in `:data`, network in `:network`, no circular dependencies\n- **Error handling**: `Result<T>` / `runCatching` throughout — no thrown exceptions from data layer\n\n## Security Review\n\nReference: `docs/ARCHITECTURE.md#security`\n\n**Critical Security Checks:**\n\n- **Sensitive data encrypted**: Passwords, keys, tokens use Android Keystore or EncryptedSharedPreferences\n- **No plaintext secrets**: No passwords/keys in logs, memory dumps, or SharedPreferences\n- **Input validation**: All user-provided data validated and sanitized\n- **Authentication tokens**: Securely stored and transmitted\n- **Zero-knowledge architecture**: Encryption happens client-side, server never sees plaintext\n\n**Red Flags:**\n```kotlin\n// ❌ CRITICAL - Plaintext storage\nsharedPreferences.edit {\n    putString(\"pin\", userPin)  // Must use EncryptedSharedPreferences\n}\n\n// ❌ CRITICAL - Logging sensitive data\nLog.d(\"Auth\", \"Password: $password\")  // Never log sensitive data\n\n// ❌ CRITICAL - Weak encryption\nval cipher = Cipher.getInstance(\"DES\")  // Use AES-256-GCM\n\n// ✅ GOOD - Keystore encryption\nval encryptedData = keystoreManager.encrypt(sensitiveData)\nsecureStorage.store(encryptedData)\n```\n\n**If security concerns found, classify as CRITICAL using `reference/priority-framework.md`**\n\n## Testing Review\n\nReference: `reference/testing-patterns.md`\n\n**Required Test Coverage:**\n\n- **ViewModels**: Unit tests for state transitions, actions, error scenarios\n- **Repositories**: Unit tests for data transformations, error handling\n- **Business logic**: Unit tests for complex algorithms, calculations\n- **Edge cases**: Null inputs, empty states, network failures, concurrent operations\n\n**Test Quality:**\n```kotlin\n// ✅ GOOD - Tests behavior\n@Test\nfun `when login succeeds then state updates to success`() = runTest {\n    val viewModel = LoginViewModel(mockRepository)\n\n    coEvery { mockRepository.login(any(), any()) } returns Result.success(User())\n\n    viewModel.onLoginClicked(\"user\", \"pass\")\n\n    viewModel.state.test {\n        assertEquals(LoginState.Success, awaitItem())\n    }\n}\n\n// ❌ BAD - Tests implementation\n@Test\nfun `repository is called with correct parameters`() {\n    // This is testing internal implementation, not behavior\n}\n```\n\n**Testing Frameworks:**\n- JUnit 5 for test structure\n- MockK for mocking\n- Turbine for Flow testing\n- Kotlinx-coroutines-test for coroutine testing\n\n## Code Quality\n\n### Null Safety\n\n- No `!!` (non-null assertion) without clear safety guarantee\n- Platform types (from Java) handled with explicit nullability\n- Nullable types have proper null checks or use safe operators (`?.`, `?:`)\n\n```kotlin\n// ❌ BAD - Unsafe assertion\nval result = apiService.getData()!!  // Could crash\n\n// ✅ GOOD - Safe handling\nval result = apiService.getData() ?: return State.Error(\"No data\")\n\n// ❌ BAD - Platform type unchecked\nval intent: Intent = getIntent()  // Could be null from Java\nintent.getStringExtra(\"key\")  // Potential NPE\n\n// ✅ GOOD - Explicit nullability\nval intent: Intent? = getIntent()\nintent?.getStringExtra(\"key\")\n```\n\n### Documentation\n\n- **Public APIs**: Have KDoc comments explaining purpose, parameters, return values\n- **Complex algorithms**: Explained in comments\n- **Non-obvious behavior**: Documented with rationale\n\n```kotlin\n// ✅ GOOD - Documented public API\n/**\n * Encrypts the given data using AES-256-GCM with a key from Android Keystore.\n *\n * @param plaintext The data to encrypt\n * @return Result containing encrypted data or encryption error\n */\nsuspend fun encrypt(plaintext: ByteArray): Result<EncryptedData>\n```\n\n### Style Compliance\n\nReference: `docs/STYLE_AND_BEST_PRACTICES.md`\n\nOnly flag style issues if:\n- Not caught by linters (Detekt, ktlint)\n- Have architectural implications\n- Significantly impact readability\n\nSkip minor formatting (spaces, line breaks, etc.) - linters handle this.\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Providing Feedback\n\nUse `reference/review-psychology.md` for phrasing guidance.\n\n**Key principles:**\n- **Ask questions** for design decisions: \"Can we use the existing BitwardenTextField component here?\"\n- **Be prescriptive** for clear violations: \"Change MutableStateFlow to StateFlow (MVVM pattern requirement)\"\n- **Explain rationale**: \"This exposes mutable state, violating unidirectional data flow\"\n- **Use I-statements**: \"It's hard for me to understand this logic without comments\"\n- **Avoid condescension**: Don't use \"just\", \"simply\", \"obviously\"\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n","isInternal":false,"tokens":1509,"sizeBytes":6814},{"name":"infrastructure.md","path":".claude/skills/reviewing-changes/checklists/infrastructure.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/checklists/infrastructure.md","title":"Checklists Skill","category":"anthropic-skill","format":"markdown","content":"# Infrastructure Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Change\n\n**1. Identify the goal:**\n- What problem does this solve?\n- Is this optimization, fix, or new capability?\n- What's the expected impact?\n\n**2. Assess risk:**\n- Does this affect production builds?\n- Could this break CI/CD pipelines?\n- Impact on developer workflow?\n\n**3. Performance implications:**\n- Will builds be faster or slower?\n- CI time impact?\n- Resource usage changes?\n\n### Second Pass: Verify Implementation\n\n**4. Configuration correctness:**\n- Syntax valid?\n- References correct?\n- Secrets/credentials handled securely?\n\n**5. Impact analysis:**\n- What workflows/builds are affected?\n- Rollback plan if this breaks?\n- Documentation for team?\n\n**6. Testing strategy:**\n- How can this be tested before merge?\n- Canary/gradual rollout possible?\n- Monitoring for issues post-merge?\n\n## What to CHECK\n\n✅ **Configuration Correctness**\n- YAML/Groovy syntax valid\n- File references correct\n- Version numbers/tags valid\n- Conditional logic sound\n\n✅ **Security**\n- No hardcoded secrets or credentials\n- GitHub secrets used properly\n- Permissions appropriately scoped\n- No sensitive data in logs\n\n✅ **Performance Impact**\n- Build time implications understood\n- CI queue time impact assessed\n- Resource usage reasonable\n\n✅ **Rollback Plan**\n- Can this be reverted easily?\n- Dependencies on other changes?\n- Gradual rollout possible?\n\n✅ **Documentation**\n- Changes documented for team?\n- README or CONTRIBUTING updated?\n- Breaking changes clearly noted?\n\n## What to SKIP\n\n❌ **Bikeshedding Configuration** - Unless clear performance/maintenance benefit\n❌ **Over-Optimization** - Unless current system has proven problems\n❌ **Suggesting Major Rewrites** - Unless current approach is fundamentally broken\n\n## Red Flags\n\n🚩 **Hardcoded secrets** - Use GitHub secrets or secure storage\n🚩 **No rollback plan** - Critical infrastructure should be revertible\n🚩 **Untested changes** - CI changes should be validated\n🚩 **Breaking changes without notice** - Team needs advance warning\n🚩 **Performance regression** - Builds shouldn't get significantly slower\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"What's the rollback plan if this breaks CI?\"\n- \"Can we test this on a feature branch before main?\"\n- \"Will this impact build times? By how much?\"\n- \"Should this be documented in CONTRIBUTING.md?\"\n\n## Common Infrastructure Patterns\n\n### GitHub Actions\n\n```yaml\n# ✅ GOOD - Secure, clear, tested\nname: Build and Test\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30  # Prevent runaway builds\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run tests\n        env:\n          API_KEY: ${{ secrets.API_KEY }}  # Secure secret usage\n        run: ./gradlew test\n\n# ❌ BAD - Insecure, unclear\nname: Build\non: push  # Too broad, runs on all branches\njobs:\n  build:\n    runs-on: ubuntu-latest\n    # No timeout - could run forever\n    steps:\n      - run: |\n          export API_KEY=\"hardcoded_key_here\"  # Hardcoded secret!\n          ./gradlew test\n```\n\n### Gradle Configuration\n\n```kotlin\n// ✅ GOOD - Clear, maintainable\ndependencies {\n    implementation(libs.androidx.core.ktx)  // Version catalog\n    implementation(libs.hilt.android)\n\n    testImplementation(libs.junit5)\n    testImplementation(libs.mockk)\n}\n\n// ❌ BAD - Hardcoded versions\ndependencies {\n    implementation(\"androidx.core:core-ktx:1.12.0\")  // Hardcoded version\n    implementation(\"com.google.dagger:hilt-android:2.48\")\n}\n```\n\n### Build Optimization\n\n```kotlin\n// ✅ GOOD - Parallel, cached\ntasks.register(\"checkAll\") {\n    dependsOn(\"detekt\", \"ktlintCheck\", \"testStandardDebug\")\n    group = \"verification\"\n    description = \"Run all checks in parallel\"\n\n    // Enable caching for faster builds\n    outputs.upToDateWhen { false }\n}\n\n// ❌ BAD - Sequential, no caching\ntasks.register(\"checkAll\") {\n    doLast {\n        exec { commandLine(\"./gradlew\", \"detekt\") }\n        exec { commandLine(\"./gradlew\", \"ktlintCheck\") }  // Sequential\n        exec { commandLine(\"./gradlew\", \"test\") }\n    }\n}\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n## Summary\nOptimizes CI build by parallelizing test execution and caching dependencies\n\nImpact: Estimated 40% reduction in CI time (12 min → 7 min per build)\n\n## Critical Issues\nNone\n\n## Suggested Improvements\n\n**.github/workflows/build.yml:23** - Add timeout for safety\n```yaml\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30  # Prevent builds from hanging\n    steps:\n      # ...\n```\nThis prevents runaway builds if something goes wrong.\n\n**.github/workflows/build.yml:45** - Consider matrix strategy for module tests\nCan we run module tests in parallel using a matrix strategy?\n```yaml\nstrategy:\n  matrix:\n    module: [app, data, network, ui]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: ./gradlew :${{ matrix.module }}:test\n```\nThis could further reduce CI time.\n\n**build.gradle.kts:12** - Document caching strategy\nCan we add a comment explaining the caching configuration?\nFuture maintainers will appreciate understanding why these specific cache keys are used.\n\n## Rollback Plan\nIf CI breaks:\n- Revert commit: `git revert [commit-hash]`\n- Previous workflow available at: `.github/workflows/build.yml@main^`\n- Monitor CI times at: https://github.com/[org]/[repo]/actions\n```\n","isInternal":false,"tokens":1351,"sizeBytes":5676},{"name":"refactoring.md","path":".claude/skills/reviewing-changes/checklists/refactoring.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/checklists/refactoring.md","title":"Checklists Skill","category":"anthropic-skill","format":"markdown","content":"# Refactoring Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Refactoring\n\n**1. Understand the goal:**\n- What pattern is being improved?\n- Why is this refactoring needed?\n- What's the scope of changes?\n\n**2. Assess completeness:**\n- Are all instances refactored or just some?\n- Are there related areas that should also change?\n- Is the migration complete or partial?\n\n**3. Risk assessment:**\n- Does this change behavior?\n- How many files affected?\n- Are tests updated to reflect changes?\n\n### Second Pass: Verify Consistency\n\n**4. Pattern consistency:**\n- Is the new pattern applied consistently throughout?\n- Are there missed instances of the old pattern?\n- Does this match established project patterns?\n\n**5. Migration completeness:**\n- Old pattern fully removed or deprecated?\n- All usages updated?\n- Documentation updated?\n\n**6. Test coverage:**\n- Do tests still pass?\n- Are tests refactored to match?\n- Does behavior remain unchanged?\n\n## What to CHECK\n\n✅ **Pattern Consistency**\n- New pattern applied consistently across all touched code\n- Follows established project patterns (MVVM, DI, error handling)\n- No mix of old and new patterns\n\n✅ **Migration Completeness**\n- All instances of old pattern updated?\n- Deprecated methods removed or marked @Deprecated?\n- Related code also updated (tests, docs)?\n\n✅ **Behavior Preservation**\n- Refactoring doesn't change behavior\n- Tests still pass\n- Edge cases still handled\n\n✅ **Deprecation Strategy** (if applicable)\n- Old APIs marked @Deprecated with migration guidance\n- Replacement clearly documented\n- Timeline for removal specified\n\n## What to SKIP\n\n❌ **Suggesting Additional Refactorings** - Unless directly related to current changes\n❌ **Scope Creep** - Don't request refactoring of untouched code\n❌ **Perfection** - Better code is better than perfect code\n\n## Red Flags\n\n🚩 **Incomplete migration** - Mix of old and new patterns\n🚩 **Behavior changes** - Refactoring shouldn't change behavior\n🚩 **Broken tests** - Tests should be updated to match refactoring\n🚩 **Undocumented pattern** - New pattern should be clear to team\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"I see the old pattern still used in [file:line] - should that be updated too?\"\n- \"Can we add @Deprecated to the old method with migration guidance?\"\n- \"How do we ensure this behavior remains the same?\"\n- \"Should this pattern be documented in ARCHITECTURE.md?\"\n\n## Common Refactoring Patterns\n\n### Extract Interface/Repository\n\n```kotlin\n// ✅ GOOD - Complete migration\ninterface FeatureRepository {\n    suspend fun getData(): Result<Data>\n}\n\nclass FeatureRepositoryImpl @Inject constructor(\n    private val apiService: FeatureApiService\n) : FeatureRepository {\n    override suspend fun getData(): Result<Data> = runCatching {\n        apiService.fetchData()\n    }\n}\n\n// All usages updated to inject interface\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository  // Interface\n) : ViewModel()\n\n// ❌ BAD - Incomplete migration\n// Some files still inject FeatureRepositoryImpl directly\n```\n\n### Modernize Error Handling\n\n```kotlin\n// ✅ GOOD - Complete migration\n// Old exception-based removed\nsuspend fun fetchData(): Result<Data> = runCatching {\n    apiService.getData()\n}\n\n// All call sites updated\nrepository.fetchData().fold(\n    onSuccess = { /* handle */ },\n    onFailure = { /* handle */ }\n)\n\n// ❌ BAD - Mixed patterns\n// Some functions use Result, others still throw exceptions\n```\n\n### Extract Reusable Component\n\n```kotlin\n// ✅ GOOD - Complete extraction\n// Component moved to :ui module\n@Composable\nfun BitwardenButton(\n    text: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier\n)\n\n// All usages updated to use new component\n// Old inline button implementations removed\n\n// ❌ BAD - Incomplete extraction\n// Some screens use new component, others still have inline implementation\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Reviews\n\n### Example 1: Refactoring with Incomplete Migration\n\n**Context**: Refactoring authentication to Repository pattern, but one ViewModel still uses old pattern\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Incomplete migration (app/vault/VaultViewModel.kt:89)\n\nSee inline comments for details.\n```\n\n**Inline Comment 1** (on `app/vault/VaultViewModel.kt:89`):\n```markdown\n**IMPORTANT**: Incomplete migration\n\n<details>\n<summary>Details and fix</summary>\n\nThis ViewModel still injects AuthManager directly. Should it use AuthRepository like the other 11 ViewModels?\n\n\\```kotlin\n// Current (old pattern)\nclass VaultViewModel @Inject constructor(\n    private val authManager: AuthManager\n)\n\n// Should be (new pattern)\nclass VaultViewModel @Inject constructor(\n    private val authRepository: AuthRepository\n)\n\\```\n\nThis is the only ViewModel still using the old pattern.\n</details>\n```\n\n**Inline Comment 2** (on `data/auth/AuthManager.kt:1`):\n```markdown\n**SUGGESTED**: Add deprecation notice\n\n<details>\n<summary>Details</summary>\n\nCan we add @Deprecated to AuthManager to guide future development?\n\n\\```kotlin\n@Deprecated(\n    message = \"Use AuthRepository interface instead\",\n    replaceWith = ReplaceWith(\"AuthRepository\"),\n    level = DeprecationLevel.WARNING\n)\nclass AuthManager @Inject constructor(...)\n\\```\n\nThis helps prevent new code from using the old pattern.\n</details>\n```\n\n---\n\n### Example 2: Clean Refactoring (No Issues)\n\n**Context**: Refactoring with complete migration, all patterns followed correctly, tests passing\n\n**Review Comment:**\n```markdown\n**Overall Assessment:** APPROVE\n\nClean refactoring moving ExitManager to :ui module. Follows established patterns, eliminates duplication, tests updated correctly.\n```\n\n**Token count:** ~30 tokens (vs ~800 for verbose format)\n\n**Why this works:**\n- 3 lines total\n- Clear approval decision\n- Briefly notes what was done\n- No elaborate sections, checkmarks, or excessive praise\n- Author gets immediate green light to merge\n\n**What NOT to do for clean refactorings:**\n```markdown\n❌ DO NOT create these sections:\n\n## Summary\nThis PR successfully refactors ExitManager into shared code...\n\n## Key Strengths\n- ✅ Follows established module organization patterns\n- ✅ Removes code duplication between apps\n- ✅ Improves test coverage\n- ✅ Maintains consistent behavior\n[...20 more checkmarks...]\n\n## Code Quality & Architecture\n**Architectural Compliance:** ✅\n- Correctly places manager in :ui module\n- Follows established pattern for UI-layer managers\n[...detailed analysis...]\n\n## Changes\n- ✅ Moved ExitManager interface from app → ui module\n- ✅ Moved ExitManagerImpl from app → ui module\n[...listing every file...]\n```\n\nThis is excessive. **For clean PRs: 2-3 lines maximum.**\n","isInternal":false,"tokens":1575,"sizeBytes":7031},{"name":"ui-refinement.md","path":".claude/skills/reviewing-changes/checklists/ui-refinement.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/checklists/ui-refinement.md","title":"Checklists Skill","category":"anthropic-skill","format":"markdown","content":"# UI Refinement Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Visual Changes\n\n**1. Understand the changes:**\n- What visual/UX problem is being solved?\n- Are there designs or screenshots to reference?\n- Is this a bug fix or enhancement?\n\n**2. Component usage:**\n- Using existing components from `:ui` module?\n- Any new custom components created?\n- Could existing components be reused?\n\n### Second Pass: Implementation Review\n\n**3. Compose best practices:**\n- Composables properly structured?\n- State hoisted correctly?\n- Preview composables included?\n\n**4. Accessibility:**\n- Content descriptions for images/icons?\n- Semantic properties for screen readers?\n- Touch targets meet minimum size (48dp)?\n\n**5. Design consistency:**\n- Using theme colors, spacing, typography?\n- Consistent with other screens?\n- Responsive to different screen sizes?\n\n## What to CHECK\n\n✅ **Compose Best Practices**\n- Composables are stateless where possible\n- State hoisting follows patterns\n- Side effects (LaunchedEffect, DisposableEffect) used correctly\n- Preview composables provided for development\n\n✅ **Component Reuse**\n- Using existing BitwardenButton, BitwardenTextField, etc.?\n- Could custom UI be replaced with existing components?\n- New reusable components placed in `:ui` module?\n\n✅ **Accessibility**\n- `contentDescription` for icons and images\n- `semantics` for custom interactions\n- Sufficient contrast ratios\n- Touch targets ≥ 48dp minimum\n\n✅ **Design Consistency**\n- Using `BitwardenTheme` colors (not hardcoded)\n- Using `BitwardenTheme` spacing (16.dp, 8.dp, etc.)\n- Using `BitwardenTheme` typography styles\n- Consistent with existing screen patterns\n\n✅ **Responsive Design**\n- Handles different screen sizes?\n- Scrollable content where appropriate?\n- Landscape orientation considered?\n\n## What to SKIP\n\n❌ **Deep Architecture Review** - Unless ViewModel changes are substantial\n❌ **Business Logic Review** - Focus is on presentation, not logic\n❌ **Security Review** - Unless UI exposes sensitive data improperly\n\n## Red Flags\n\n🚩 **Duplicating existing components** - Should reuse from `:ui` module\n🚩 **Hardcoded colors/dimensions** - Should use theme\n🚩 **Missing accessibility properties** - Critical for screen readers\n🚩 **State management in UI** - Should be hoisted to ViewModel\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"Can we use BitwardenButton here instead of this custom button?\"\n- \"Should this color come from BitwardenTheme instead of being hardcoded?\"\n- \"How will this look on a small screen?\"\n- \"Is there a contentDescription for this icon?\"\n\n## Common Patterns\n\n### Composable Structure\n\n```kotlin\n// ✅ GOOD - Stateless, hoisted state\n@Composable\nfun FeatureScreen(\n    state: FeatureState,\n    onActionClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    // UI rendering only\n}\n\n// ❌ BAD - Business state in composable\n@Composable\nfun FeatureScreen() {\n    var userData by remember { mutableStateOf<User?>(null) }  // Business state should be in ViewModel\n    var isLoading by remember { mutableStateOf(false) }  // App state should be in ViewModel\n    // ...\n}\n\n// ✅ OK - UI-local state in composable\n@Composable\nfun LoginForm(onSubmit: (String, String) -> Unit) {\n    var username by remember { mutableStateOf(\"\") }  // UI-local input state is fine\n    var password by remember { mutableStateOf(\"\") }\n    // Hoist only as high as needed\n}\n```\n\n### Theme Usage\n\n```kotlin\n// ✅ GOOD - Using theme\nText(\n    text = \"Title\",\n    style = BitwardenTheme.typography.titleLarge,\n    color = BitwardenTheme.colorScheme.primary\n)\n\n// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)\nSpacer(modifier = Modifier.height(16.dp))\n\n// ❌ BAD - Hardcoded\nText(\n    text = \"Title\",\n    style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold),  // Should use theme\n    color = Color(0xFF0000FF)  // Should use theme color\n)\n\nSpacer(modifier = Modifier.height(17.dp))  // Non-standard spacing\n```\n\n### Accessibility\n\n```kotlin\n// ✅ GOOD - Interactive element with description\nIcon(\n    painter = painterResource(R.drawable.ic_password),\n    contentDescription = \"Password visibility toggle\",\n    modifier = Modifier.clickable { onToggle() }\n)\n\n// ✅ GOOD - Decorative icon with explicit null\nIcon(\n    painter = painterResource(R.drawable.ic_check),\n    contentDescription = null,  // Decorative icon next to descriptive text\n    tint = BitwardenTheme.colorScheme.success\n)\n\n// ❌ BAD - Interactive element missing description\nIcon(\n    painter = painterResource(R.drawable.ic_delete),\n    contentDescription = null,  // Interactive elements need descriptions\n    modifier = Modifier.clickable { onDelete() }\n)\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n## Summary\nUpdates login screen layout for improved visual hierarchy and touch targets\n\n## Critical Issues\nNone\n\n## Suggested Improvements\n\n**app/auth/LoginScreen.kt:67** - Can we use BitwardenTextField?\nThis custom text field looks very similar to `ui/components/BitwardenTextField.kt:89`.\nWould using the existing component maintain consistency?\n\n**app/auth/LoginScreen.kt:123** - Add contentDescription\n```kotlin\nIcon(\n    painter = painterResource(R.drawable.ic_visibility),\n    contentDescription = \"Show password\",  // Add for accessibility\n    modifier = Modifier.clickable { onToggleVisibility() }\n)\n```\n\n**app/auth/LoginScreen.kt:145** - Use design system spacing\n```kotlin\n// Current\nSpacer(modifier = Modifier.height(17.dp))\n\n// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)\nSpacer(modifier = Modifier.height(16.dp))\n```\n```\n","isInternal":false,"tokens":1375,"sizeBytes":5858},{"name":"review-outputs.md","path":".claude/skills/reviewing-changes/examples/review-outputs.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/examples/review-outputs.md","title":"Examples Skill","category":"anthropic-skill","format":"markdown","content":"# Review Output Examples\n\nWell-structured code reviews demonstrating appropriate depth, tone, and formatting for different change types.\n\n## Table of Contents\n\n**Format Reference:**\n- [Quick Format Reference](#quick-format-reference)\n  - [Inline Comment Format](#inline-comment-format-required)\n  - [Summary Comment Format](#summary-comment-format)\n\n**Examples:**\n- [Example 1: Clean PR (No Issues)](#example-1-clean-pr-no-issues)\n- [Example 2: Dependency Update with Breaking Changes](#example-2-dependency-update-with-breaking-changes)\n- [Example 3: Feature Addition with Critical Issues](#example-3-feature-addition-with-critical-issues)\n\n**Anti-Patterns:**\n- [❌ Anti-Patterns to Avoid](#-anti-patterns-to-avoid)\n  - [Problem: Verbose Summary with Multiple Sections](#problem-verbose-summary-with-multiple-sections)\n  - [Problem: Praise-Only Inline Comments](#problem-praise-only-inline-comments)\n  - [Problem: Missing `<details>` Tags](#problem-missing-details-tags)\n\n**Summary:**\n- [Summary](#summary)\n\n---\n\n## Quick Format Reference\n\n### Inline Comment Format (REQUIRED)\n\n**MUST use `<details>` tags.** Only severity + description visible; all other content collapsed.\n\n```\n[emoji] **[SEVERITY]**: [One-line issue description]\n\n<details>\n<summary>Details and fix</summary>\n\n[Code example or specific fix]\n\n[Rationale explaining why]\n\nReference: [docs link if applicable]\n</details>\n```\n\n**Severity Levels:**\n- ❌ **CRITICAL** - Blocking, must fix (security, crashes, architecture violations)\n- ⚠️ **IMPORTANT** - Should fix (missing tests, quality issues)\n- ♻️ **DEBT** - Technical debt (duplication, convention violations, future rework needed)\n- 🎨 **SUGGESTED** - Nice to have (refactoring, improvements)\n- ❓ **QUESTION** - Seeking clarification (requirements, design decisions)\n\n### Summary Comment Format\n\nUses the agent's `posting-review-summary` skill format. Surface ❌ CRITICAL issues at the top level for immediate visibility, wrap the full findings list in `<details>` for scannability.\n\n```\n**Overall Assessment:** APPROVE / REQUEST CHANGES\n\n[1-2 neutral sentences describing what was reviewed]\n\n**Critical Issues** (if any):\n- ❌ [One-line summary with file:line]\n\n<details>\n<summary>All findings</summary>\n\n- ❌ **CRITICAL**: [description] (`file:line`)\n- ⚠️ **IMPORTANT**: [description] (`file:line`)\n- ♻️ **DEBT**: [description] (`file:line`)\n- 🎨 **SUGGESTED**: [description] (`file:line`)\n- ❓ **QUESTION**: [description] (`file:line`)\n</details>\n```\n\nFor clean PRs with no findings, omit both sections entirely — verdict + 1-2 sentences is sufficient.\n\n**GitHub pitfall**: Never use `#` followed by a number in comment text (e.g., `#42`, `#PR123`). GitHub autolinks these to issues/PRs. Use `Finding 1:` or `item 42` instead.\n\n---\n\n## Example 1: Clean PR (No Issues)\n\n**Context**: Moving shared code to common module, complete migration, all patterns followed\n\n**Review Comment:**\n```markdown\n**Overall Assessment:** APPROVE\n\nClean refactoring that moves ExitManager to :ui module, eliminating duplication between apps.\n```\n\n**Why this works:**\n- Immediate approval visible (2-3 lines)\n- One sentence acknowledging the work\n- No unnecessary sections or elaborate praise\n- Author gets quick feedback and can proceed\n\n---\n\n## Example 2: Dependency Update with Breaking Changes\n\n**Context**: Major version update requiring code migration\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- API migration required for Retrofit 3.0 breaking changes (network/api/BitwardenApiService.kt:34)\n\nSee inline comments for migration details.\n```\n\n**Inline Comment 1** (on `network/api/BitwardenApiService.kt:34`):\n```markdown\n❌ **CRITICAL**: API migration required for Retrofit 3.0\n\n<details>\n<summary>Details and fix</summary>\n\nRetrofit 3.0 removes the `Call<T>` return type. All 12 API methods in this file need migration:\n\n```kotlin\n// Current (deprecated in Retrofit 3.0)\n@GET(\"api/accounts/profile\")\nfun getProfile(): Call<ProfileResponse>\n\n// Must migrate to\n@GET(\"api/accounts/profile\")\nsuspend fun getProfile(): Response<ProfileResponse>\n```\n\nBreaking API change affects:\n- 12 methods in BitwardenApiService\n- 8 methods in VaultApiService\n- All call sites using enqueue/execute\n- Test utilities\n\nConsider creating separate PR for this migration given the scope.\n\nReference: [Retrofit 3.0 migration guide](https://square.github.io/retrofit/changelogs/changelog-3.x/)\n</details>\n```\n\n**Key Features:**\n- Minimal summary (2-3 lines)\n- Full details in collapsed inline comment\n- Specific file:line references\n- Code examples in <details>\n- Migration guidance and scope assessment\n\n---\n\n## Example 3: Feature Addition with Critical Issues\n\n**Context**: Implements PIN unlock for vault access\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Exposes mutable state violating MVVM (UnlockViewModel.kt:78)\n- PIN stored without encryption - SECURITY ISSUE (UnlockRepository.kt:145)\n\nSee inline comments for all issues and suggestions.\n```\n\n**Inline Comment 1** (on `app/vault/unlock/UnlockViewModel.kt:78`):\n```markdown\n❌ **CRITICAL**: Exposes mutable state\n\n<details>\n<summary>Details and fix</summary>\n\nChange `MutableStateFlow<State>` to `StateFlow<State>`:\n\n```kotlin\n// Current (problematic)\nval unlockState: MutableStateFlow<UnlockState>\n\n// Should be\nprivate val _unlockState = MutableStateFlow<UnlockState>()\nval unlockState: StateFlow<UnlockState> = _unlockState.asStateFlow()\n```\n\nExposing MutableStateFlow allows external mutation, violating MVVM unidirectional data flow.\n\nReference: docs/ARCHITECTURE.md#mvvm-pattern\n</details>\n```\n\n**Inline Comment 2** (on `data/vault/UnlockRepository.kt:145`):\n```markdown\n❌ **CRITICAL**: PIN stored without encryption - SECURITY ISSUE\n\n<details>\n<summary>Details and fix</summary>\n\nStoring PIN in plaintext SharedPreferences exposes it to backup systems and rooted devices.\n\n```kotlin\n// Current (CRITICAL SECURITY ISSUE)\nsharedPreferences.edit {\n    putString(KEY_PIN, pin)\n}\n\n// Must use Android Keystore encryption\nsuspend fun storePin(pin: String): Result<Unit> = runCatching {\n    val encrypted = keystoreManager.encrypt(pin.toByteArray())\n    encryptedPrefs.putBytes(KEY_PIN, encrypted)\n}\n```\n\nUse Android Keystore encryption or EncryptedSharedPreferences per security architecture.\n\nReference: docs/ARCHITECTURE.md#security\n</details>\n```\n\n**Inline Comment 3** (on `app/vault/unlock/UnlockViewModel.kt:92`):\n```markdown\n⚠️ **IMPORTANT**: Missing error handling test\n\n<details>\n<summary>Details and fix</summary>\n\nAdd test to prevent regression if error handling changes:\n\n```kotlin\n@Test\nfun `when incorrect PIN entered then returns error state`() = runTest {\n    val viewModel = UnlockViewModel(mockRepository)\n    coEvery { mockRepository.validatePin(\"1234\") }\n        returns Result.failure(InvalidPinException())\n\n    viewModel.onPinEntered(\"1234\")\n\n    assertEquals(UnlockState.Error(\"Invalid PIN\"), viewModel.state.value)\n}\n```\n\nEnsures error flow remains robust across refactorings.\n</details>\n```\n\n**Inline Comment 4** (on `app/vault/unlock/UnlockViewModel.kt:105`):\n```markdown\n🎨 **SUGGESTED**: Consider rate limiting for PIN attempts\n\n<details>\n<summary>Details and fix</summary>\n\nCurrently allows unlimited attempts, which could enable brute force attacks.\n\n```kotlin\nprivate var attemptCount = 0\nprivate var lockoutUntil: Instant? = null\n\nfun onPinEntered(pin: String) {\n    if (isLockedOut()) {\n        _state.value = UnlockState.LockedOut(lockoutUntil!!)\n        return\n    }\n    // ... validate PIN ...\n    if (invalid) {\n        attemptCount++\n        if (attemptCount >= MAX_ATTEMPTS) {\n            lockoutUntil = clock.millis() + 15.minutes\n        }\n    }\n}\n```\n\nWould add security layer against brute force. Consider discussing threat model with security team.\n</details>\n```\n\n**Inline Comment 5** (on `app/vault/unlock/UnlockScreen.kt:134`):\n```markdown\n❓ **QUESTION**: Can we use BitwardenTextField?\n\n<details>\n<summary>Details</summary>\n\nThis custom PIN input field looks similar to `ui/components/BitwardenTextField.kt:67`.\n\nWould using the existing component maintain consistency and reduce custom UI code?\n</details>\n```\n\n**Key Features:**\n- Minimal summary (3-4 lines) with critical issues only\n- Each issue gets separate inline comment with `<details>` tag\n- Multiple severity levels demonstrated (CRITICAL, IMPORTANT, SUGGESTED, QUESTION)\n- Mix of prescriptive fixes and collaborative questions\n- Code examples collapsed in <details>\n- No \"Good Practices\" or \"Action Items\" sections\n\n---\n\n## ❌ Anti-Patterns to Avoid\n\n### Problem: Verbose Summary with Multiple Sections\n\n**What NOT to do:**\n```markdown\n### Review Complete ✅\n\n## Summary\n[Lengthy description of what the PR does]\n\n### Strengths 👍\n1. **Excellent documentation** - KDoc comments are comprehensive\n2. **Proper fail-closed design** - Security defaults to rejection\n3. **Defense in depth** - Multiple validation layers\n[7 total items with elaboration]\n\n### Critical Issues ⚠️\n- Missing test coverage for security-critical code (with full details)\n- [More issues with full explanations]\n\n### Recommendations 🎨\n- [Multiple recommendations]\n\n### Test Coverage Status 📊\n- [Analysis]\n\n### Architecture Compliance ✅\n- [Analysis]\n\n## Recommendation\n**Conditional approval** with follow-up...\n```\n\n**Why this is wrong:**\n- 800+ tokens for a summary comment\n- Multiple sections (Strengths, Recommendations, Test Coverage, Architecture)\n- Elaborates on positive aspects (\"Excellent documentation...\")\n- Duplicates critical issues (summary has details + inline comments have same details)\n- Creates visual clutter in PR conversation\n\n**Correct approach:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Missing test coverage for security-critical code (PasswordManagerSignatureVerifierImpl.kt:47)\n\nSee inline comments for details.\n```\n\n**Key differences:**\n- 3-5 lines vs 800+ tokens\n- Verdict + critical issues only\n- All details belong in inline comments\n- No positive commentary sections\n- Scales with PR complexity, not analysis thoroughness\n\n### Problem: Praise-Only Inline Comments\n\n**What NOT to do:**\n\nCreating inline comment on `AuthenticatorBridgeManagerImpl.kt:73`:\n```markdown\n👍 **Excellent integration of signature verification**\n\nThe signature verification is properly integrated into the connection flow:\n- Checked during initialization (line 73)\n- Checked before binding (line 134)\n- Ensures only validated apps can connect\n\nThis is exactly the right approach for fail-safe security.\n```\n\n**Why this is wrong:**\n- Entire comment is positive feedback with no actionable issue\n- Takes up space in PR conversation\n- Distracts from actual issues\n- Violates \"focus on actionable feedback\" principle\n\n**Correct approach:**\n- Do not create this comment at all\n- Reserve inline comments exclusively for issues requiring attention\n\n### Problem: Missing `<details>` Tags\n\n**What NOT to do:**\n\n```markdown\n❌ **CRITICAL**: Missing test coverage for security-critical code\n\nThe `@OmitFromCoverage` annotation excludes this entire class from test coverage.\n\n**Problems:**\n1. No validation that certificate hashes match actual Bitwarden certificates\n2. No verification of fail-closed behavior on edge cases\n3. No tests for multiple signer rejection logic\n4. Certificate hash typos would go undetected until production\n\n**Recommendation:**\nReplace `@OmitFromCoverage` with proper unit tests.\n\nExample test structure:\n[long code block]\n\nSecurity-critical code should have the highest test coverage, not be omitted.\n```\n\n**Why this is wrong:**\n- All content visible immediately (code examples, problems list, rationale)\n- Creates visual clutter in PR conversation\n- Makes it hard to scan multiple issues quickly\n\n**Correct approach:**\n```markdown\n❌ **CRITICAL**: Missing test coverage for security-critical code\n\n<details>\n<summary>Details and fix</summary>\n\nThe `@OmitFromCoverage` annotation excludes this entire class from test coverage.\n\n**Problems:**\n1. No validation that certificate hashes match actual Bitwarden certificates\n2. No verification of fail-closed behavior on edge cases\n3. No tests for multiple signer rejection logic\n4. Certificate hash typos would go undetected until production\n\n**Recommendation:**\nReplace `@OmitFromCoverage` with proper unit tests.\n\nExample test structure:\n[code block]\n\nSecurity-critical code should have the highest test coverage, not be omitted.\n</details>\n```\n\n**Key difference:** Only severity + one-line description visible. All details collapsed.\n\n---\n\n## Summary\n\n**Always use:**\n- Minimal summary (verdict + critical issues)\n- Separate inline comments with `<details>` tags\n- Hybrid emoji + text severity prefixes\n- Focus exclusively on actionable feedback\n\n**Never use:**\n- Multiple summary sections (Strengths, Recommendations, etc.)\n- Praise-only inline comments\n- Duplication between summary and inline comments\n- Verbose analysis in summary (belongs in inline comments)\n","isInternal":false,"tokens":3027,"sizeBytes":13065},{"name":"architectural-patterns.md","path":".claude/skills/reviewing-changes/reference/architectural-patterns.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/reference/architectural-patterns.md","title":"Reference Skill","category":"anthropic-skill","format":"markdown","content":"# Architectural Patterns Quick Reference\n\nQuick reference for Bitwarden Android architectural patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## Table of Contents\n\n**Core Patterns:**\n- [MVVM + UDF Pattern](#mvvm--udf-pattern)\n  - [ViewModel Structure](#viewmodel-structure)\n  - [UI Layer (Compose)](#ui-layer-compose)\n- [Hilt Dependency Injection](#hilt-dependency-injection)\n  - [ViewModels](#viewmodels)\n  - [Repositories and Managers](#repositories-and-managers)\n  - [Clock/Time Handling](#clocktime-handling)\n- [Module Organization](#module-organization)\n- [Error Handling](#error-handling)\n  - [Use Result Types, Not Exceptions](#use-result-types-not-exceptions)\n- [Quick Checklist](#quick-checklist)\n\n---\n\n## MVVM + UDF Pattern\n\n### ViewModel Structure\n\n**✅ GOOD - Proper state encapsulation**:\n```kotlin\n@HiltViewModel\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository\n) : ViewModel() {\n    // Private mutable state\n    private val _state = MutableStateFlow<FeatureState>(FeatureState.Initial)\n\n    // Public immutable state\n    val state: StateFlow<FeatureState> = _state.asStateFlow()\n\n    // Actions as functions, state updated via internal action\n    fun onActionClicked() {\n        viewModelScope.launch {\n            val result = repository.performAction()\n            sendAction(FeatureAction.Internal.ActionComplete(result))\n        }\n    }\n\n    // The ViewModel has a handler that processes the internal action\n    private fun handleInternalAction(action: FeatureAction.Internal) {\n        when (action) {\n            is FeatureAction.Internal.ActionComplete -> {\n                // The action handler evaluates the result and updates state\n                action.result.fold(\n                    onSuccess = { _state.value = State.Success },\n                    onFailure = { _state.value = State.Error(it) }\n                )\n            }\n        }\n    }\n}\n```\n\n**❌ BAD - Common violations**:\n```kotlin\nclass FeatureViewModel : ViewModel() {\n    // ❌ Exposes mutable state\n    val state: MutableStateFlow<FeatureState>\n\n    // ❌ Business logic in ViewModel\n    fun onSubmit() {\n        val encrypted = encryptionManager.encrypt(data)  // Should be in Repository\n        _state.value = FeatureState.Success\n    }\n\n    // ❌ Direct Android framework dependency\n    fun onCreate(context: Context) {  // ViewModels shouldn't depend on Context\n        // ...\n    }\n}\n```\n\n**Key Rules**:\n- Expose `StateFlow<T>`, never `MutableStateFlow<T>`\n- Delegate business logic to Repository/Manager\n- No direct Android framework dependencies (except ViewModel, SavedStateHandle)\n- Use `viewModelScope` for coroutines\n\nReference: `docs/ARCHITECTURE.md#mvvm-pattern`\n\n---\n\n### UI Layer (Compose)\n\n**✅ GOOD - Stateless, observes only**:\n```kotlin\n@Composable\nfun FeatureScreen(\n    state: FeatureState,\n    onActionClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    Column(modifier = modifier) {\n        when (state) {\n            is FeatureState.Loading -> LoadingIndicator()\n            is FeatureState.Success -> SuccessContent(state.data)\n            is FeatureState.Error -> ErrorMessage(state.error)\n        }\n\n        BitwardenButton(\n            text = \"Action\",\n            onClick = onActionClick  // Sends event to ViewModel\n        )\n    }\n}\n```\n\n**❌ BAD - Stateful, modifies state**:\n```kotlin\n@Composable\nfun FeatureScreen(viewModel: FeatureViewModel) {\n    var localState by remember { mutableStateOf(...) }  // ❌ State in UI\n\n    Button(onClick = {\n        viewModel._state.value = FeatureState.Loading  // ❌ Directly modifying ViewModel state\n    })\n}\n```\n\n**Key Rules**:\n- Compose screens observe state, never modify\n- User actions passed as events/callbacks to ViewModel\n- No business logic in UI layer\n- Use existing components from `:ui` module\n\n---\n\n## Hilt Dependency Injection\n\n### ViewModels\n\n**✅ GOOD - Interface injection**:\n```kotlin\n@HiltViewModel\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository,  // Interface, not implementation\n    private val authManager: AuthManager,\n    savedStateHandle: SavedStateHandle\n) : ViewModel()\n```\n\n**❌ BAD - Common violations**:\n```kotlin\n// ❌ No @HiltViewModel annotation\nclass FeatureViewModel @Inject constructor(...)\n\n// ❌ Injecting implementation instead of interface\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepositoryImpl  // Should inject interface\n)\n\n// ❌ Manual instantiation\nclass FeatureViewModel : ViewModel() {\n    private val repository = FeatureRepositoryImpl()  // Should use @Inject\n}\n```\n\n**Key Rules**:\n- Annotate with `@HiltViewModel`\n- Use `@Inject constructor`\n- Inject interfaces, not implementations\n- Use `SavedStateHandle` for process death survival\n\nReference: `docs/ARCHITECTURE.md#dependency-injection`\n\n---\n\n### Repositories and Managers\n\n**✅ GOOD - Implementation with @Inject**:\n```kotlin\ninterface FeatureRepository {\n    suspend fun fetchData(): Result<Data>\n}\n\nclass FeatureRepositoryImpl @Inject constructor(\n    private val apiService: FeatureApiService,\n    private val database: FeatureDao\n) : FeatureRepository {\n    override suspend fun fetchData(): Result<Data> = runCatching {\n        apiService.getData()\n    }\n}\n```\n\n**Module provides interface**:\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nabstract class DataModule {\n    @Binds\n    @Singleton\n    abstract fun bindFeatureRepository(\n        impl: FeatureRepositoryImpl\n    ): FeatureRepository\n}\n```\n\n**Key Rules**:\n- Define interface for abstraction\n- Implementation uses `@Inject constructor`\n- Module binds implementation to interface\n- Appropriate scoping (`@Singleton`, `@ViewModelScoped`)\n\n---\n\n### Clock/Time Handling\n\nTime-dependent code must use injected `Clock` rather than direct `Instant.now()` or `DateTime.now()` calls. This follows the same DI principle as other dependencies.\n\n**✅ GOOD - Injected Clock**:\n```kotlin\n// ViewModel with Clock injection\nclass MyViewModel @Inject constructor(\n    private val clock: Clock,\n) {\n    fun save() {\n        val timestamp = clock.instant()\n    }\n}\n\n// Extension function with Clock parameter\nfun State.getTimestamp(clock: Clock): Instant =\n    existingTime ?: clock.instant()\n```\n\n**❌ BAD - Static/direct calls**:\n```kotlin\n// Hidden dependency, non-testable\nval timestamp = Instant.now()\nval dateTime = DateTime.now()\n```\n\n**Key Rules**:\n- Inject `Clock` via Hilt constructor (like other dependencies)\n- Pass `Clock` as parameter to extension functions\n- `Clock` is provided via `CoreModule` as singleton\n- Enables deterministic testing with `Clock.fixed(...)`\n\nReference: `docs/STYLE_AND_BEST_PRACTICES.md#best-practices--time-and-clock-handling`\n\n---\n\n## Module Organization\n\n```\nandroid/\n├── core/           # Shared utilities (cryptography, analytics, logging)\n├── data/           # Repositories, database, domain models\n├── network/        # API clients, network utilities\n├── ui/             # Reusable Compose components, theme\n├── app/            # Application, feature screens, ViewModels\n└── authenticator/  # Authenticator app (separate from password manager)\n```\n\n**Correct Placement**:\n- UI screens and ViewModels → `:app`\n- Reusable Compose components → `:ui`\n- Data models and Repositories → `:data`\n- API services → `:network`\n- Cryptography, logging → `:core`\n\n**Check for**:\n- No circular dependencies\n- Correct module placement\n- Proper visibility (internal vs public)\n\nReference: `docs/ARCHITECTURE.md#module-structure`\n\n---\n\n## Error Handling\n\n### Use Result Types, Not Exceptions\n\n**✅ GOOD - Result-based**:\n```kotlin\n// Repository\nsuspend fun fetchData(): Result<Data> = runCatching {\n    apiService.getData()\n}\n\n// ViewModel\nfun onFetch() {\n    viewModelScope.launch {\n        val result = repository.fetchData()\n        sendAction(FeatureAction.Internal.FetchComplete(result))\n    }\n}\n```\n\n**❌ BAD - Exception-based in business logic**:\n```kotlin\n// ❌ Don't throw in business logic\nsuspend fun fetchData(): Data {\n    try {\n        return apiService.getData()\n    } catch (e: Exception) {\n        throw FeatureException(e)  // Don't throw in repositories\n    }\n}\n\n// ❌ Try-catch in ViewModel\nfun onFetch() {\n    viewModelScope.launch {\n        try {\n            val data = repository.fetchData()\n            sendAction(FeatureAction.Internal.FetchComplete(data))\n        } catch (e: Exception) {\n            sendAction(FeatureAction.Internal.FetchComplete(e))\n        }\n    }\n}\n```\n\n**Key Rules**:\n- Use `Result<T>` return types in repositories\n- Use `runCatching { }` to wrap API calls\n- Handle results with `.fold()` in ViewModels\n- Don't throw exceptions in business logic\n\nReference: `docs/ARCHITECTURE.md#error-handling`\n\n---\n\n## Quick Checklist\n\n### Architecture\n- [ ] ViewModels expose StateFlow, not MutableStateFlow?\n- [ ] Business logic in Repository, not ViewModel?\n- [ ] Using Hilt DI (@HiltViewModel, @Inject constructor)?\n- [ ] Injecting interfaces, not implementations?\n- [ ] Time-dependent code uses injected `Clock` (not `Instant.now()`)?\n- [ ] Correct module placement?\n\n### Error Handling\n- [ ] Using Result types, not exceptions in business logic?\n- [ ] Errors handled with .fold() in ViewModels?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n","isInternal":false,"tokens":2166,"sizeBytes":9533},{"name":"priority-framework.md","path":".claude/skills/reviewing-changes/reference/priority-framework.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/reference/priority-framework.md","title":"Reference Skill","category":"anthropic-skill","format":"markdown","content":"# Finding Priority Framework\n\nUse this framework to classify findings during code review. Clear prioritization helps authors triage and address issues effectively.\n\n## Table of Contents\n\n**Severity Categories:**\n- [❌ CRITICAL (Blocker - Must Fix Before Merge)](#critical-blocker---must-fix-before-merge)\n- [⚠️ IMPORTANT (Should Fix)](#important-should-fix)\n- [♻️ DEBT (Technical Debt)](#debt-technical-debt)\n- [🎨 SUGGESTED (Nice to Have)](#suggested-nice-to-have)\n- [❓ QUESTION (Seeking Clarification)](#question-seeking-clarification)\n- [Optional (Acknowledge But Don't Require)](#optional-acknowledge-but-dont-require)\n\n**Guidelines:**\n- [Classification Guidelines](#classification-guidelines)\n  - [When Something is Between Categories](#when-something-is-between-categories)\n  - [Context Matters](#context-matters)\n- [Examples by Change Type](#examples-by-change-type)\n- [Special Cases](#special-cases)\n- [Summary](#summary)\n\n---\n\n## ❌ **CRITICAL** (Blocker - Must Fix Before Merge)\n\nThese issues **must** be addressed before the PR can be merged. They pose immediate risks to security, stability, or architecture integrity.\n\n### Security\n- Data leaks or plaintext sensitive data (passwords, keys, tokens)\n- Weak encryption or insecure key storage\n- Missing authentication or authorization checks\n- Input injection vulnerabilities (SQL, XSS, command injection)\n- Sensitive data in logs or error messages\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption\nPIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.\nReference: docs/ARCHITECTURE.md#security\n```\n\n### Stability\n- Compilation errors or warnings\n- Null pointer exceptions in production paths\n- Resource leaks (file handles, network connections, memory)\n- Crashes or unhandled exceptions in critical paths\n- Thread safety violations\n\n**Example**:\n```\n**app/auth/BiometricRepository.kt:120** - CRITICAL: Missing null safety check\nbiometricPrompt result can be null. Add explicit null check to prevent crash.\n```\n\n### Architecture\n- Mutable state exposure in ViewModels (violates MVVM)\n- Exception-based error handling in business logic (should use Result)\n- Circular dependencies between modules\n- Violation of zero-knowledge principles\n- Direct dependency instantiation (should use DI)\n\n**Example**:\n```\n**app/login/LoginViewModel.kt:45** - CRITICAL: Exposes mutable state\nChange MutableStateFlow to StateFlow in public API to prevent external state mutation.\nThis violates MVVM encapsulation pattern.\n```\n\n---\n\n## ⚠️ **IMPORTANT** (Should Fix)\n\nThese issues should be addressed but don't block merge if there's a compelling reason. They improve code quality, maintainability, or robustness.\n\n### Testing\n- Missing tests for critical paths (authentication, encryption, data sync)\n- Missing tests for new public APIs\n- Tests that don't verify actual behavior (test implementation, not behavior)\n- Missing test coverage for error scenarios\n\n**Example**:\n```\n**data/auth/BiometricRepository.kt** - IMPORTANT: Missing test for cancellation\nAdd test for user cancellation scenario to prevent regression.\n```\n\n### Architecture\n- Inconsistent patterns within PR (mixing error handling approaches)\n- Poor separation of concerns\n- Tight coupling between components\n- Not following established project patterns\n\n**Example**:\n```\n**app/vault/VaultViewModel.kt:89** - IMPORTANT: Business logic in ViewModel\nEncryption logic should be in Repository, not ViewModel.\nReference: docs/ARCHITECTURE.md#mvvm-pattern\n```\n\n### Documentation\n- Undocumented public APIs (missing KDoc)\n- Missing documentation for complex algorithms\n- Unclear naming or confusing interfaces\n\n**Example**:\n```\n**core/crypto/EncryptionManager.kt:34** - IMPORTANT: Missing KDoc\nPublic encryption method should document parameters, return value, and exceptions.\n```\n\n### Performance\n- Inefficient algorithms in hot paths (with evidence from profiling)\n- Blocking main thread with I/O operations\n- Memory inefficient data structures (with evidence)\n\n**Example**:\n```\n**app/vault/VaultListViewModel.kt:78** - IMPORTANT: N+1 query pattern\nFetching items one-by-one in loop. Consider batch fetch to reduce database queries.\n```\n\n---\n\n## ♻️ **DEBT** (Technical Debt)\n\nCode that duplicates existing patterns, violates established conventions, or will require rework within 6 months. Introduces technical debt that should be tracked for future cleanup.\n\n### Duplication\n- Copy-pasted code blocks across files\n- Repeated validation or business logic\n- Multiple implementations of same pattern\n- Data transformation duplicated in multiple places\n\n**Example**:\n```\n**app/vault/VaultListViewModel.kt:156** - DEBT: Duplicates encryption logic\nSame encryption pattern exists in VaultRepository.kt:234 and SyncManager.kt:89.\nExtract to shared EncryptionUtil to reduce maintenance burden.\n```\n\n### Convention Violations\n- Inconsistent error handling approaches within same module\n- Mixing architectural patterns (MVVM + MVC)\n- Not following established DI patterns\n- Deviating from project code style significantly\n\n**Example**:\n```\n**data/auth/AuthRepository.kt:78** - DEBT: Exception-based error handling\nProject standard is Result<T> for error handling. This uses try-catch with throws.\nCreates inconsistency and makes testing harder.\nReference: docs/ARCHITECTURE.md#error-handling\n```\n\n### Future Rework Required\n- Hardcoded values that should be configurable\n- Temporary workarounds without TODO/FIXME\n- Code that will need changes when planned features arrive\n- Tight coupling that prevents future extensibility\n\n**Example**:\n```\n**app/settings/SettingsViewModel.kt:45** - DEBT: Hardcoded feature flags\nFeature flags should come from remote config for A/B testing.\nWill require rework when experimentation framework launches.\n```\n\n---\n\n## 🎨 **SUGGESTED** (Nice to Have)\n\nImprovements with measurable value only. A finding qualifies as SUGGESTED if it provides: security gain, cyclomatic complexity reduction, bug class prevention, or elimination of an O(n²) pattern. Subjective style preferences, vague simplifications, and naming nitpicks do not qualify — leave those out entirely or raise in conversation.\n\n### Code Quality\n- Extractable duplicated logic that reduces measurable complexity or improves testability\n- Patterns that would prevent a recurring bug class in this module\n- Architecture improvements that eliminate tight coupling with measurable impact\n\n**Example**:\n```\n**app/vault/VaultScreen.kt:145** - SUGGESTED: Consider extracting helper function\nThis 20-line block appears in 3 places. Consider extracting to reduce duplication.\n```\n\n### Testing\n- Additional test coverage for edge cases (beyond critical paths)\n- More comprehensive integration tests\n- Performance tests for non-critical paths\n\n**Example**:\n```\n**app/login/LoginViewModelTest.kt** - SUGGESTED: Add test for concurrent login attempts\nNot critical, but would increase confidence in edge case handling.\n```\n\n### Refactoring\n- Extracting reusable patterns\n- Modernizing old patterns (if touching related code)\n- Improving testability\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:200** - SUGGESTED: Consider extracting validation logic\nCould be extracted to separate validator class for reusability and testing.\n```\n\n---\n\n## ❓ **QUESTION** (Seeking Clarification)\n\nQuestions about requirements, unclear intent, or potential conflicts that require human knowledge to answer. Open inquiries that cannot be resolved through code inspection alone.\n\n### Requirements Clarification\n- Ambiguous acceptance criteria\n- Multiple valid implementation approaches\n- Unclear business rules or edge case handling\n- Conflicting requirements between specs and implementation\n\n**Example**:\n```\n**app/vault/ItemListViewModel.kt:67** - QUESTION: Expected sort behavior for equal timestamps?\nWhen items have identical timestamps, should secondary sort be by:\n- Name (alphabetical)\n- Creation order\n- Item type priority\n\nSpec doesn't specify tie-breaking logic.\n```\n\n### Design Decisions\n- Architecture choices that could go multiple ways\n- Trade-offs between approaches without clear winner\n- Feature flag strategy or rollout approach\n- API design with multiple valid patterns\n\n**Example**:\n```\n**data/sync/SyncManager.kt:134** - QUESTION: Should sync failures retry automatically?\nCurrent implementation fails immediately. Options:\n- Exponential backoff (3 retries)\n- User-triggered retry only\n- Background retry on network restore\n\nWhat's the expected UX?\n```\n\n### System Integration\n- Unclear contracts with external systems\n- Potential conflicts with other features/modules\n- Assumptions about third-party API behavior\n- Cross-team coordination needs\n\n**Example**:\n```\n**app/auth/BiometricPrompt.kt:89** - QUESTION: Compatibility with pending device credentials PR?\nPR #1234 is refactoring device credentials. Should this:\n- Merge first and adapt later\n- Wait for #1234 to land\n- Coordinate with that author\n\nTiming unclear.\n```\n\n### Testing Strategy\n- Uncertainty about test scope or approach\n- Questions about mocking external dependencies\n- Edge cases that need product input\n- Performance testing requirements\n\n**Example**:\n```\n**data/vault/EncryptionTest.kt:45** - QUESTION: Should we test against real Keystore?\nCurrently using mocked Keystore. Real Keystore testing would:\n+ Catch hardware-specific issues\n- Slow down CI significantly\n- Require API 23+ emulators\n\nWhat's the priority?\n```\n\n---\n\n## Optional (Acknowledge But Don't Require)\n\nNote good practices to reinforce positive patterns. Keep these **brief** - list only, no elaboration.\n\n### Good Practices\n\n**Format**: Simple bullet list, no explanation\n\n```markdown\n## Good Practices\n- Proper Hilt DI usage throughout\n- Comprehensive unit test coverage\n- Clear separation of concerns\n- Well-documented public APIs\n```\n\n**Don't do this** (too verbose):\n```markdown\n## Good Practices\n- Proper Hilt DI usage throughout: Great job using @Inject constructor and injecting interfaces! This follows our established patterns perfectly and makes the code very testable. Really excellent work here! 👍\n```\n\n---\n\n## Classification Guidelines\n\n### When Something is Between Categories\n\n**If unsure between Critical and Important**:\n- Ask: \"Could this cause production incidents, data loss, or security breaches?\"\n- If yes → Critical\n- If no → Important\n\n**If unsure between Important and Debt**:\n- Ask: \"Is this a bug/defect or just duplication/inconsistency?\"\n- If bug/defect → Important\n- If duplication/inconsistency → Debt\n\n**If unsure between Important and Suggested**:\n- Ask: \"Would I block merge over this?\"\n- If yes → Important\n- If no → Suggested\n\n**If unsure between Debt and Suggested**:\n- Ask: \"Will this require rework within 6 months?\"\n- If yes → Debt\n- If no → Suggested\n\n**If unsure between Suggested and Question**:\n- Ask: \"Am I requesting a change or asking for clarification?\"\n- If requesting change → Suggested\n- If seeking clarification → Question\n\n**If unsure between Suggested and Optional**:\n- Ask: \"Is this actionable feedback or just acknowledgment?\"\n- If actionable → Suggested\n- If acknowledgment → Optional\n\n### Context Matters\n\n**Same issue, different contexts**:\n\n```\n// Critical for production code\nMissing null safety check in auth flow → CRITICAL\n\n// Suggested for internal test utility\nMissing null safety check in test helper → SUGGESTED\n```\n\n**Same pattern, different risk levels**:\n\n```\n// Critical for new feature\nMissing tests for new auth method → CRITICAL\n\n// Important for bug fix\nMissing regression test → IMPORTANT\n\n// Suggested for refactoring\nMissing tests for refactored helper → SUGGESTED\n```\n\n---\n\n## Examples by Change Type\n\n### Dependency Update\n- **Critical**: Known CVEs in old version not addressed\n- **Important**: Breaking changes that need migration\n- **Suggested**: Beta/alpha version stability concerns\n\n### Bug Fix\n- **Critical**: Fix doesn't address root cause\n- **Important**: Missing regression test\n- **Suggested**: Similar bugs in related code\n\n### Feature Addition\n- **Critical**: Security vulnerabilities, architecture violations\n- **Important**: Missing tests for critical paths\n- **Suggested**: Additional test coverage, minor refactoring\n\n### UI Refinement\n- **Critical**: Missing accessibility for key actions\n- **Important**: Not using theme (hardcoded colors)\n- **Suggested**: Minor spacing/alignment improvements\n\n### Refactoring\n- **Critical**: Changes behavior (should be behavior-preserving)\n- **Important**: Incomplete migration (mix of old/new patterns)\n- **Suggested**: Additional instances that could be refactored\n\n### Infrastructure\n- **Critical**: Hardcoded secrets, no rollback plan\n- **Important**: Performance regression in build times\n- **Suggested**: Further optimization opportunities\n\n---\n\n## Special Cases\n\n### Technical Debt\n- Acknowledge existing tech debt but don't require fixing in unrelated PR\n- Exception: If change makes tech debt worse, it's Important to address\n\n### Scope Creep\n- Don't request changes outside PR scope\n- Can note as \"Future consideration\" but not required for this PR\n\n### Linter-Catchable Issues\n- Don't flag issues that automated tools handle\n- Exception: If linter is misconfigured and missing real issues\n\n### Personal Preferences\n- Don't flag unless grounded in project standards or architectural principles\n- Use \"I-statements\" if suggesting alternative approaches\n\n---\n\n## Summary\n\n**Critical**: Block merge, must fix (security, stability, architecture)\n**Important**: Should fix before merge (testing, quality, performance)\n**Debt**: Technical debt introduced, track for future cleanup\n**Suggested**: Nice to have, consider effort vs benefit\n**Question**: Seeking clarification on requirements or design\n**Optional**: Acknowledge good practices, keep brief\n","isInternal":false,"tokens":2997,"sizeBytes":13861},{"name":"review-psychology.md","path":".claude/skills/reviewing-changes/reference/review-psychology.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/reference/review-psychology.md","title":"Reference Skill","category":"anthropic-skill","format":"markdown","content":"# Review Psychology: Constructive Feedback Phrasing\n\nEffective code review feedback is clear, actionable, and constructive. This guide provides phrasing patterns for inline comments.\n\n## Table of Contents\n\n**Guidelines:**\n- [Phrasing Templates](#phrasing-templates)\n  - [Critical Issues (Prescriptive)](#critical-issues-prescriptive)\n  - [Suggested Improvements (Exploratory)](#suggested-improvements-exploratory)\n  - [Questions (Collaborative)](#questions-collaborative)\n  - [Test Suggestions](#test-suggestions)\n- [When to Be Prescriptive vs Ask Questions](#when-to-be-prescriptive-vs-ask-questions)\n- [Special Cases](#special-cases)\n\n---\n\n## Phrasing Templates\n\n### Critical Issues (Prescriptive)\n\n**Pattern**: State problem + Provide solution + Explain why\n\n```\n**[file:line]** - CRITICAL: [Issue description]\n\n[Specific fix with code example if applicable]\n\n[Rationale explaining why this is critical]\n\nReference: [docs link if applicable]\n```\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption\n\nPIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.\nPlaintext storage exposes the PIN to backup systems and rooted devices.\n\nReference: docs/ARCHITECTURE.md#security\n```\n\n---\n\n### Suggested Improvements (Exploratory)\n\n**Pattern**: Observe + Suggest + Explain benefit\n\n```\n**[file:line]** - Consider [alternative approach]\n\n[Current observation]\nCan we [specific suggestion]?\n\n[Benefit or rationale]\n```\n\n**Example**:\n```\n**app/login/LoginScreen.kt:89** - Consider using existing BitwardenButton\n\nThis custom button implementation looks similar to `ui/components/BitwardenButton.kt:45`.\nCan we use the existing component to maintain consistency across the app?\n```\n\n---\n\n### Questions (Collaborative)\n\n**Pattern**: Ask + Provide context (optional)\n\n```\n**[file:line]** - [Question about intent or approach]?\n\n[Optional context or observation]\n```\n\n**Example**:\n```\n**data/sync/SyncManager.kt:234** - How does this handle concurrent sync attempts?\n\nIt looks like multiple coroutines could call `startSync()` simultaneously.\nIs there a mechanism to prevent race conditions, or is that handled elsewhere?\n```\n\n---\n\n### Test Suggestions\n\n**Pattern**: Observe gap + Suggest specific test + Provide skeleton\n\n```\n**[file:line]** - Consider adding test for [scenario]\n\n[Rationale]\n\n```kotlin\n@Test\nfun `test description`() = runTest {\n    // Test skeleton\n}\n```\n```\n\n**Example**:\n```\n**data/auth/BiometricRepository.kt** - Consider adding test for cancellation scenario\n\nThis would prevent regression of the bug you just fixed:\n\n```kotlin\n@Test\nfun `when biometric cancelled then returns cancelled state`() = runTest {\n    coEvery { biometricPrompt.authenticate() } returns null\n\n    val result = repository.authenticate()\n\n    assertEquals(AuthResult.Cancelled, result)\n}\n```\n```\n\n---\n\n## When to Be Prescriptive vs Ask Questions\n\n**Be Prescriptive** (Tell them what to do):\n- Security issues\n- Architecture pattern violations\n- Null safety problems\n- Compilation errors\n- Documented project standards\n\n**Ask Questions** (Seek explanation):\n- Design decisions with multiple valid approaches\n- Performance trade-offs without data\n- Unclear intent or reasoning\n- Scope decisions (this PR vs future work)\n- Patterns not documented in project guidelines\n\n---\n\n## Special Cases\n\n**Nitpicks** - For truly minor suggestions, use \"Nit:\" prefix:\n```\n**Nit**: Extra blank line at line 145\n```\n\n**Uncertainty** - If unsure, acknowledge it:\n```\nI'm not certain, but this might be called frequently.\nHas this been profiled?\n```\n\n**Positive Feedback** - Brief list only, no elaboration:\n```\n## Good Practices\n- Proper Hilt DI usage throughout\n- Comprehensive unit test coverage\n- Clear separation of concerns\n```\n","isInternal":false,"tokens":868,"sizeBytes":3777},{"name":"security-patterns.md","path":".claude/skills/reviewing-changes/reference/security-patterns.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/reference/security-patterns.md","title":"Reference Skill","category":"anthropic-skill","format":"markdown","content":"# Security Patterns Quick Reference\n\nQuick reference for Bitwarden Android security patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md#security`.\n\n## Encryption and Key Storage\n\n**✅ GOOD - Android Keystore**:\n```kotlin\n// Sensitive data encrypted with Keystore\nclass SecureStorage @Inject constructor(\n    private val keystoreManager: KeystoreManager\n) {\n    suspend fun storePin(pin: String): Result<Unit> = runCatching {\n        val encrypted = keystoreManager.encrypt(pin.toByteArray())\n        securePreferences.putBytes(KEY_PIN, encrypted)\n    }\n}\n\n// Or use EncryptedSharedPreferences\nval encryptedPrefs = EncryptedSharedPreferences.create(\n    context,\n    \"secure_prefs\",\n    masterKey,\n    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n)\n```\n\n**❌ BAD - Plaintext or weak encryption**:\n```kotlin\n// ❌ CRITICAL - Plaintext storage\nsharedPreferences.edit {\n    putString(\"pin\", userPin)  // Never store sensitive data in plaintext\n}\n\n// ❌ CRITICAL - Weak encryption\nval cipher = Cipher.getInstance(\"DES\")  // Use AES-256-GCM\n\n// ❌ CRITICAL - Hardcoded keys\nval key = \"my_secret_key_123\"  // Use Android Keystore\n```\n\n**Key Rules**:\n- Use Android Keystore for encryption keys\n- Use EncryptedSharedPreferences for simple key-value storage\n- Use AES-256-GCM for encryption\n- Never store sensitive data in plaintext\n- Never hardcode encryption keys\n\nReference: `docs/ARCHITECTURE.md#security`\n\n---\n\n## Logging Sensitive Data\n\n**✅ GOOD - No sensitive data**:\n```kotlin\nLog.d(TAG, \"Authentication attempt for user\")\nLog.d(TAG, \"Vault sync completed with ${items.size} items\")\n```\n\n**❌ BAD - Logs sensitive data**:\n```kotlin\n// ❌ CRITICAL\nLog.d(TAG, \"Password: $password\")\nLog.d(TAG, \"Auth token: $token\")\nLog.d(TAG, \"PIN: $pin\")\nLog.d(TAG, \"Encryption key: ${key.encoded}\")\n```\n\n**Key Rules**:\n- Never log passwords, PINs, tokens, keys\n- Never log encryption keys or sensitive data\n- Be careful with error messages (don't include sensitive context)\n\n---\n\n## Quick Checklist\n\n### Security\n- [ ] Sensitive data encrypted with Keystore?\n- [ ] No plaintext passwords/keys?\n- [ ] No sensitive data in logs?\n- [ ] Using AES-256-GCM for encryption?\n- [ ] No hardcoded encryption keys?\n\n---\n\nFor comprehensive security details, always refer to:\n- `docs/ARCHITECTURE.md#security` - Complete security architecture and zero-knowledge principles\n","isInternal":false,"tokens":595,"sizeBytes":2468},{"name":"style-patterns.md","path":".claude/skills/reviewing-changes/reference/style-patterns.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/reference/style-patterns.md","title":"Reference Skill","category":"anthropic-skill","format":"markdown","content":"# Style Patterns Quick Reference\n\nProject-specific Kotlin style rules to catch during code review. These supplement (not replace) `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## `when` branches with wrapped right-hand side require curly braces\n\nWhen a `when` branch's expression is too long to fit on the same line as `->` and is wrapped to its own line, the body must be wrapped in `{ }`. A bare `->` followed by an indented expression on the next line should be flagged.\n\n**Flag this:**\n\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT ->\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n}\n```\n\n**Accept this:**\n\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT -> {\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    }\n}\n```\n\nSingle-line branches (body fits alongside `->`) do **not** require braces.\n\n**Suggested classification:** SUGGESTED (style consistency, not correctness).\n","isInternal":false,"tokens":258,"sizeBytes":1110},{"name":"testing-patterns.md","path":".claude/skills/reviewing-changes/reference/testing-patterns.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/reference/testing-patterns.md","title":"Reference Skill","category":"anthropic-skill","format":"markdown","content":"# Testing Patterns Quick Reference\n\nQuick reference for Bitwarden Android testing patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## ViewModel Tests\n\n**✅ GOOD - Tests behavior**:\n```kotlin\n@Test\nfun `when login succeeds then state updates to success`() = runTest {\n    // Arrange\n    val viewModel = LoginViewModel(mockRepository)\n    coEvery { mockRepository.login(any(), any()) } returns Result.success(User())\n\n    // Act\n    viewModel.onLoginClicked(\"user@example.com\", \"password\")\n\n    // Assert\n    viewModel.state.test {\n        assertEquals(LoginState.Loading, awaitItem())\n        assertEquals(LoginState.Success, awaitItem())\n    }\n}\n```\n\n**❌ BAD - Tests implementation**:\n```kotlin\n@Test\nfun `repository is called with correct parameters`() {\n    // ❌ This tests implementation details, not behavior\n    viewModel.onLoginClicked(\"user\", \"pass\")\n    coVerify { mockRepository.login(\"user\", \"pass\") }\n}\n```\n\n**Key Rules**:\n- Test behavior, not implementation\n- Use `runTest` for coroutine tests\n- Use Turbine for Flow testing\n- Use MockK for mocking\n\n---\n\n## Repository Tests\n\n**✅ GOOD - Tests data transformations**:\n```kotlin\n@Test\nfun `fetchItems maps API response to domain model`() = runTest {\n    // Arrange\n    val apiResponse = listOf(ApiItem(id = \"1\", name = \"Test\"))\n    coEvery { apiService.getItems() } returns apiResponse\n\n    // Act\n    val result = repository.fetchItems()\n\n    // Assert\n    assertTrue(result.isSuccess)\n    assertEquals(\n        listOf(DomainItem(id = \"1\", name = \"Test\")),\n        result.getOrThrow()\n    )\n}\n```\n\n**Key Rules**:\n- Test data transformations\n- Test error handling (network failures, API errors)\n- Test caching behavior if applicable\n- Mock API services and databases\n\nReference: Project uses JUnit 5, MockK, Turbine, kotlinx-coroutines-test\n\n---\n\n## Null Safety\n\n**✅ GOOD - Safe handling**:\n```kotlin\n// Safe call with elvis operator\nval result = apiService.getData() ?: return State.Error(\"No data\")\n\n// Let with safe call\nintent?.getStringExtra(\"key\")?.let { value ->\n    processValue(value)\n}\n\n// Require with message\nval data = requireNotNull(response.data) { \"Response data must not be null\" }\n```\n\n**❌ BAD - Unsafe assertions**:\n```kotlin\n// ❌ Unsafe - can crash\nval result = apiService.getData()!!\n\n// ❌ Platform type unchecked\nval intent: Intent = getIntent()  // Could be null from Java\nval value = intent.getStringExtra(\"key\")  // Potential NPE\n```\n\n**Key Rules**:\n- Avoid `!!` unless safety is guaranteed (rare)\n- Handle platform types with explicit nullability\n- Use safe calls (`?.`), elvis operator (`?:`), or explicit checks\n- Use `requireNotNull` with descriptive message if crash is acceptable\n\n---\n\n## Quick Checklist\n\n### Testing\n- [ ] ViewModels have unit tests?\n- [ ] Tests verify behavior, not implementation?\n- [ ] Edge cases covered?\n- [ ] Error scenarios tested?\n\n### Code Quality\n- [ ] Null safety handled properly (no `!!` without guarantee)?\n- [ ] Public APIs have KDoc?\n- [ ] Following naming conventions?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n","isInternal":false,"tokens":772,"sizeBytes":3251},{"name":"ui-patterns.md","path":".claude/skills/reviewing-changes/reference/ui-patterns.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/reference/ui-patterns.md","title":"Reference Skill","category":"anthropic-skill","format":"markdown","content":"# Compose UI Patterns Quick Reference\n\nQuick reference for Bitwarden Android Compose UI patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## Component Reuse\n\n**✅ GOOD - Uses existing components**:\n```kotlin\nBitwardenButton(\n    text = \"Submit\",\n    onClick = onSubmit\n)\n\nBitwardenTextField(\n    value = text,\n    onValueChange = onTextChange,\n    label = \"Email\"\n)\n```\n\n**❌ BAD - Duplicates existing components**:\n```kotlin\n// ❌ Recreating BitwardenButton\nButton(\n    onClick = onSubmit,\n    colors = ButtonDefaults.buttonColors(\n        containerColor = BitwardenTheme.colorScheme.primary\n    )\n) {\n    Text(\"Submit\")\n}\n```\n\n**Key Rules**:\n- Check `:ui` module for existing components before creating custom ones\n- Use BitwardenButton, BitwardenTextField, etc. for consistency\n- Place new reusable components in `:ui` module\n\n---\n\n## Theme Usage\n\n**✅ GOOD - Uses theme**:\n```kotlin\nText(\n    text = \"Title\",\n    style = BitwardenTheme.typography.titleLarge,\n    color = BitwardenTheme.colorScheme.primary\n)\n\nSpacer(modifier = Modifier.height(16.dp))  // Standard spacing\n```\n\n**❌ BAD - Hardcoded values**:\n```kotlin\nText(\n    text = \"Title\",\n    style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold),  // Use theme\n    color = Color(0xFF0066FF)  // Use theme color\n)\n\nSpacer(modifier = Modifier.height(17.dp))  // Non-standard spacing\n```\n\n**Key Rules**:\n- Use `BitwardenTheme.colorScheme` for colors\n- Use `BitwardenTheme.typography` for text styles\n- Use standard spacing (4.dp, 8.dp, 16.dp, 24.dp)\n\n---\n\n## Quick Checklist\n\n### UI Patterns\n- [ ] Using existing Bitwarden components from `:ui` module?\n- [ ] Using BitwardenTheme for colors and typography?\n- [ ] Using standard spacing values (4, 8, 16, 24 dp)?\n- [ ] No hardcoded colors or text styles?\n- [ ] UI is stateless (observes state, doesn't modify)?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n","isInternal":false,"tokens":550,"sizeBytes":2074},{"name":"SKILL.md","path":".claude/skills/reviewing-changes/SKILL.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/reviewing-changes/SKILL.md","title":"reviewing-changes","category":"anthropic-skill","format":"markdown","content":"---\nname: reviewing-changes\ndescription: Android-specific code review checklist and MVVM/Compose pattern validation for Bitwarden Android — use this for any review task, even if the user doesn't explicitly ask for a \"checklist\". Detects change type automatically and loads the right review strategy (feature additions, bug fixes, UI refinements, refactoring, dependency updates, infrastructure). Triggered by \"review PR\", \"review changes\", \"review this code\", \"check this code\", \"Android review\", code review requests on Kotlin/ViewModel/Composable/Repository/Gradle files, or any time someone asks to look at a diff, PR, or code changes in bitwarden/android.\n---\n\n# Reviewing Changes - Android Additions\n\nThis skill provides Android-specific workflow additions that complement the base `bitwarden-code-reviewer` agent standards.\n\n## Instructions\n\n**IMPORTANT**: Work systematically through each step before providing feedback. Each checklist file includes structured thinking guidance for its review passes.\n\n### Step 1: Retrieve Additional Details\n\nRetrieve any additional information linked to the pull request using available tools (JIRA MCP, GitHub API).\n\nIf pull request title and message do not provide enough context, request additional details from the reviewer:\n- Link a JIRA ticket\n- Associate a GitHub issue\n- Link to another pull request\n- Add more detail to the PR title or body\n\n**Android metadata checks** — flag as ❓ if any of these are missing:\n- PR includes `*Screen.kt` or Composable changes but has no screenshots\n- PR adds new `ViewModel` or `Repository` but has no test plan or test file changes\n\n### Step 2: Detect Change Type with Android Refinements\n\nUse the base change type detection from the agent, with Android-specific refinements:\n\n**Android-specific patterns:**\n- **Feature Addition**: New `ViewModel`, new `Repository`, new `@Composable` functions, new `*Screen.kt` files\n- **UI Refinement**: Changes only in `*Screen.kt`, `*Composable.kt`, `ui/` package files\n- **Infrastructure**: Changes to `.github/workflows/`, `gradle/`, `build.gradle.kts`, `libs.versions.toml`\n- **Dependency Update**: Changes only to `libs.versions.toml` or `build.gradle.kts` with version bumps\n\n### Step 3: Load Appropriate Checklist\n\nBased on detected type, read the relevant checklist file:\n\n- **Dependency Update** → `checklists/dependency-update.md` (expedited review)\n- **Bug Fix** → `checklists/bug-fix.md` (focused review)\n- **Feature Addition** → `checklists/feature-addition.md` (comprehensive review)\n- **UI Refinement** → `checklists/ui-refinement.md` (design-focused review)\n- **Refactoring** → `checklists/refactoring.md` (pattern-focused review)\n- **Infrastructure** → `checklists/infrastructure.md` (tooling-focused review)\n\nThe checklist provides:\n- Multi-pass review strategy\n- Type-specific focus areas\n- What to check and what to skip\n- Structured thinking guidance\n\n### Step 4: Execute Review Following Checklist\n\nFollow the checklist's multi-pass strategy, thinking through each pass systematically.\n\n### Step 5: Consult Android Reference Materials As Needed\n\nLoad reference files only when needed for specific questions:\n\n- **Re-reviews** → invoke `reviewing-incremental-changes` agent skill; scope to changed lines only, do not flag new issues in unchanged code\n- **Issue prioritization** → `reference/priority-framework.md` (Critical vs Suggested vs Optional)\n- **Phrasing feedback** → `reference/review-psychology.md` (questions vs commands, I-statements)\n- **Architecture questions** → `reference/architectural-patterns.md` (MVVM, Hilt DI, module org, error handling)\n- **Security questions (quick reference)** → `reference/security-patterns.md` (common patterns and anti-patterns)\n- **Security questions (comprehensive)** → `docs/ARCHITECTURE.md#security` (full zero-knowledge architecture)\n- **Testing questions** → `reference/testing-patterns.md` (unit tests, mocking, null safety)\n- **UI questions** → `reference/ui-patterns.md` (Compose patterns, theming)\n- **Style questions (project-specific)** → `reference/style-patterns.md` (Kotlin rules enforced in review)\n- **Style questions (general)** → `docs/STYLE_AND_BEST_PRACTICES.md`\n\n## Core Principles\n\n- **Priority order**: Security → Correctness → Breaking Changes → Performance → Maintainability\n- **Appropriate depth**: Match review rigor to change complexity and risk\n- **Specific references**: Always use `file:line_number` format for precise location\n- **Actionable feedback**: Say what to do and why, not just what's wrong\n- **Efficient reviews**: Use multi-pass strategy, skip what's not relevant\n- **Android patterns**: Validate MVVM, Hilt DI, Compose conventions, Kotlin idioms\n","frontmatter":{"name":"reviewing-changes","description":"Android-specific code review checklist and MVVM/Compose pattern validation for Bitwarden Android — use this for any review task, even if the user doesn't explicitly ask for a \"checklist\". Detects change type automatically and loads the right review strategy (feature additions, bug fixes, UI refinements, refactoring, dependency updates, infrastructure). Triggered by \"review PR\", \"review changes\", \"review this code\", \"check this code\", \"Android review\", code review requests on Kotlin/ViewModel/Composable/Repository/Gradle files, or any time someone asks to look at a diff, PR, or code changes in bitwarden/android."},"isInternal":false,"tokens":1059,"sizeBytes":4727},{"name":"compose-screen-test-example.md","path":".claude/skills/testing-android-code/examples/compose-screen-test-example.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/testing-android-code/examples/compose-screen-test-example.md","title":"Examples Skill","category":"anthropic-skill","format":"markdown","content":"/**\n * Complete Compose Screen Test Example\n *\n * Key patterns demonstrated:\n * - Extending BitwardenComposeTest\n * - Mocking ViewModel with flows\n * - Testing UI interactions\n * - Testing navigation callbacks\n * - Using bufferedMutableSharedFlow for events\n * - Testing dialogs with isDialog() and hasAnyAncestor()\n */\npackage com.bitwarden.example.feature\n\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.filterToOne\nimport androidx.compose.ui.test.hasAnyAncestor\nimport androidx.compose.ui.test.isDialog\nimport androidx.compose.ui.test.onAllNodesWithText\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow\nimport com.bitwarden.ui.util.assertNoDialogExists\nimport com.bitwarden.ui.util.isProgressBar\nimport com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.verify\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport junit.framework.TestCase.assertTrue\nimport org.junit.Before\nimport org.junit.Test\n\nclass ExampleScreenTest : BitwardenComposeTest() {\n\n    // Track navigation callbacks\n    private var haveCalledNavigateBack = false\n    private var haveCalledNavigateToNext = false\n\n    // Use bufferedMutableSharedFlow for events (default replay = 0)\n    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n\n    // Mock ViewModel with relaxed = true\n    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        haveCalledNavigateBack = false\n        haveCalledNavigateToNext = false\n\n        setContent {\n            ExampleScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                onNavigateToNext = { haveCalledNavigateToNext = true },\n                viewModel = viewModel,\n            )\n        }\n    }\n\n    /**\n     * Test: Back button sends action to ViewModel\n     */\n    @Test\n    fun `on back click should send BackClick action`() {\n        composeTestRule\n            .onNodeWithContentDescription(\"Back\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.BackClick) }\n    }\n\n    /**\n     * Test: Submit button sends action to ViewModel\n     */\n    @Test\n    fun `on submit click should send SubmitClick action`() {\n        composeTestRule\n            .onNodeWithText(\"Submit\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.SubmitClick) }\n    }\n\n    /**\n     * Test: Loading state shows progress indicator\n     */\n    @Test\n    fun `loading state should display progress indicator`() {\n        mutableStateFlow.update { it.copy(isLoading = true) }\n\n        composeTestRule\n            .onNode(isProgressBar)\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Data state shows content\n     */\n    @Test\n    fun `data state should display content`() {\n        mutableStateFlow.update { it.copy(data = \"Test Data\") }\n\n        composeTestRule\n            .onNodeWithText(\"Test Data\")\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Error state shows error message\n     */\n    @Test\n    fun `error state should display error message`() {\n        mutableStateFlow.update { it.copy(errorMessage = \"Something went wrong\") }\n\n        composeTestRule\n            .onNodeWithText(\"Something went wrong\")\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: NavigateBack event triggers navigation callback\n     */\n    @Test\n    fun `NavigateBack event should call onNavigateBack`() {\n        mutableEventFlow.tryEmit(ExampleEvent.NavigateBack)\n\n        assertTrue(haveCalledNavigateBack)\n    }\n\n    /**\n     * Test: NavigateToNext event triggers navigation callback\n     */\n    @Test\n    fun `NavigateToNext event should call onNavigateToNext`() {\n        mutableEventFlow.tryEmit(ExampleEvent.NavigateToNext)\n\n        assertTrue(haveCalledNavigateToNext)\n    }\n\n    /**\n     * Test: Item in list can be clicked\n     */\n    @Test\n    fun `on item click should send ItemClick action`() {\n        val itemId = \"item-123\"\n        mutableStateFlow.update {\n            it.copy(items = listOf(ExampleItem(id = itemId, name = \"Test Item\")))\n        }\n\n        composeTestRule\n            .onNodeWithText(\"Test Item\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.ItemClick(itemId)) }\n    }\n\n    // ==================== DIALOG TESTS ====================\n\n    /**\n     * Test: No dialog exists when dialogState is null\n     */\n    @Test\n    fun `no dialog should exist when dialogState is null`() {\n        mutableStateFlow.update { it.copy(dialogState = null) }\n\n        composeTestRule.assertNoDialogExists()\n    }\n\n    /**\n     * Test: Loading dialog displays when state updates\n     * PATTERN: Use isDialog() to check dialog exists\n     */\n    @Test\n    fun `loading dialog should display when dialogState is Loading`() {\n        mutableStateFlow.update {\n            it.copy(dialogState = ExampleState.DialogState.Loading(\"Please wait...\"))\n        }\n\n        composeTestRule\n            .onNode(isDialog())\n            .assertIsDisplayed()\n\n        // Verify loading text within dialog using hasAnyAncestor(isDialog())\n        composeTestRule\n            .onAllNodesWithText(\"Please wait...\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Error dialog displays title and message\n     * PATTERN: Use filterToOne(hasAnyAncestor(isDialog())) to find text within dialogs\n     */\n    @Test\n    fun `error dialog should display title and message`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Error(\n                    title = \"An error has occurred\",\n                    message = \"Something went wrong. Please try again.\",\n                ),\n            )\n        }\n\n        // Verify dialog exists\n        composeTestRule\n            .onNode(isDialog())\n            .assertIsDisplayed()\n\n        // Verify title within dialog\n        composeTestRule\n            .onAllNodesWithText(\"An error has occurred\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n\n        // Verify message within dialog\n        composeTestRule\n            .onAllNodesWithText(\"Something went wrong. Please try again.\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Dialog button click sends action\n     * PATTERN: Find button with hasAnyAncestor(isDialog()) then performClick()\n     */\n    @Test\n    fun `error dialog dismiss button should send DismissDialog action`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Error(\n                    title = \"Error\",\n                    message = \"An error occurred\",\n                ),\n            )\n        }\n\n        // Click dismiss button within dialog\n        composeTestRule\n            .onAllNodesWithText(\"Ok\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.DismissDialog) }\n    }\n\n    /**\n     * Test: Confirmation dialog with multiple buttons\n     * PATTERN: Test both confirm and cancel actions\n     */\n    @Test\n    fun `confirmation dialog confirm button should send ConfirmAction`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Confirmation(\n                    title = \"Confirm Action\",\n                    message = \"Are you sure you want to proceed?\",\n                ),\n            )\n        }\n\n        // Click confirm button\n        composeTestRule\n            .onAllNodesWithText(\"Confirm\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.ConfirmAction) }\n    }\n\n    @Test\n    fun `confirmation dialog cancel button should send DismissDialog action`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Confirmation(\n                    title = \"Confirm Action\",\n                    message = \"Are you sure?\",\n                ),\n            )\n        }\n\n        // Click cancel button\n        composeTestRule\n            .onAllNodesWithText(\"Cancel\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.DismissDialog) }\n    }\n}\n\nprivate val DEFAULT_STATE = ExampleState(\n    isLoading = false,\n    data = null,\n    errorMessage = null,\n    items = emptyList(),\n    dialogState = null,\n)\n\n// Example types (normally in separate files)\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    val errorMessage: String? = null,\n    val items: List<ExampleItem> = emptyList(),\n    val dialogState: DialogState? = null,\n) {\n    /**\n     * PATTERN: Nested sealed class for dialog states.\n     * Common dialog types: Loading, Error, Confirmation\n     */\n    sealed class DialogState {\n        data class Loading(val message: String) : DialogState()\n        data class Error(val title: String, val message: String) : DialogState()\n        data class Confirmation(val title: String, val message: String) : DialogState()\n    }\n}\n\ndata class ExampleItem(val id: String, val name: String)\n\nsealed class ExampleAction {\n    data object BackClick : ExampleAction()\n    data object SubmitClick : ExampleAction()\n    data class ItemClick(val itemId: String) : ExampleAction()\n    data object DismissDialog : ExampleAction()\n    data object ConfirmAction : ExampleAction()\n}\n\nsealed class ExampleEvent {\n    data object NavigateBack : ExampleEvent()\n    data object NavigateToNext : ExampleEvent()\n}\n","isInternal":false,"tokens":2128,"sizeBytes":10198},{"name":"repository-test-example.md","path":".claude/skills/testing-android-code/examples/repository-test-example.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/testing-android-code/examples/repository-test-example.md","title":"Examples Skill","category":"anthropic-skill","format":"markdown","content":"/**\n * Complete Repository Test Example\n *\n * Key patterns demonstrated:\n * - Fake for disk sources, Mock for network services\n * - Using FakeDispatcherManager for deterministic coroutines\n * - Using fixed Clock for deterministic time\n * - Testing Result types with .asSuccess() / .asFailure()\n * - Asserting actual objects (not isSuccess/isFailure) for better diagnostics\n * - Testing Flow emissions with Turbine\n */\npackage com.bitwarden.example.data.repository\n\nimport app.cash.turbine.test\nimport com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager\nimport com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow\nimport com.bitwarden.core.data.util.asFailure\nimport com.bitwarden.core.data.util.asSuccess\nimport io.mockk.coEvery\nimport io.mockk.coVerify\nimport io.mockk.mockk\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.onSubscription\nimport kotlinx.coroutines.test.runTest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNull\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport java.time.Clock\nimport java.time.Instant\nimport java.time.ZoneOffset\n\nclass ExampleRepositoryTest {\n\n    // Fixed clock for deterministic time-based tests\n    private val fixedClock: Clock = Clock.fixed(\n        Instant.parse(\"2023-10-27T12:00:00Z\"),\n        ZoneOffset.UTC,\n    )\n\n    // Use FakeDispatcherManager for deterministic coroutine execution\n    private val dispatcherManager = FakeDispatcherManager()\n\n    // Mock service (network layer is always mocked)\n    private val mockService: ExampleService = mockk()\n\n    /**\n     * PATTERN: Use Fake for disk source in happy path tests.\n     * This is the Bitwarden convention for repository testing.\n     */\n    private val fakeDiskSource = FakeExampleDiskSource()\n\n    private lateinit var repository: ExampleRepositoryImpl\n\n    @BeforeEach\n    fun setup() {\n        repository = ExampleRepositoryImpl(\n            clock = fixedClock,\n            service = mockService,\n            diskSource = fakeDiskSource,\n            dispatcherManager = dispatcherManager,\n        )\n    }\n\n    // ==================== HAPPY PATH TESTS (use Fake) ====================\n\n    /**\n     * Test: Successful fetch returns data and saves to disk\n     */\n    @Test\n    fun `fetchData should return success and save to disk when service succeeds`() = runTest {\n        val expectedData = ExampleData(id = \"1\", name = \"Test\", updatedAt = fixedClock.instant())\n        coEvery { mockService.getData() } returns expectedData.asSuccess()\n\n        val result = repository.fetchData()\n\n        assertEquals(expectedData, result.getOrThrow())\n        // Fake automatically stores the data - verify it's there\n        assertEquals(expectedData, fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Service failure returns failure without saving\n     */\n    @Test\n    fun `fetchData should return failure when service fails`() = runTest {\n        val exception = Exception(\"Network error\")\n        coEvery { mockService.getData() } returns exception.asFailure()\n\n        val result = repository.fetchData()\n\n        assertEquals(exception, result.exceptionOrNull())\n        // Fake was not updated\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Repository flow emits when disk source updates\n     */\n    @Test\n    fun `dataFlow should emit when disk source updates`() = runTest {\n        val data1 = ExampleData(id = \"1\", name = \"First\", updatedAt = fixedClock.instant())\n        val data2 = ExampleData(id = \"2\", name = \"Second\", updatedAt = fixedClock.instant())\n\n        repository.dataFlow.test {\n            // Initial null value from Fake\n            assertNull(awaitItem())\n\n            // Update via Fake property setter (triggers emission)\n            fakeDiskSource.storedData = data1\n            assertEquals(data1, awaitItem())\n\n            // Another update\n            fakeDiskSource.storedData = data2\n            assertEquals(data2, awaitItem())\n        }\n    }\n\n    /**\n     * Test: Refresh fetches and saves new data\n     */\n    @Test\n    fun `refresh should fetch new data and update disk source`() = runTest {\n        val newData = ExampleData(id = \"new\", name = \"Fresh\", updatedAt = fixedClock.instant())\n        coEvery { mockService.getData() } returns newData.asSuccess()\n\n        val result = repository.refresh()\n\n        assertEquals(Unit, result.getOrThrow())\n        coVerify { mockService.getData() }\n        assertEquals(newData, fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Delete clears data from disk\n     */\n    @Test\n    fun `deleteData should clear disk source`() = runTest {\n        // Pre-populate the fake\n        fakeDiskSource.storedData = ExampleData(id = \"1\", name = \"Test\", updatedAt = fixedClock.instant())\n\n        repository.deleteData()\n\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Cached data returns from disk when available\n     */\n    @Test\n    fun `getCachedData should return disk data without network call`() = runTest {\n        val cachedData = ExampleData(\n            id = \"cached\",\n            name = \"Cached\",\n            updatedAt = fixedClock.instant(),\n        )\n        fakeDiskSource.storedData = cachedData\n\n        val result = repository.getCachedData()\n\n        assertEquals(cachedData, result)\n        coVerify(exactly = 0) { mockService.getData() }\n    }\n\n    // ==================== ERROR PATH TESTS ====================\n\n    /**\n     * PATTERN: For error paths, reconfigure the class-level mock per-test.\n     * Use coEvery to change mock behavior for each specific test case.\n     */\n    @Test\n    fun `fetchData should return failure when service returns error`() = runTest {\n        val exception = Exception(\"Server unavailable\")\n        coEvery { mockService.getData() } returns exception.asFailure()\n\n        val result = repository.fetchData()\n\n        assertEquals(exception, result.exceptionOrNull())\n        // Fake state unchanged on failure\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    @Test\n    fun `refresh should return failure and preserve cached data when service fails`() = runTest {\n        // Pre-populate cache via Fake\n        val cachedData = ExampleData(id = \"cached\", name = \"Old\", updatedAt = fixedClock.instant())\n        fakeDiskSource.storedData = cachedData\n\n        // Reconfigure mock to return failure\n        coEvery { mockService.getData() } returns Exception(\"Network error\").asFailure()\n\n        val result = repository.refresh()\n\n        assertTrue(result.isFailure)\n        // Cached data preserved on failure\n        assertEquals(cachedData, fakeDiskSource.storedData)\n    }\n}\n\n// Example types (normally in separate files)\ndata class ExampleData(\n    val id: String,\n    val name: String,\n    val updatedAt: Instant,\n)\n\ninterface ExampleService {\n    suspend fun getData(): Result<ExampleData>\n}\n\ninterface ExampleDiskSource {\n    val dataFlow: kotlinx.coroutines.flow.Flow<ExampleData?>\n    fun getData(): ExampleData?\n    fun saveData(data: ExampleData)\n    fun clearData()\n}\n\n/**\n * PATTERN: Fake implementation for happy path testing.\n *\n * Key characteristics:\n * - Uses bufferedMutableSharedFlow(replay = 1) for proper replay behavior\n * - Uses .onSubscription { emit(state) } for immediate state emission\n * - Private storage with override property setter that emits to flow\n * - Test assertions done via the override property getter\n */\nclass FakeExampleDiskSource : ExampleDiskSource {\n    private var storedDataValue: ExampleData? = null\n    private val mutableDataFlow = bufferedMutableSharedFlow<ExampleData?>(replay = 1)\n\n    /**\n     * Override property with getter/setter. Setter emits to flow automatically.\n     * Tests can read this property for assertions and write to trigger emissions.\n     */\n    var storedData: ExampleData?\n        get() = storedDataValue\n        set(value) {\n            storedDataValue = value\n            mutableDataFlow.tryEmit(value)\n        }\n\n    override val dataFlow: Flow<ExampleData?>\n        get() = mutableDataFlow.onSubscription { emit(storedData) }\n\n    override fun getData(): ExampleData? = storedData\n\n    override fun saveData(data: ExampleData) {\n        storedData = data\n    }\n\n    override fun clearData() {\n        storedData = null\n    }\n}\n","isInternal":false,"tokens":1760,"sizeBytes":8387},{"name":"viewmodel-test-example.md","path":".claude/skills/testing-android-code/examples/viewmodel-test-example.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/testing-android-code/examples/viewmodel-test-example.md","title":"Examples Skill","category":"anthropic-skill","format":"markdown","content":"/**\n * Complete ViewModel Test Example\n *\n * Key patterns demonstrated:\n * - Extending BaseViewModelTest\n * - Testing StateFlow with Turbine\n * - Testing EventFlow with Turbine\n * - Using stateEventFlow() for simultaneous testing\n * - MockK mocking patterns\n * - Test factory method design (accepts domain state, not SavedStateHandle)\n * - Complete state assertions (assert entire state objects)\n */\npackage com.bitwarden.example.feature\n\nimport androidx.lifecycle.SavedStateHandle\nimport app.cash.turbine.test\nimport com.bitwarden.ui.platform.base.BaseViewModelTest\nimport io.mockk.coEvery\nimport io.mockk.coVerify\nimport io.mockk.every\nimport io.mockk.mockk\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.test.runTest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Test\n\nclass ExampleViewModelTest : BaseViewModelTest() {\n\n    // Mock dependencies\n    private val mockRepository: ExampleRepository = mockk()\n    private val mockAuthDiskSource: AuthDiskSource = mockk {\n        every { userStateFlow } returns MutableStateFlow(null)\n    }\n\n    /**\n     * StateFlow has replay=1, so first awaitItem() returns current state\n     */\n    @Test\n    fun `initial state should be default state`() = runTest {\n        val viewModel = createViewModel()\n\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n        }\n    }\n\n    /**\n     * Test state transitions: initial -> loading -> success\n     */\n    @Test\n    fun `LoadData action should update state from idle to loading to success`() = runTest {\n        val expectedData = \"loaded data\"\n        coEvery { mockRepository.fetchData(any()) } returns Result.success(expectedData)\n\n        val viewModel = createViewModel()\n\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.LoadData)\n\n            assertEquals(DEFAULT_STATE.copy(isLoading = true), awaitItem())\n            assertEquals(DEFAULT_STATE.copy(isLoading = false, data = expectedData), awaitItem())\n        }\n\n        coVerify { mockRepository.fetchData(any()) }\n    }\n\n    /**\n     * EventFlow has no replay - MUST call expectNoEvents() first\n     */\n    @Test\n    fun `SubmitClick action should emit NavigateToNext event`() = runTest {\n        coEvery { mockRepository.submitData(any()) } returns Result.success(Unit)\n\n        val viewModel = createViewModel()\n\n        viewModel.eventFlow.test {\n            expectNoEvents() // CRITICAL for EventFlow\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n            assertEquals(ExampleEvent.NavigateToNext, awaitItem())\n        }\n    }\n\n    /**\n     * Use stateEventFlow() helper for simultaneous testing\n     */\n    @Test\n    fun `complex action should update state and emit event`() = runTest {\n        coEvery { mockRepository.complexOperation(any()) } returns Result.success(\"result\")\n\n        val viewModel = createViewModel()\n\n        viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n            assertEquals(DEFAULT_STATE, stateFlow.awaitItem())\n            eventFlow.expectNoEvents()\n\n            viewModel.trySendAction(ExampleAction.ComplexAction)\n\n            assertEquals(DEFAULT_STATE.copy(isLoading = true), stateFlow.awaitItem())\n            assertEquals(DEFAULT_STATE.copy(data = \"result\"), stateFlow.awaitItem())\n            assertEquals(ExampleEvent.ShowToast(\"Success!\"), eventFlow.awaitItem())\n        }\n    }\n\n    /**\n     * Test state restoration from saved state.\n     * Note: Use initialState parameter, NOT SavedStateHandle directly.\n     */\n    @Test\n    fun `initial state from saved state should be preserved`() = runTest {\n        // Build complete expected state - always assert full objects\n        val savedState = ExampleState(\n            isLoading = false,\n            data = \"restored data\",\n            errorMessage = null,\n        )\n\n        val viewModel = createViewModel(initialState = savedState)\n\n        viewModel.stateFlow.test {\n            assertEquals(savedState, awaitItem())\n        }\n    }\n\n    /**\n     * Factory method accepts domain state, NOT SavedStateHandle.\n     * This hides Android framework details from test logic.\n     */\n    private fun createViewModel(\n        initialState: ExampleState? = null,\n    ): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle().apply { set(\"state\", initialState) },\n        repository = mockRepository,\n        authDiskSource = mockAuthDiskSource,\n    )\n}\n\nprivate val DEFAULT_STATE = ExampleState(\n    isLoading = false,\n    data = null,\n    errorMessage = null,\n)\n\n// Example types (normally in separate files)\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    val errorMessage: String? = null,\n)\n\nsealed class ExampleAction {\n    data object LoadData : ExampleAction()\n    data object SubmitClick : ExampleAction()\n    data object ComplexAction : ExampleAction()\n}\n\nsealed class ExampleEvent {\n    data object NavigateToNext : ExampleEvent()\n    data class ShowToast(val message: String) : ExampleEvent()\n}\n","isInternal":false,"tokens":1043,"sizeBytes":5153},{"name":"critical-gotchas.md","path":".claude/skills/testing-android-code/references/critical-gotchas.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/testing-android-code/references/critical-gotchas.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Critical Gotchas and Anti-Patterns\n\nCommon mistakes and pitfalls when writing tests in the Bitwarden Android codebase.\n\n## ❌ NEVER wrap assertCoroutineThrows in runTest\n\n### The Problem\n\n`runTest` catches exceptions and rethrows them, which breaks the `assertCoroutineThrows` assertion pattern.\n\n### Wrong\n\n```kotlin\n@Test\nfun `test exception`() = runTest {\n    assertCoroutineThrows<Exception> {\n        repository.throwingFunction()\n    }  // Won't work - exception is caught by runTest!\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `test exception`() {\n    assertCoroutineThrows<Exception> {\n        repository.throwingFunction()\n    }  // Works correctly\n}\n```\n\n### Why This Happens\n\n`runTest` provides a coroutine scope and catches exceptions to provide better error messages. However, `assertCoroutineThrows` needs to catch the exception itself to verify it was thrown. When wrapped in `runTest`, the exception is caught twice, breaking the assertion.\n\n## ❌ ALWAYS unmock static functions\n\n### The Problem\n\nMockK's static mocking persists across tests. Forgetting to clean up causes mysterious failures in subsequent tests.\n\n### Wrong\n\n```kotlin\n@Before\nfun setup() {\n    mockkStatic(::isBuildVersionAtLeast)\n    every { isBuildVersionAtLeast(any()) } returns true\n}\n\n// Forgot @After - subsequent tests will fail mysteriously!\n```\n\n### Correct\n\n```kotlin\n@Before\nfun setup() {\n    mockkStatic(::isBuildVersionAtLeast)\n    every { isBuildVersionAtLeast(any()) } returns true\n}\n\n@After\nfun tearDown() {\n    unmockkStatic(::isBuildVersionAtLeast)  // CRITICAL\n}\n```\n\n### Common Static Functions to Watch\n\n```kotlin\n// Platform version checks\nmockkStatic(::isBuildVersionAtLeast)\nunmockkStatic(::isBuildVersionAtLeast)\n\n// URI parsing\nmockkStatic(Uri::class)\nunmockkStatic(Uri::class)\n\n// Static utility functions\nmockkStatic(MyUtilClass::class)\nunmockkStatic(MyUtilClass::class)\n```\n\n### Debugging Tip\n\nIf tests pass individually but fail when run together, suspect static mocking cleanup issues.\n\n## ❌ Don't confuse StateFlow and EventFlow testing\n\n### StateFlow (replay = 1)\n\n```kotlin\n// CORRECT - StateFlow always has current value\nviewModel.stateFlow.test {\n    val initial = awaitItem()  // Gets current state immediately\n    viewModel.trySendAction(action)\n    val updated = awaitItem()  // Gets new state\n}\n```\n\n### EventFlow (no replay)\n\n```kotlin\n// CORRECT - EventFlow has no initial value\nviewModel.eventFlow.test {\n    expectNoEvents()  // MUST do this first\n    viewModel.trySendAction(action)\n    val event = awaitItem()  // Gets emitted event\n}\n```\n\n### Common Mistake\n\n```kotlin\n// WRONG - Forgetting expectNoEvents() on EventFlow\nviewModel.eventFlow.test {\n    viewModel.trySendAction(action)  // May cause flaky tests\n    assertEquals(event, awaitItem())\n}\n```\n\n## ❌ Don't mix real and test dispatchers\n\n### Wrong\n\n```kotlin\nprivate val repository = ExampleRepositoryImpl(\n    dispatcherManager = DispatcherManagerImpl(),  // Real dispatcher!\n)\n\n@Test\nfun `test repository`() = runTest {\n    // Test will have timing issues - real dispatcher != test dispatcher\n}\n```\n\n### Correct\n\n```kotlin\nprivate val repository = ExampleRepositoryImpl(\n    dispatcherManager = FakeDispatcherManager(),  // Test dispatcher\n)\n\n@Test\nfun `test repository`() = runTest {\n    // Test runs deterministically\n}\n```\n\n### Why This Matters\n\nReal dispatchers use actual thread pools and delays. Test dispatchers (UnconfinedTestDispatcher) execute immediately and deterministically. Mixing them causes:\n- Non-deterministic test failures\n- Real delays in tests (slow test suite)\n- Race conditions\n\n### Always Use\n\n- `FakeDispatcherManager()` for repositories\n- `UnconfinedTestDispatcher()` when manually creating dispatchers\n- `runTest` for coroutine tests (provides TestDispatcher automatically)\n\n## ❌ Don't forget to use runTest for coroutine tests\n\n### Wrong\n\n```kotlin\n@Test\nfun `test coroutine`() {\n    viewModel.stateFlow.test { /* ... */ }  // Missing runTest!\n}\n```\n\nThis causes:\n- Test completes before coroutines finish\n- False positives (test passes but assertions never run)\n- Mysterious failures\n\n### Correct\n\n```kotlin\n@Test\nfun `test coroutine`() = runTest {\n    viewModel.stateFlow.test { /* ... */ }\n}\n```\n\n### When runTest is Required\n\n- Testing ViewModels (they use `viewModelScope`)\n- Testing Flows with Turbine `.test {}`\n- Testing repositories with suspend functions\n- Any test calling suspend functions\n\n### Exception: assertCoroutineThrows\n\nAs noted above, `assertCoroutineThrows` should NOT be wrapped in `runTest`.\n\n## ❌ Don't forget relaxed = true for complex mocks\n\n### Without relaxed\n\n```kotlin\nprivate val viewModel = mockk<ExampleViewModel>()  // Must mock every method!\n\n// Error: \"no answer found for: stateFlow\"\n```\n\n### With relaxed\n\n```kotlin\nprivate val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n    // Only mock what you care about\n    every { stateFlow } returns mutableStateFlow\n    every { eventFlow } returns mutableEventFlow\n}\n```\n\n### When to Use relaxed\n\n- Mocking ViewModels in Compose tests\n- Mocking complex objects with many methods\n- When you only care about specific method calls\n\n### When NOT to Use relaxed\n\n- Mocking repository interfaces (be explicit about behavior)\n- When you want to verify NO unexpected calls\n- Testing error paths (want test to fail if unexpected method called)\n\n## ❌ Don't assert individual fields when complete state is available\n\n### The Problem\n\nAsserting individual state fields can miss unintended side effects on other fields.\n\n### Wrong\n\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    viewModel.trySendAction(SomeAction.DoThing)\n\n    val state = viewModel.stateFlow.value\n    assertEquals(null, state.dialog)  // Only checks one field!\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    viewModel.trySendAction(SomeAction.DoThing)\n\n    val expected = SomeState(\n        isLoading = false,\n        data = \"result\",\n        dialog = null,\n    )\n    assertEquals(expected, viewModel.stateFlow.value)  // Checks all fields\n}\n```\n\n### Why This Matters\n\n- Catches unintended mutations to other state fields\n- Makes expected state explicit and readable\n- Prevents silent regressions when state structure changes\n\n---\n\n## ❌ Don't use Kotlin assert() for boolean checks\n\n### The Problem\n\nKotlin's `assert()` doesn't follow JUnit conventions and provides poor failure messages.\n\n### Wrong\n\n```kotlin\n@Test\nfun `event should trigger callback`() {\n    mutableEventFlow.tryEmit(SomeEvent.Navigate)\n\n    assert(onNavigateCalled)  // Kotlin assert - bad failure messages\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `event should trigger callback`() {\n    mutableEventFlow.tryEmit(SomeEvent.Navigate)\n\n    assertTrue(onNavigateCalled)  // JUnit assertTrue - proper assertion\n}\n```\n\n### Always Use JUnit Assertions\n\n- `assertTrue()` / `assertFalse()` for booleans\n- `assertEquals()` for value comparisons\n- `assertNotNull()` / `assertNull()` for nullability\n- `assertThrows<T>()` for exceptions\n\n---\n\n## ❌ Don't pass SavedStateHandle to test factory methods\n\n### The Problem\n\nExposing `SavedStateHandle` in test factory methods leaks Android framework details into test logic.\n\n### Wrong\n\n```kotlin\nprivate fun createViewModel(\n    savedStateHandle: SavedStateHandle = SavedStateHandle(),  // Framework type exposed\n): MyViewModel = MyViewModel(\n    savedStateHandle = savedStateHandle,\n    repository = mockRepository,\n)\n\n@Test\nfun `initial state from saved state`() = runTest {\n    val savedState = MyState(isLoading = true)\n    val savedStateHandle = SavedStateHandle(mapOf(\"state\" to savedState))\n\n    val viewModel = createViewModel(savedStateHandle = savedStateHandle)\n    // ...\n}\n```\n\n### Correct\n\n```kotlin\nprivate fun createViewModel(\n    initialState: MyState? = null,  // Domain type only\n): MyViewModel = MyViewModel(\n    savedStateHandle = SavedStateHandle().apply { set(\"state\", initialState) },\n    repository = mockRepository,\n)\n\n@Test\nfun `initial state from saved state`() = runTest {\n    val savedState = MyState(isLoading = true)\n\n    val viewModel = createViewModel(initialState = savedState)\n    // ...\n}\n```\n\n### Why This Matters\n\n- Cleaner, more intuitive test code\n- Hides SavedStateHandle implementation details\n- Follows Bitwarden conventions\n\n---\n\n## ❌ Don't test SavedStateHandle persistence in unit tests\n\n### The Problem\n\nTesting whether state persists to SavedStateHandle is testing Android framework behavior, not your business logic.\n\n### Wrong\n\n```kotlin\n@Test\nfun `state should persist to SavedStateHandle`() = runTest {\n    val savedStateHandle = SavedStateHandle()\n    val viewModel = createViewModel(savedStateHandle = savedStateHandle)\n\n    viewModel.trySendAction(SomeAction)\n\n    val savedState = savedStateHandle.get<MyState>(\"state\")\n    assertEquals(expectedState, savedState)  // Testing framework, not logic!\n}\n```\n\n### Correct\n\nFocus on testing business logic and state transformations:\n\n```kotlin\n@Test\nfun `action should update state correctly`() = runTest {\n    val viewModel = createViewModel()\n\n    viewModel.trySendAction(SomeAction)\n\n    assertEquals(expectedState, viewModel.stateFlow.value)  // Test observable state\n}\n```\n\n---\n\n## ❌ Don't use static mocking when DI pattern is available\n\n### The Problem\n\nStatic mocking (`mockkStatic`) is harder to maintain and less testable than dependency injection.\n\n### Wrong\n\n```kotlin\nclass ParserTest {\n    @BeforeEach\n    fun setup() {\n        mockkStatic(UUID::class)\n        every { UUID.randomUUID() } returns mockk {\n            every { toString() } returns \"fixed-uuid\"\n        }\n    }\n\n    @AfterEach\n    fun tearDown() {\n        unmockkStatic(UUID::class)\n    }\n}\n```\n\n### Correct\n\nExtract an interface and inject it:\n\n```kotlin\n// Production code\ninterface UuidManager {\n    fun generateUuid(): String\n}\n\nclass UuidManagerImpl : UuidManager {\n    override fun generateUuid(): String = UUID.randomUUID().toString()\n}\n\nclass Parser(private val uuidManager: UuidManager) { ... }\n\n// Test code\nclass ParserTest {\n    private val mockUuidManager = mockk<UuidManager>()\n\n    @BeforeEach\n    fun setup() {\n        every { mockUuidManager.generateUuid() } returns \"fixed-uuid\"\n    }\n\n    // No tearDown needed - no static mocking!\n}\n```\n\n### When to Use This Pattern\n\n- UUID generation\n- Timestamp/Clock operations\n- System property access\n- Any static function that needs deterministic testing\n\n---\n\n## ❌ Don't forget to test null stream returns from Android APIs\n\n### The Problem\n\nAndroid's `ContentResolver.openOutputStream()` and `openInputStream()` can return null, not just throw exceptions.\n\n### Wrong\n\n```kotlin\nclass FileManagerTest {\n    @Test\n    fun `stringToUri with exception should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } throws IOException()\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n\n        assertFalse(result)\n    }\n    // Missing: test for null return!\n}\n```\n\n### Correct\n\n```kotlin\nclass FileManagerTest {\n    @Test\n    fun `stringToUri with exception should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } throws IOException()\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n        assertFalse(result)\n    }\n\n    @Test\n    fun `stringToUri with null stream should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } returns null\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n        assertFalse(result)  // CRITICAL: must handle null!\n    }\n}\n```\n\n### Common Android APIs That Return Null\n\n- `ContentResolver.openOutputStream()` / `openInputStream()`\n- `Context.getExternalFilesDir()`\n- `PackageManager.getApplicationInfo()` (can throw)\n\n---\n\n## Bitwarden Mocking Guidelines\n\n**Mock at architectural boundaries:**\n- Repository → ViewModel (mock repository)\n- Service → Repository (mock service)\n- API → Service (use MockWebServer, not mocks)\n- DiskSource → Repository (mock disk source)\n\n**Fake vs Mock Strategy (IMPORTANT):**\n- **Happy paths**: Use Fake implementations (`FakeAuthenticatorDiskSource`, `FakeVaultDiskSource`)\n- **Error paths**: Use MockK with isolated repository instances\n\n```kotlin\n// Happy path - use Fake\nprivate val fakeDiskSource = FakeAuthenticatorDiskSource()\n\n@Test\nfun `createItem should return Success`() = runTest {\n    val result = repository.createItem(mockItem)\n    assertEquals(CreateItemResult.Success, result)\n}\n\n// Error path - use isolated Mock\n@Test\nfun `createItem with exception should return Error`() = runTest {\n    val mockDiskSource = mockk<AuthenticatorDiskSource> {\n        coEvery { saveItem(any()) } throws RuntimeException()\n    }\n    val repository = RepositoryImpl(diskSource = mockDiskSource)\n\n    val result = repository.createItem(mockItem)\n    assertEquals(CreateItemResult.Error, result)\n}\n```\n\n**Use Fakes for:**\n- `FakeDispatcherManager` - deterministic coroutines\n- `FakeConfigDiskSource` - in-memory config storage\n- `FakeSharedPreferences` - memory-backed preferences\n- `FakeAuthenticatorDiskSource` - in-memory authenticator storage\n\n**Create real instances for:**\n- Data classes, value objects (User, Config, CipherView)\n- Test data builders (`createMockCipher(number = 1)`)\n\n## ❌ Don't forget bufferedMutableSharedFlow with onSubscription for Fakes\n\n### The Problem\n\nFake data sources using `MutableSharedFlow` won't emit cached state to new subscribers without explicit handling.\n\n### Wrong\n\n```kotlin\nclass FakeDataSource : DataSource {\n    private val mutableFlow = MutableSharedFlow<List<Item>>()\n    private val storedItems = mutableListOf<Item>()\n\n    override fun getItems(): Flow<List<Item>> = mutableFlow\n\n    override suspend fun saveItem(item: Item) {\n        storedItems.add(item)\n        mutableFlow.emit(storedItems)\n    }\n}\n\n// Test: Initial collection gets nothing!\nrepository.dataFlow.test {\n    // Hangs or fails - no initial emission\n}\n```\n\n### Correct\n\n```kotlin\nclass FakeDataSource : DataSource {\n    private val mutableFlow = bufferedMutableSharedFlow<List<Item>>()\n    private val storedItems = mutableListOf<Item>()\n\n    override fun getItems(): Flow<List<Item>> = mutableFlow\n        .onSubscription { emit(storedItems.toList()) }\n\n    override suspend fun saveItem(item: Item) {\n        storedItems.add(item)\n        mutableFlow.emit(storedItems.toList())\n    }\n}\n\n// Test: Initial collection receives current state\nrepository.dataFlow.test {\n    assertEquals(emptyList(), awaitItem())  // Works!\n}\n```\n\n### Key Points\n\n- Use `bufferedMutableSharedFlow()` from `core/data/repository/util/`\n- Add `.onSubscription { emit(currentState) }` for immediate state emission\n- This ensures new collectors receive the current cached state\n\n---\n\n## ✅ Use Result extension functions for assertions\n\n### The Pattern\n\nUse `asSuccess()` and `asFailure()` extensions from `com.bitwarden.core.data.util` for cleaner Result assertions.\n\n### Success Path\n\n```kotlin\n@Test\nfun `getData should return success`() = runTest {\n    val result = repository.getData()\n    val expected = expectedData.asSuccess()\n\n    assertEquals(expected.getOrNull(), result.getOrNull())\n}\n```\n\n### Failure Path\n\n```kotlin\n@Test\nfun `getData with error should return failure`() = runTest {\n    val exception = IOException(\"Network error\")\n    coEvery { mockService.getData() } returns exception.asFailure()\n\n    val result = repository.getData()\n\n    assertTrue(result.isFailure)\n    assertEquals(exception, result.exceptionOrNull())\n}\n```\n\n### Avoid Redundant Assertions\n\n```kotlin\n// WRONG - redundant success checks\nassertTrue(result.isSuccess)\nassertTrue(expected.isSuccess)\nassertArrayEquals(expected.getOrNull(), result.getOrNull())\n\n// CORRECT - final assertion is sufficient\nassertArrayEquals(expected.getOrNull(), result.getOrNull())\n```\n\n---\n\n## Summary Checklist\n\nBefore submitting tests, verify:\n\n**Core Patterns:**\n- [ ] No `assertCoroutineThrows` inside `runTest`\n- [ ] All static mocks have `unmockk` in `@After`\n- [ ] EventFlow tests start with `expectNoEvents()`\n- [ ] Using FakeDispatcherManager, not real dispatchers\n- [ ] All coroutine tests use `runTest`\n\n**Assertion Patterns:**\n- [ ] Assert complete state objects, not individual fields\n- [ ] Use JUnit `assertTrue()`, not Kotlin `assert()`\n- [ ] Use `asSuccess()` for Result type assertions\n- [ ] Avoid redundant assertion patterns\n\n**Test Design:**\n- [ ] Test factory methods accept domain types, not SavedStateHandle\n- [ ] Use Fakes for happy paths, Mocks for error paths\n- [ ] Prefer DI patterns over static mocking\n- [ ] Test null returns from Android APIs (streams, files)\n- [ ] Fakes use `bufferedMutableSharedFlow()` with `.onSubscription`\n\n**General:**\n- [ ] Tests don't depend on execution order\n- [ ] Complex mocks use `relaxed = true`\n- [ ] Test data is created fresh for each test\n- [ ] Mocking behavior, not value objects\n- [ ] Testing observable behavior, not implementation\n\nWhen tests fail mysteriously, check these gotchas first.\n","isInternal":false,"tokens":3825,"sizeBytes":17037},{"name":"flow-testing-patterns.md","path":".claude/skills/testing-android-code/references/flow-testing-patterns.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/testing-android-code/references/flow-testing-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Flow Testing with Turbine\n\nBitwarden Android uses Turbine for testing Kotlin Flows, including the critical distinction between StateFlow and EventFlow patterns.\n\n## StateFlow vs EventFlow\n\n### StateFlow (Replayed)\n\n**Characteristics:**\n- `replay = 1` - Always emits current value to new collectors\n- First `awaitItem()` returns the current/initial state\n- Survives configuration changes\n- Used for UI state that needs to be immediately available\n\n**Test Pattern:**\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateFlow.test {\n        // First awaitItem() gets CURRENT state\n        assertEquals(INITIAL_STATE, awaitItem())\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.LoadData)\n\n        // Next awaitItem() gets UPDATED state\n        assertEquals(LOADING_STATE, awaitItem())\n        assertEquals(SUCCESS_STATE, awaitItem())\n    }\n}\n```\n\n### EventFlow (No Replay)\n\n**Characteristics:**\n- `replay = 0` - Only emits new events after subscription\n- No initial value emission\n- One-time events (navigation, toasts, dialogs)\n- Does not survive configuration changes\n\n**Test Pattern:**\n```kotlin\n@Test\nfun `action should emit event`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.eventFlow.test {\n        // MUST call expectNoEvents() first - nothing emitted yet\n        expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.Submit)\n\n        // Now expect the event\n        assertEquals(MyEvent.NavigateToNext, awaitItem())\n    }\n}\n```\n\n**Critical:** Always call `expectNoEvents()` before triggering actions on EventFlow. Forgetting this causes flaky tests.\n\n## Testing State and Events Simultaneously\n\nUse the `stateEventFlow()` helper from `BaseViewModelTest`:\n\n```kotlin\n@Test\nfun `complex action should update state and emit event`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        // Initial state\n        assertEquals(INITIAL_STATE, stateFlow.awaitItem())\n\n        // No events yet\n        eventFlow.expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.ComplexAction)\n\n        // Verify state progression\n        assertEquals(LOADING_STATE, stateFlow.awaitItem())\n        assertEquals(SUCCESS_STATE, stateFlow.awaitItem())\n\n        // Verify event emission\n        assertEquals(MyEvent.ShowToast, eventFlow.awaitItem())\n    }\n}\n```\n\n## Repository Flow Testing\n\n### Testing Database Flows\n\n```kotlin\n@Test\nfun `dataFlow should emit when database updates`() = runTest {\n    val dataFlow = MutableStateFlow(initialData)\n    every { mockDiskSource.dataFlow } returns dataFlow\n\n    repository.dataFlow.test {\n        // Initial value\n        assertEquals(initialData, awaitItem())\n\n        // Update disk source\n        dataFlow.value = updatedData\n\n        // Verify emission\n        assertEquals(updatedData, awaitItem())\n    }\n}\n```\n\n### Testing Transformed Flows\n\n```kotlin\n@Test\nfun `flow transformation should map correctly`() = runTest {\n    val sourceFlow = MutableStateFlow(UserEntity(id = \"1\", name = \"John\"))\n    every { mockDao.observeUser() } returns sourceFlow\n\n    // Repository transforms entity to domain model\n    repository.userFlow.test {\n        val expectedUser = User(id = \"1\", name = \"John\")\n        assertEquals(expectedUser, awaitItem())\n    }\n}\n```\n\n## Common Patterns\n\n### Pattern 1: Testing Initial State + Action\n\n```kotlin\n@Test\nfun `load data should update from idle to loading to success`() = runTest {\n    coEvery { repository.getData() } returns \"data\".asSuccess()\n\n    viewModel.stateFlow.test {\n        assertEquals(DEFAULT_STATE, awaitItem())\n\n        viewModel.loadData()\n\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Success), awaitItem())\n    }\n}\n```\n\n### Pattern 2: Testing Error States\n\n```kotlin\n@Test\nfun `load data with error should emit failure state`() = runTest {\n    val error = Exception(\"Network error\")\n    coEvery { repository.getData() } returns error.asFailure()\n\n    viewModel.stateFlow.test {\n        assertEquals(DEFAULT_STATE, awaitItem())\n\n        viewModel.loadData()\n\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())\n        assertEquals(\n            DEFAULT_STATE.copy(loadingState = LoadingState.Error(\"Network error\")),\n            awaitItem(),\n        )\n    }\n}\n```\n\n### Pattern 3: Testing Event Sequences\n\n```kotlin\n@Test\nfun `submit should emit validation then navigation events`() = runTest {\n    viewModel.eventFlow.test {\n        expectNoEvents()\n\n        viewModel.trySendAction(MyAction.Submit)\n\n        assertEquals(MyEvent.ShowValidation, awaitItem())\n        assertEquals(MyEvent.NavigateToNext, awaitItem())\n    }\n}\n```\n\n### Pattern 4: Testing Cancellation\n\n```kotlin\n@Test\nfun `cancelling collection should stop emissions`() = runTest {\n    val flow = flow {\n        repeat(100) {\n            emit(it)\n            delay(100)\n        }\n    }\n\n    flow.test {\n        assertEquals(0, awaitItem())\n        assertEquals(1, awaitItem())\n\n        // Cancel after 2 items\n        cancel()\n\n        // No more items received\n    }\n}\n```\n\n## Anti-Patterns\n\n### ❌ Forgetting expectNoEvents() on EventFlow\n\n```kotlin\n// WRONG\nviewModel.eventFlow.test {\n    viewModel.trySendAction(action)  // May fail - no initial expectNoEvents\n    assertEquals(event, awaitItem())\n}\n\n// CORRECT\nviewModel.eventFlow.test {\n    expectNoEvents()  // ALWAYS do this first\n    viewModel.trySendAction(action)\n    assertEquals(event, awaitItem())\n}\n```\n\n### ❌ Not Using runTest\n\n```kotlin\n// WRONG - Missing runTest\n@Test\nfun `test flow`() {\n    flow.test { /* ... */ }\n}\n\n// CORRECT\n@Test\nfun `test flow`() = runTest {\n    flow.test { /* ... */ }\n}\n```\n\n### ❌ Mixing StateFlow and EventFlow Patterns\n\n```kotlin\n// WRONG - Treating StateFlow like EventFlow\nstateFlow.test {\n    expectNoEvents()  // Unnecessary - StateFlow always has value\n    /* ... */\n}\n\n// WRONG - Treating EventFlow like StateFlow\neventFlow.test {\n    val item = awaitItem()  // Will hang - no initial value!\n    /* ... */\n}\n```\n\n## Reference Implementations\n\n**ViewModel with StateFlow and EventFlow:**\n`app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n**Repository Flow Testing:**\n`data/src/test/kotlin/com/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt`\n\n**Complex Flow Transformations:**\n`data/src/test/kotlin/com/bitwarden/data/vault/repository/VaultRepositoryTest.kt`\n","isInternal":false,"tokens":1504,"sizeBytes":6764},{"name":"test-base-classes.md","path":".claude/skills/testing-android-code/references/test-base-classes.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/testing-android-code/references/test-base-classes.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Test Base Classes Reference\n\nBitwarden Android provides specialized base classes that configure test environments and provide helper utilities.\n\n## BaseViewModelTest\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`\n\n### Purpose\nProvides essential setup for testing ViewModels with proper coroutine dispatcher configuration and Flow testing helpers.\n\n### Automatic Configuration\n- Registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`\n- Ensures deterministic coroutine execution in tests\n- All coroutines complete immediately without real delays\n\n### Key Feature: stateEventFlow() Helper\n\n**Use Case:** When you need to test both StateFlow and EventFlow simultaneously.\n\n```kotlin\n@Test\nfun `complex action should update state and emit event`() = runTest {\n    val viewModel = ExampleViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        // Verify initial state\n        assertEquals(INITIAL_STATE, stateFlow.awaitItem())\n\n        // No events yet\n        eventFlow.expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(ExampleAction.ComplexAction)\n\n        // Verify state updated\n        assertEquals(LOADING_STATE, stateFlow.awaitItem())\n\n        // Verify event emitted\n        assertEquals(ExampleEvent.ShowToast, eventFlow.awaitItem())\n    }\n}\n```\n\n### Usage Pattern\n\n```kotlin\nclass MyViewModelTest : BaseViewModelTest() {\n    private val mockRepository: MyRepository = mockk()\n    private val savedStateHandle = SavedStateHandle(\n        mapOf(KEY_STATE to INITIAL_STATE)\n    )\n\n    @Test\n    fun `test action`() = runTest {\n        val viewModel = MyViewModel(\n            savedStateHandle = savedStateHandle,\n            repository = mockRepository\n        )\n\n        // Test with automatic dispatcher setup\n        viewModel.stateFlow.test {\n            assertEquals(INITIAL_STATE, awaitItem())\n        }\n    }\n}\n```\n\n## BitwardenComposeTest\n\n**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`\n\n### Purpose\nPre-configured test class for Compose UI tests with all Bitwarden managers and theme setup.\n\n### Automatic Configuration\n- All Bitwarden managers pre-configured (FeatureFlags, AuthTab, Biometrics, etc.)\n- Wraps content in `BitwardenTheme` and `LocalManagerProvider`\n- Provides fixed `Clock` for deterministic time-based tests\n- Extends `BaseComposeTest` for Robolectric and dispatcher setup\n\n### Key Features\n\n**Pre-configured Managers:**\n- `FeatureFlagManager` - Controls feature flag behavior\n- `AuthTabManager` - Manages auth tab state\n- `BiometricsManager` - Handles biometric authentication\n- `ClipboardManager` - Clipboard operations\n- `NotificationManager` - Notification display\n\n**Fixed Clock:**\nAll tests use a fixed clock for deterministic time-based testing:\n```kotlin\n// Tests use consistent time: 2023-10-27T12:00:00Z\nval fixedClock: Clock\n```\n\n### Usage Pattern\n\n```kotlin\nclass MyScreenTest : BitwardenComposeTest() {\n    private var haveCalledNavigateBack = false\n    private val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n    private val viewModel = mockk<MyViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        setContent {\n            MyScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                viewModel = viewModel\n            )\n        }\n    }\n\n    @Test\n    fun `on back click should send action`() {\n        composeTestRule.onNodeWithContentDescription(\"Back\").performClick()\n        verify { viewModel.trySendAction(MyAction.BackClick) }\n    }\n\n    @Test\n    fun `loading state should show progress`() {\n        mutableStateFlow.value = DEFAULT_STATE.copy(isLoading = true)\n        composeTestRule.onNode(isProgressBar).assertIsDisplayed()\n    }\n}\n```\n\n### Important: bufferedMutableSharedFlow for Events\n\nIn Compose tests, use `bufferedMutableSharedFlow` instead of regular `MutableSharedFlow` (default replay is 0):\n\n```kotlin\n// Correct for Compose tests\nprivate val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()\n\n// This allows triggering events and having the UI react\nmutableEventFlow.tryEmit(MyEvent.NavigateBack)\n```\n\n## BaseServiceTest\n\n**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt`\n\n### Purpose\nProvides MockWebServer setup for testing API service implementations.\n\n### Automatic Configuration\n- `server: MockWebServer` - Auto-started before each test, stopped after\n- `retrofit: Retrofit` - Pre-configured with:\n  - JSON converter (kotlinx.serialization)\n  - NetworkResultCallAdapter for Result<T> responses\n  - Base URL pointing to MockWebServer\n- `json: Json` - kotlinx.serialization JSON instance\n\n### Usage Pattern\n\n```kotlin\nclass MyServiceTest : BaseServiceTest() {\n    private val api: MyApi = retrofit.create()\n    private val service = MyServiceImpl(api)\n\n    @Test\n    fun `getConfig should return success when API succeeds`() = runTest {\n        // Enqueue mock response\n        server.enqueue(MockResponse().setBody(EXPECTED_JSON))\n\n        // Call service\n        val result = service.getConfig()\n\n        // Verify result\n        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)\n    }\n\n    @Test\n    fun `getConfig should return failure when API fails`() = runTest {\n        // Enqueue error response\n        server.enqueue(MockResponse().setResponseCode(500))\n\n        // Call service\n        val result = service.getConfig()\n\n        // Verify failure\n        assertTrue(result.isFailure)\n    }\n}\n```\n\n### MockWebServer Patterns\n\n**Enqueue successful response:**\n```kotlin\nserver.enqueue(MockResponse().setBody(\"\"\"{\"key\": \"value\"}\"\"\"))\n```\n\n**Enqueue error response:**\n```kotlin\nserver.enqueue(MockResponse().setResponseCode(404))\nserver.enqueue(MockResponse().setResponseCode(500))\n```\n\n**Enqueue delayed response:**\n```kotlin\nserver.enqueue(\n    MockResponse()\n        .setBody(\"\"\"{\"key\": \"value\"}\"\"\")\n        .setBodyDelay(1000, TimeUnit.MILLISECONDS)\n)\n```\n\n**Verify request details:**\n```kotlin\nval request = server.takeRequest()\nassertEquals(\"/api/config\", request.path)\nassertEquals(\"GET\", request.method)\nassertEquals(\"Bearer token\", request.getHeader(\"Authorization\"))\n```\n\n## BaseComposeTest\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseComposeTest.kt`\n\n### Purpose\nBase class for Compose tests that extends `BaseRobolectricTest` and provides `setTestContent()` helper.\n\n### Features\n- Robolectric configuration for Compose\n- Proper dispatcher setup\n- `composeTestRule` for UI testing\n- `setTestContent()` helper wraps content in theme\n\n### Usage\nTypically you'll extend `BitwardenComposeTest` which extends this class. Use `BaseComposeTest` directly only for tests that don't need Bitwarden-specific manager configuration.\n\n## When to Use Each Base Class\n\n| Test Type | Base Class | Use When |\n|-----------|------------|----------|\n| ViewModel tests | `BaseViewModelTest` | Testing ViewModel state and events |\n| Compose screen tests | `BitwardenComposeTest` | Testing Compose UI with Bitwarden components |\n| API service tests | `BaseServiceTest` | Testing network layer with MockWebServer |\n| Repository tests | None (manual setup) | Testing repository logic with mocked dependencies |\n| Utility/helper tests | None (manual setup) | Testing pure functions or utilities |\n\n## Complete Examples\n\n**ViewModel Test:**\n`../examples/viewmodel-test-example.md`\n\n**Compose Screen Test:**\n`../examples/compose-screen-test-example.md`\n\n**Repository Test:**\n`../examples/repository-test-example.md`\n","isInternal":false,"tokens":1709,"sizeBytes":7805},{"name":"SKILL.md","path":".claude/skills/testing-android-code/SKILL.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/skills/testing-android-code/SKILL.md","title":"testing-android-code","category":"anthropic-skill","format":"markdown","content":"---\nname: testing-android-code\ndescription: This skill should be used when writing or reviewing tests for Android code in Bitwarden. Triggered by \"BaseViewModelTest\", \"BitwardenComposeTest\", \"BaseServiceTest\", \"stateEventFlow\", \"bufferedMutableSharedFlow\", \"FakeDispatcherManager\", \"expectNoEvents\", \"assertCoroutineThrows\", \"createMockCipher\", \"createMockSend\", \"asSuccess\", \"Why is my Bitwarden test failing?\", or testing questions about ViewModels, repositories, Compose screens, or data sources in Bitwarden.\nversion: 1.0.0\n---\n\n# Testing Android Code - Bitwarden Testing Patterns\n\n**This skill provides tactical testing guidance for Bitwarden-specific patterns.** For comprehensive architecture and testing philosophy, consult `docs/ARCHITECTURE.md`.\n\n## Test Framework Configuration\n\n**Required Dependencies:**\n- **JUnit 5** (jupiter), **MockK**, **Turbine** (app.cash.turbine)\n- **kotlinx.coroutines.test**, **Robolectric**, **Compose Test**\n\n**Critical Note:** Tests run with en-US locale for consistency. Don't assume other locales.\n\n---\n\n## A. ViewModel Testing Patterns\n\n### Base Class: BaseViewModelTest\n\n**Always extend `BaseViewModelTest` for ViewModel tests.**\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`\n\n**Benefits:**\n- Automatically registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`\n- Provides `stateEventFlow()` helper for simultaneous StateFlow/EventFlow testing\n\n**Pattern:**\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n    private val mockRepository: ExampleRepository = mockk()\n    private val savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to INITIAL_STATE))\n\n    @Test\n    fun `ButtonClick should fetch data and update state`() = runTest {\n        coEvery { mockRepository.fetchData(any()) } returns Result.success(\"data\")\n\n        val viewModel = ExampleViewModel(savedStateHandle, mockRepository)\n\n        viewModel.stateFlow.test {\n            assertEquals(INITIAL_STATE, awaitItem())\n            viewModel.trySendAction(ExampleAction.ButtonClick)\n            assertEquals(INITIAL_STATE.copy(data = \"data\"), awaitItem())\n        }\n\n        coVerify { mockRepository.fetchData(any()) }\n    }\n}\n```\n\n**For complete examples:** See `references/test-base-classes.md`\n\n### StateFlow vs EventFlow (Critical Distinction)\n\n| Flow Type | Replay | First Action | Pattern |\n|-----------|--------|--------------|---------|\n| StateFlow | Yes (1) | `awaitItem()` gets current state | Expect initial → trigger → expect new |\n| EventFlow | No | `expectNoEvents()` first | expectNoEvents → trigger → expect event |\n\n**For detailed patterns:** See `references/flow-testing-patterns.md`\n\n---\n\n## B. Compose UI Testing Patterns\n\n### Base Class: BitwardenComposeTest\n\n**Always extend `BitwardenComposeTest` for Compose screen tests.**\n\n**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`\n\n**Benefits:**\n- Pre-configures all Bitwarden managers (FeatureFlags, AuthTab, Biometrics, etc.)\n- Wraps content in `BitwardenTheme` and `LocalManagerProvider`\n- Provides fixed Clock for deterministic time-based tests\n\n**Pattern:**\n```kotlin\nclass ExampleScreenTest : BitwardenComposeTest() {\n    private var haveCalledNavigateBack = false\n    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        setContent {\n            ExampleScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                viewModel = viewModel,\n            )\n        }\n    }\n\n    @Test\n    fun `on back click should send BackClick action`() {\n        composeTestRule.onNodeWithContentDescription(\"Back\").performClick()\n        verify { viewModel.trySendAction(ExampleAction.BackClick) }\n    }\n}\n```\n\n**Note:** Use `bufferedMutableSharedFlow` for event testing in Compose tests. Default replay is 0; pass `replay = 1` if needed.\n\n**For complete base class details:** See `references/test-base-classes.md`\n\n---\n\n## C. Repository and Service Testing\n\n### Service Testing with MockWebServer\n\n**Base Class:** `BaseServiceTest` (`network/src/testFixtures/`)\n\n```kotlin\nclass ExampleServiceTest : BaseServiceTest() {\n    private val api: ExampleApi = retrofit.create()\n    private val service = ExampleServiceImpl(api)\n\n    @Test\n    fun `getConfig should return success when API succeeds`() = runTest {\n        server.enqueue(MockResponse().setBody(EXPECTED_JSON))\n        val result = service.getConfig()\n        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)\n    }\n}\n```\n\n### Repository Testing Pattern\n\n```kotlin\nclass ExampleRepositoryTest {\n    private val fixedClock: Clock = Clock.fixed(\n        Instant.parse(\"2023-10-27T12:00:00Z\"),\n        ZoneOffset.UTC,\n    )\n    private val dispatcherManager = FakeDispatcherManager()\n    private val mockDiskSource: ExampleDiskSource = mockk()\n    private val mockService: ExampleService = mockk()\n\n    private val repository = ExampleRepositoryImpl(\n        clock = fixedClock,\n        exampleDiskSource = mockDiskSource,\n        exampleService = mockService,\n        dispatcherManager = dispatcherManager,\n    )\n\n    @Test\n    fun `fetchData should return success when service succeeds`() = runTest {\n        coEvery { mockService.getData(any()) } returns expectedData.asSuccess()\n        val result = repository.fetchData(userId)\n        assertTrue(result.isSuccess)\n    }\n}\n```\n\n**Key patterns:** Use `FakeDispatcherManager`, fixed Clock, and `.asSuccess()` helpers.\n\n---\n\n## D. Test Data Builders\n\n### Builder Pattern with Number Parameter\n\n**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/model/`\n\n```kotlin\nfun createMockCipher(\n    number: Int,\n    id: String = \"mockId-$number\",\n    name: String? = \"mockName-$number\",\n    // ... more parameters with defaults\n): SyncResponseJson.Cipher\n\n// Usage:\nval cipher1 = createMockCipher(number = 1)  // mockId-1, mockName-1\nval cipher2 = createMockCipher(number = 2)  // mockId-2, mockName-2\nval custom = createMockCipher(number = 3, name = \"Custom\")\n```\n\n**Available Builders (35+):**\n- **Cipher:** `createMockCipher()`, `createMockLogin()`, `createMockCard()`, `createMockIdentity()`, `createMockSecureNote()`, `createMockSshKey()`, `createMockField()`, `createMockUri()`, `createMockFido2Credential()`, `createMockPasswordHistory()`, `createMockCipherPermissions()`\n- **Sync:** `createMockSyncResponse()`, `createMockFolder()`, `createMockCollection()`, `createMockPolicy()`, `createMockDomains()`\n- **Send:** `createMockSend()`, `createMockFile()`, `createMockText()`, `createMockSendJsonRequest()`\n- **Profile:** `createMockProfile()`, `createMockOrganization()`, `createMockProvider()`, `createMockPermissions()`\n- **Attachments:** `createMockAttachment()`, `createMockAttachmentJsonRequest()`, `createMockAttachmentResponse()`\n\nSee `network/src/testFixtures/kotlin/com/bitwarden/network/model/` for full list.\n\n---\n\n## E. Result Type Testing\n\n**Locations:**\n- `.asSuccess()`, `.asFailure()`: `core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.kt`\n- `assertCoroutineThrows`: `core/src/testFixtures/kotlin/com/bitwarden/core/data/util/TestHelpers.kt`\n\n```kotlin\n// Create results\n\"data\".asSuccess()              // Result.success(\"data\")\nthrowable.asFailure()           // Result.failure<T>(throwable)\n\n// Assertions\nassertTrue(result.isSuccess)\nassertEquals(expectedValue, result.getOrNull())\n```\n\n---\n\n## F. Test Utilities and Helpers\n\n### Fake Implementations\n\n| Fake | Location | Purpose |\n|------|----------|---------|\n| `FakeDispatcherManager` | `core/src/testFixtures/` | Deterministic coroutine execution |\n| `FakeConfigDiskSource` | `data/src/testFixtures/` | In-memory config storage |\n| `FakeSharedPreferences` | `data/src/testFixtures/` | Memory-backed SharedPreferences |\n\n### Exception Testing (CRITICAL)\n\n```kotlin\n// CORRECT - Call directly, NOT inside runTest\n@Test\nfun `test exception`() {\n    assertCoroutineThrows<IllegalStateException> {\n        repository.throwingFunction()\n    }\n}\n```\n\n**Why:** `runTest` catches exceptions and rethrows them, breaking the assertion pattern.\n\n---\n\n## G. Critical Gotchas\n\nCommon testing mistakes in Bitwarden. **For complete details and examples:** See `references/critical-gotchas.md`\n\n**Core Patterns:**\n- **assertCoroutineThrows + runTest** - Never wrap in `runTest`; call directly\n- **Static mock cleanup** - Always `unmockkStatic()` in `@After`\n- **StateFlow vs EventFlow** - StateFlow: `awaitItem()` first; EventFlow: `expectNoEvents()` first\n- **FakeDispatcherManager** - Always use instead of real `DispatcherManagerImpl`\n- **Coroutine test wrapper** - Use `runTest { }` for all Flow/coroutine tests\n\n**Assertion Patterns:**\n- **Complete state assertions** - Assert entire state objects, not individual fields\n- **JUnit over Kotlin** - Use `assertTrue()`, not Kotlin's `assert()`\n- **Use Result extensions** - Use `asSuccess()` and `asFailure()` for Result type assertions\n\n**Test Design:**\n- **Fake vs Mock strategy** - Use Fakes for happy paths, Mocks for error paths\n- **DI over static mocking** - Extract interfaces (like UuidManager) instead of mockkStatic\n- **Null stream testing** - Test null returns from ContentResolver operations\n- **bufferedMutableSharedFlow** - Use with `.onSubscription { emit(state) }` in Fakes\n- **Test factory methods** - Accept domain state types, not SavedStateHandle\n\n---\n\n## H. Test File Organization\n\n### Directory Structure\n\n```\nmodule/src/test/kotlin/com/bitwarden/.../\n├── ui/*ScreenTest.kt, *ViewModelTest.kt\n├── data/repository/*RepositoryTest.kt\n└── network/service/*ServiceTest.kt\n\nmodule/src/testFixtures/kotlin/com/bitwarden/.../\n├── util/TestHelpers.kt\n├── base/Base*Test.kt\n└── model/*Util.kt\n```\n\n### Test Constants Placement\n\nDeclare test constants as top-level `private const val` at the **bottom** of the file, after the class closing brace. Do NOT use `companion object` for test constants.\n\n### Test Naming\n\n- Classes: `*Test.kt`, `*ScreenTest.kt`, `*ViewModelTest.kt`\n- Functions: `` `given state when action should result` ``\n\n---\n\n## Summary\n\nKey Bitwarden-specific testing patterns:\n\n1. **BaseViewModelTest** - Automatic dispatcher setup with `stateEventFlow()` helper\n2. **BitwardenComposeTest** - Pre-configured with all managers and theme\n3. **BaseServiceTest** - MockWebServer setup for network testing\n4. **Turbine Flow Testing** - StateFlow (replay) vs EventFlow (no replay)\n5. **Test Data Builders** - Consistent `number: Int` parameter pattern\n6. **Fake Implementations** - FakeDispatcherManager, FakeConfigDiskSource\n7. **Result Type Testing** - `.asSuccess()`, `.asFailure()`\n\n**Always consult:** `docs/ARCHITECTURE.md` and existing test files for reference implementations.\n\n---\n\n## Reference Documentation\n\nFor detailed information, see:\n\n- `references/test-base-classes.md` - Detailed base class documentation and usage patterns\n- `references/flow-testing-patterns.md` - Complete Turbine patterns for StateFlow/EventFlow\n- `references/critical-gotchas.md` - Full anti-pattern reference and debugging tips\n\n**Complete Examples:**\n- `examples/viewmodel-test-example.md` - Full ViewModel test with StateFlow/EventFlow\n- `examples/compose-screen-test-example.md` - Full Compose screen test\n- `examples/repository-test-example.md` - Full repository test with mocks and fakes\n","frontmatter":{"name":"testing-android-code","description":"This skill should be used when writing or reviewing tests for Android code in Bitwarden. Triggered by \"BaseViewModelTest\", \"BitwardenComposeTest\", \"BaseServiceTest\", \"stateEventFlow\", \"bufferedMutableSharedFlow\", \"FakeDispatcherManager\", \"expectNoEvents\", \"assertCoroutineThrows\", \"createMockCipher\", \"createMockSend\", \"asSuccess\", \"Why is my Bitwarden test failing?\", or testing questions about ViewModels, repositories, Compose screens, or data sources in Bitwarden.","version":"1.0.0"},"isInternal":false,"tokens":2736,"sizeBytes":11629},{"name":"plan-android-work.md","path":".claude/commands/plan-android-work.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/commands/plan-android-work.md","title":"Command: /plan-android-work","category":"command-prompt","format":"markdown","content":"---\ndescription: Guided requirements refinement and implementation planning for Bitwarden Android\nargument-hint: <Jira ticket (PM-12345), Confluence URL, or free-text description>\n---\n\n# Android Planning Workflow\n\nYou are guiding the developer through requirements refinement and implementation planning for the Bitwarden Android project. The input to plan from is:\n\n**Input**: $ARGUMENTS\n\n## Prerequisites\n\n- **Jira/Confluence access**: Fetching tickets and wiki pages requires the `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin. If the plugin is not installed, Jira ticket IDs and Confluence URLs cannot be fetched automatically.\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** The user may skip phases that are not applicable. If starting from a partially completed plan, skip to the appropriate phase.\n\n### Phase 1: Ingest Requirements\n\nParse the input to detect and fetch all available sources:\n\n**Source Detection Rules:**\n- **Jira tickets** (patterns like `PM-\\d+`, `BWA-\\d+`, `EC-\\d+`): Fetch via `get_issue` and `get_issue_comments`. Also fetch linked issue summaries (parent epic, sub-tasks, blockers) for context.\n- **Confluence URLs** (containing `atlassian.net/wiki` or confluence page IDs): Extract page ID and fetch via `get_confluence_page`. If the page is a parent page, fetch child pages via `get_child_pages` and ask the user which are relevant.\n- **Free text**: Treat as direct requirements — no fetching needed.\n- **Multiple inputs**: All are first-class sources. Fetch each independently and consolidate.\n- **Tool unavailable**: If `get_issue`, `get_confluence_page`, or other Atlassian tools are not available, inform the user that the `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin is required and prompt them to install and configure it. In the meantime, ask the user to paste the relevant content directly. Treat pasted content as free-text input.\n\n**Present a structured summary:**\n1. Sources identified and fetched (with links)\n2. Raw requirements extracted from each source\n3. Initial scope assessment (small / medium / large)\n\n**Edge cases:**\n- Jira ticket with no description → flag as critical gap that Phase 2 must address\n- Multiple tickets → fetch all, consolidate, flag any contradictions\n- Ticket + free text → both treated as first-class; free text supplements ticket\n\n**Gate**: User confirms the summary is complete and may add additional sources or context before proceeding.\n\n### Phase 2: Refine Requirements\n\nInvoke the `refining-android-requirements` skill and use it to perform gap analysis on the raw requirements from Phase 1.\n\nThe skill will:\n1. Consolidate all sources into a working document\n2. Evaluate requirements against a structured rubric (functional, technical, security, UX, cross-cutting)\n3. Present categorized gaps as blocking or non-blocking questions\n4. After user answers, produce a structured specification with numbered IDs\n\n**Gate**: User approves the refined specification. They may request changes or provide additional answers.\n\n### Phase 3: Plan Implementation\n\nInvoke the `planning-android-implementation` skill and use it to design the implementation approach based on the refined spec from Phase 2.\n\nThe skill will:\n1. Classify the change type\n2. Explore the codebase for reference implementations and integration points\n3. Design the architecture with component relationships\n4. Produce a file inventory and phased implementation plan\n5. Assess risks and define verification criteria\n\n**Gate**: User reviews the implementation plan and may request changes to architecture, phasing, or scope.\n\n### Phase 4: Finalize & Save\n\nMerge the outputs from Phase 2 (specification) and Phase 3 (implementation plan) into a single design document using this template:\n\n```markdown\n# [Feature Name] — Design Document\n\n**Feature**: [concise description]\n**Date**: [current date]\n**Status**: Ready for Implementation\n**Jira**: [ticket ID if available]\n**Sources**: [list of all input sources with links]\n\n---\n\n## Requirements Specification\n\n[Full output from Phase 2 — the refined specification with numbered IDs]\n\n---\n\n## Implementation Plan\n\n[Full output from Phase 3 — architecture, file inventory, phases, risks]\n\n---\n\n## Executing This Plan\n\nTo implement this plan, run:\n\n    /work-on-android [ticket or feature reference]\n\nReference this design document during implementation for architecture decisions,\nfile locations, and phase ordering.\n```\n\n**Save the document:**\n- With ticket: `.claude/outputs/plans/PM-XXXXX-FEATURE-NAME-PLAN.md`\n- Without ticket: `.claude/outputs/plans/FEATURE-NAME-PLAN.md`\n- Feature name should be uppercase with hyphens (e.g., `BIOMETRIC-TIMEOUT-CONFIG-PLAN.md`)\n- Create the output directory if it does not exist\n\n**On completion**: Present the saved file path and remind the user they can execute the plan with `/work-on-android`.\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- When fetching from Jira/Confluence, summarize what was found rather than dumping raw content.\n- Questions in Phase 2 should be specific and actionable, not generic.\n- The implementation plan in Phase 3 should reference concrete files in the codebase, not abstract descriptions.\n","frontmatter":{"description":"Guided requirements refinement and implementation planning for Bitwarden Android","argument-hint":"<Jira ticket (PM-12345), Confluence URL, or free-text description>"},"isInternal":false,"tokens":1159,"sizeBytes":5381},{"name":"review-android.md","path":".claude/commands/review-android.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/commands/review-android.md","title":"Command: /review-android","category":"command-prompt","format":"markdown","content":"---\ndescription: Guided Android code review workflow through context gathering, Android-specific review, and output\nargument-hint: [PR# | PR URL | \"local\"]\n---\n\n# Android Code Review Workflow\n\nYou are guiding the developer through a comprehensive Android code review for the Bitwarden Android project.\n\n**Input**: $ARGUMENTS\n\n## Prerequisites\n\n- **Jira/Confluence access**: The `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin is required to fetch linked Jira tickets. If unavailable, skip ticket context.\n- **GitHub CLI**: Required for fetching PR metadata. Verify with `gh auth status`.\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** The user may skip phases that are not applicable.\n\n### Phase 1: Ingest\n\nParse the input to determine review context:\n\n**Source Detection Rules:**\n- **PR number** (`123`, `PR #123`, `https://github.com/.../pull/123`): Extract the numeric ID. Fetch PR metadata via `gh pr view <N> --json title,body,headRefName,baseRefName,author,files`. Fetch existing review threads to avoid duplicate comments via `gh api graphql` with `reviewThreads(first: 100)`.\n- **\"local\"** or no argument: Review current branch changes via `git diff main...HEAD` and `git log main...HEAD --oneline --no-merges`.\n- **No input**: Ask the user whether to review a PR (provide number/URL) or local branch changes.\n\n**Additional context:**\n- Detect Jira ticket references in PR title/body (patterns like `PM-\\d+`, `BWA-\\d+`). Fetch via `get_issue` if the MCP plugin is available.\n- Summarize what was fetched rather than dumping raw content.\n\n**Present a structured summary:**\n1. What is being reviewed (PR title/number, branch, or local changes description)\n2. Jira ticket context if found (summary and acceptance criteria)\n3. Files changed (count and modules affected)\n4. Existing review thread count (PR reviews only — avoids duplicate comments)\n\n**Gate**: User confirms the summary is complete before proceeding.\n\n### Phase 2: Review\n\nInvoke the `reviewing-changes` skill and use it to perform the Android-specific code review. Use the PR context from Phase 1 (change type, files affected, modules, Jira requirements) to inform the skill's change type detection and checklist selection.\n\nThe skill will:\n1. Detect the change type based on files and PR context from Phase 1\n2. Load the appropriate type-specific checklist\n3. Execute the multi-pass review strategy\n4. Consult reference materials as needed\n\n**Before advancing**: Share a summary of key findings (critical issues if any, overall assessment) and confirm the user is ready to output the review.\n\n### Phase 3: Output\n\nWrite the completed review to local files:\n\n- `review-summary.md` — Overall assessment (APPROVE / REQUEST CHANGES) plus critical issues list\n- `review-inline-comments.md` — All inline findings with `<details>` tags\n\nFollow the exact output format from `.claude/skills/reviewing-changes/examples/review-outputs.md`.\n\nFor PR reviews: offer to post the review to GitHub using `gh pr review <N> --comment -b \"$(cat review-summary.md)\"` for the summary. For inline comments, use the GitHub API or the `bitwarden-code-review` plugin if installed.\n\n**Before advancing**: Confirm the files were written successfully and ask if the user wants to post to GitHub (PR reviews only).\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- Never proceed to another phase without user confirmation.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- If starting from a partially completed review (e.g., review already written), skip to the appropriate phase.\n","frontmatter":{"description":"Guided Android code review workflow through context gathering, Android-specific review, and output","argument-hint":["PR# | PR URL | \"local\""]},"isInternal":false,"tokens":829,"sizeBytes":3681},{"name":"work-on-android.md","path":".claude/commands/work-on-android.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/.claude/commands/work-on-android.md","title":"Command: /work-on-android","category":"command-prompt","format":"markdown","content":"---\ndescription: Guided Android development workflow through all lifecycle phases\nargument-hint: <task description, plan, or Jira ticket reference>\n---\n\n# Android Development Workflow\n\nYou are guiding the developer through a complete Android development lifecycle for the Bitwarden Android project. The task to work on is:\n\n**Task**: $ARGUMENTS\n\n## Workflow Phases\n\nWork through each phase sequentially. **Confirm with the user before advancing to the next phase.** If a phase fails (tests fail, lint errors, etc.), loop on that phase until resolved before advancing. The user may skip phases that are not applicable.\n\n### Phase 1: Implement\n\nInvoke `Skill(implementing-android-code)` to guide your implementation of the task. Understand what needs to be done, explore the relevant code, and write the implementation.\n\n**Before advancing**: Summarize what was implemented and confirm the user is ready to move to testing.\n\n### Phase 2: Test\n\nInvoke `Skill(testing-android-code)` to write tests for the changes made in Phase 1. Follow the project's test patterns and conventions.\n\n**Before advancing**: Summarize what tests were written and confirm readiness for build verification.\n\n### Phase 3: Build & Verify\n\nInvoke `Skill(build-test-verify)` to run tests, lint, and detekt. Ensure everything passes.\n\n**If failures occur**: Fix the issues and re-run verification. Do not advance until all checks pass.\n\n**Before advancing**: Report build/test/lint results and confirm readiness for self-review.\n\n### Phase 4: Self-Review\n\nInvoke `Skill(bitwarden-delivery-tools:perform-preflight)` to perform a quality gate check on all changes. Address any issues found.\n\n**Before advancing**: Share the self-review results and confirm readiness to commit.\n\n### Phase 5: Commit\n\nInvoke `Skill(bitwarden-delivery-tools:committing-changes)` to stage and commit the changes with a properly formatted commit message.\n\n**Before advancing**: Confirm the commit was successful and ask if the user wants to proceed to review and PR creation, or stop here.\n\n### Phase 6: Review\n\n**Pre-requisites:**\n- `bitwarden-code-review` from the [Bitwarden Plugin Marketplace](https://github.com/bitwarden/ai-plugins) must be installed in order to perform this phase. If it is not installed prompt the user to install it, or skip the review phase.\n\nLaunch a subagent with the `/bitwarden-code-review:code-review-local` command to perform a **local** code review of the committed diff. Validate and address any issues found before proceeding.\n\n**Before advancing**: Share review findings and confirm readiness for PR creation.\n\n### Phase 7: Pull Request\n\nPrompt the user to invoke `Skill(bitwarden-delivery-tools:creating-pull-request)` to create the pull request with proper description and formatting. **Create as a draft PR by default** unless the user has explicitly requested a ready-for-review PR.\n\n## Guidelines\n\n- Be explicit about which phase you are in at all times.\n- Never proceed to another phase without user confirmation.\n- If the user wants to skip a phase, acknowledge and move to the next applicable phase.\n- If starting from a partially completed task (e.g., code already written), skip to the appropriate phase.\n","frontmatter":{"description":"Guided Android development workflow through all lifecycle phases","argument-hint":"<task description, plan, or Jira ticket reference>"},"isInternal":false,"tokens":675,"sizeBytes":3198},{"name":"ARCHITECTURE.md","path":"docs/ARCHITECTURE.md","rawUrl":"https://raw.githubusercontent.com/bitwarden/android/HEAD/docs/ARCHITECTURE.md","title":"docs - System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# Architecture\n\n- [Overview](#overview)\n- [Application Modules](#application-modules)\n  - [Core Library Modules](#core-library-modules)\n  - [Specialized Library Modules](#specialized-library-modules)\n  - [Supporting Modules (Source Sets and Test Fixtures)](#supporting-modules-source-sets-and-test-fixtures)\n- [Data Layer](#data-layer)\n  - [Data Sources](#data-sources)\n  - [Managers](#managers)\n  - [Repositories](#repositories)\n  - [Note on Dependency Injection](#note-on-dependency-injection)\n- [UI Layer](#ui-layer)\n  - [ViewModels / MVVM](#viewmodels--mvvm)\n    - [Example](#example)\n  - [Screens / Compose](#screens--compose)\n    - [State Hoisting](#state-hoisting)\n    - [Example](#example-1)\n  - [Navigation](#navigation)\n    - [State-based Navigation](#state-based-navigation)\n    - [Event-based Navigation](#event-based-navigation)\n    - [Navigation Implementation](#navigation-implementation)\n    - [Example](#example-2)\n\n## Overview\n\nThe Bitwarden Android project employs a multi-module architecture to enhance separation of concerns, maintainability, and build efficiency. This architecture distinguishes between application modules, which are user-facing, and library modules, which provide shared functionalities.\n\n### Application modules\nThese modules represent the entry points and primary user interfaces for the applications. They are structured with standard Android source sets to organize code for different build types and testing:\n\n* **`Bitwarden.app`**: This module constitutes the Bitwarden Password Manager application. It integrates various library modules to deliver password management and secure data storage features. It is responsible for the application's top-level setup, primary navigation graph, and dependency injection rooting for password manager functionalities. The `app` module utilizes product flavors to generate different versions of the application:\n    * `src/standard/`: Contains code and resources specific to the 'standard' product flavor. Builds produced from this flavor are intended for distribution via the Google Play Store and include functionality dependent on Google Play Services.\n    * `src/fdroid/`: Contains code and resources specific to the 'fdroid' product flavor. Builds produced from this flavor are intended for distribution via F-Droid. This flavor is stripped of all Google Play Services libraries and, as such, does not contain features like push notifications for real-time syncing.\n    * `src/debug/`: Contains code and resources specific to the 'debug' build type (e.g., for enabling debug-specific features, configurations, or alternative API endpoints), which can be combined with any product flavor.\n    * `src/release/`: Contains code and resources specific to the 'release' build type (e.g., for ProGuard/R8 configurations, release-specific API keys, or analytics configurations), which can be combined with any product flavor.\n* **`Bitwarden.authenticator`**: This module implements the Bitwarden Authenticator application. Its purpose is to store and generate two-factor authentication (2FA) codes, such as Time-based One-Time Passwords (TOTP), directly on the user's device. It provides the user interface and logic for managing authenticator accounts and displaying codes, functioning as a standalone authenticator application.\n    * `src/debug/`: Contains code and resources specific to the 'debug' build type of the Authenticator.\n    * `src/release/`: Contains code and resources specific to the 'release' build type of the Authenticator.\n\n### Core Library Modules\n\nThese libraries provide foundational capabilities utilized by application modules and other feature-specific libraries:\n\n* **`Bitwarden.core`**: A fundamental library containing shared business logic, essential data models (entities, value objects), domain-specific utilities, and common interfaces. It is designed with minimal or no direct Android framework dependencies to ensure high reusability and testability.\n* **`Bitwarden.data`**: Manages the data layer for the applications. Responsibilities include defining repositories, data sources (for network APIs and local persistence like Room), data transformation logic (mappers), and managing data synchronization. It abstracts data origin and storage details from other parts of the system.\n* **`Bitwarden.network`**: Encapsulates all network communication logic. This includes API client setup (e.g., using Retrofit or Ktor), defining request/response models for network operations, and handling network interceptors and authentication.\n* **`Bitwarden.ui`**: A library module providing common UI elements, theming resources (colors, typography, shapes), custom Jetpack Compose components, base UI classes, and UI-related utility functions. It ensures a consistent user experience across different features and applications.\n\n### Specialized Library Modules\n\nThese libraries offer more targeted functionalities:\n\n* **`Bitwarden.authenticatorbridge`**: Serves as an interface or bridge facilitating communication and integration between the `Bitwarden.authenticator` module's functionalities and the`Bitwarden.app` module. This allows for controlled interaction while maintaining modular decoupling.\n* **`Bitwarden.annotation`**: Contains custom Java/Kotlin annotations used throughout the project. These annotations support compile-time code generation (e.g., for dependency injection with Dagger Hilt, or database schema generation with Room) and provide metadata for other development tools.\n\n### Supporting Modules (Source Sets and Test Fixtures)\n\nThe project structure includes conventional Gradle source sets and test fixture modules that support\ndevelopment and testing:\n\n* **`src/main/`**: The primary source set of a module, containing its functional code.\n* **`src/test/`**: Contains local unit tests (JVM-based) for the associated module.\n* **`src/testFixtures/`**: Provides shared testing utilities, mock objects, sample data, or helper\n  functions reusable across various test modules.\n\n## Data Layer\n\nThe data layer is where all the UI-independent data is stored and retrieved. It consists of both raw data sources as well as higher-level \"repository\" and \"manager\" classes.\n\nNote that any functions exposed by a data layer class that must perform asynchronous work do so by exposing **suspending functions** that may run inside [coroutines](https://kotlinlang.org/docs/coroutines-guide.html) while any streaming sources of data are handled by exposing [Flows](https://kotlinlang.org/docs/flow.html).\n\n### Data Sources\n\nThe lowest level of the data layer are the \"data source\" classes. These are the raw sources of data that include data persisted in [Room](https://developer.android.com/jetpack/androidx/releases/room3) / [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences), data retrieved from network requests using [Retrofit](https://github.com/square/retrofit), and data retrieved via interactions with the [Bitwarden SDK](https://github.com/bitwarden/sdk).\n\nNote that these data sources are constructed in a manner that adheres to a very important principle of the app: **that function calls should not throw exceptions** (see the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--kotlin) for more details.) In the case of data sources, this tends to mean that suspending functions like those representing network requests or Bitwarden SDK calls should return a [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/) type. This is an important responsibility of the data layer as a wrapper around other third party libraries, as dependencies like Retrofit and the Bitwarden SDK tend to throw exceptions to indicate errors instead.\n\n### Managers\n\nManager classes represent something of a middle level of the data layer. While some manager classes like [VaultLockManager](../app/src/main/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManager.kt) depend on the the lower-level data sources, others are wrappers around OS-level classes (ex: [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt)) while others have no dependencies at all (ex: [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt)). The commonality amongst the manager classes is that they tend to have a single discrete responsibility. These classes may also exist solely in the data layer for use inside a repository or manager class, like [AppStateManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/AppStateManager.kt), or may be exposed directly to the UI layer, like [SpecialCircumstanceManager](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/SpecialCircumstanceManager.kt).\n\n### Repositories\n\nRepository classes represent the outermost level of the data layer. They can take data sources, managers, and in rare cases even other repositories as dependencies and are meant to be exposed directly to the UI layer. They synthesize data from multiple sources and combine various asynchronous requests as necessary in order to expose data to the UI layer in a more appropriate form. These classes tend to have broad responsibilities that generally cover a major domain of the app, such as authentication ([AuthRepository](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt)) or vault access ([VaultRepository](../app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt)).\n\nRepository classes also feature functions that do not throw exceptions, but unlike the lower levels of the data layer the `Result` type should be avoided in favor of custom sealed classes that represent the various success/error cases in a more processed form. Returning raw `Throwable`/`Exception` instances as part of \"error\" states should be avoided when possible.\n\nIn some cases a source of data may be continuously observed and in these cases a repository may choose to expose a [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/) that emits data updates using the [DataState](../app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/DataState.kt) wrapper.\n\n### Note on Dependency Injection\n\nNearly all classes in the data layer consist of interfaces representing exposed behavior and a corresponding `...Impl` class implementing that interface (ex: [AuthDiskSource](../app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt) / [AuthDiskSourceImpl](../app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt)). All `...Impl` classes are intended to be manually constructed while their associated interfaces are provided for dependency injection via a [Hilt Module](https://dagger.dev/hilt/modules.html) (ex: [PlatformNetworkModule](../app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt)). This prevents the `...Impl` classes from being injected by accident and allows the interfaces to be easily mocked/faked in tests.\n\n## UI Layer\n\nThe UI layer adheres to the concept of [unidirectional data flow](https://developer.android.com/develop/ui/compose/architecture#udf) and makes use of the MVVM design pattern. Both concepts are in line what Google currently recommends as the best approach for building the UI-layer of a modern Android application and this allows us to make use of all the available tooling Google provides as part of the [Jetpack suite of libraries](https://developer.android.com/jetpack). The MVVM implementation is built around the Android [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) class and the UI itself is constructed using the [Jetpack Compose](https://developer.android.com/develop/ui/compose), a declarative UI framework specifically built around the unidirectional data flow approach.\n\nEach screen in the app is associated with at least the following three classes/files:\n\n- A `...ViewModel` class responsible for managing the data and state for the screen.\n- A `...Screen` class that contains the Compose implementation of the UI.\n- A `...Navigation` file containing the details for how to add the screen to the overall navigation graph and how navigate to it within the graph.\n\n### ViewModels / MVVM\n\nThe app's approach to MVVM is based around the handling of \"state\", \"actions\", and \"events\" and is encoded in the [BaseViewModel](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/BaseViewModel.kt) class.\n\n- **State:** The \"state\" represents the complete internal and external **total state** of the ViewModel (VM). Any and all information needed to configure the UI that is associated with the VM should be included in this state and is exposed via the `BaseViewModel.stateFlow` property as `StateFlow<S>` (where `S` represents the unique type of state associated with the VM). This state is typically a combination of state from the data layer (such as account information) and state from the UI layer (such as data entered into a text field).\n\n  There should be no additional `StateFlow` exposed from a VM that would represent some other kind of state; all state should be represented by `S`. Additionally, any internal state not directly needed by the UI but which influences the behavior of the VM should be included as well in order to keep all state managed by the VM in a single place.\n\n- **Actions:** The \"actions\" represent interactions with the VM in some way that could potentially cause an update to that total state. These can be external actions coming from the user's interaction with the UI, like click events, or internal actions coming from some asynchronous process internal to the VM itself, like the result of some suspending functions. Actions are sent by interacting directly with `BaseViewModel.actionChannel` or by using the `BaseViewModel.sendAction` and `BaseViewModel.trySendAction` helpers. All actions are then processed synchronously in a queue in the `handleAction` function.\n\n  It is worth emphasizing that state should never be updated inside a coroutine in a VM; all asynchronous work that results in a state update should do so by posting an internal action. This ensures that the only place that state changes can occur is synchronously inside the `handleAction` function. This makes the process of finding and reasoning about state changes easier and simplifies debugging.\n\n- **Events:** The \"events\" represent discrete, one-shot side-effects and are typically associated with navigation events triggered by some user action. They are sent internally using `BaseViewModel.sendEvent` and may be consumed by the UI layer via the `BaseViewModel.eventFlow` property. An [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) should typically be used to simplify the consumption of these events.\n\nVMs are typically injected using [Dagger Hilt](https://developer.android.com/training/dependency-injection/hilt-android) by annotating them with `@HiltViewModel`. This allows them to be constructed, retrieved, and cached in the Compose UI layer by calling `hiltViewModel()` with the appropriate type. Dependencies passed into the VM constructor will typically be singletons of the graph, such as [AuthRepository](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt). In cases where the VM needs to initialized with some specific data (like an ID) that is sent from the previous screen, this data should be retrieved by injecting the [SavedStateHandle](https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-savedstate) and pulling out data using a type-safe wrapper (ex: [LoginArgs](../app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt)).\n\n#### Example\n\nThe following is an example that demonstrates many of the above principles and best practices when using `BaseViewModel` to implement a VM. It has the following features:\n\n- It is injected with a repository (`ExampleRepository`) that provides streaming `ExampleData` and a `SavedStateHandle` in order to pull initial data using a `ExampleArgs` wrapper class.\n- It has state that manages several properties of interest to the UI: `exampleData`, `isToggledEnabled`, and `dialogState`.\n- It receives external actions from the UI (`ContinueButtonClick` and `ToggleValueUpdate`) as well as internal actions launch from inside different coroutines coroutines (`Internal.ExampleDataReceive`, `Internal.ToggleValueSync`). These actions result in state updates or the emission of an event (`NavigateToNextScreen`).\n- It saves the current state to the `SavedStateHandle` in order to restore it later after process death and restoration. This ensures the user's actions and associated state will not be lost if the app goes into the background and is temporarily killed by the OS to conserve memory.\n\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\nprivate const val KEY_STATE = \"state\"\n\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    private val exampleRepository: ExampleRepository,\n    private val savedStateHandle: SavedStateHandle,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    // If previously saved state data is present in the SavedStateHandle, use that as the initial\n    // state, otherwise initialize from repository and navigation data.\n    initialState = savedStateHandle[KEY_STATE] ?: ExampleState(\n        exampleData = exampleRepository.exampleDataStateFlow.value,\n        isToggleEnabled = ExampleArgs(savedStateHandle).isToggleEnabledInitialValue,\n        dialogState = null,\n    )\n) {\n    init {\n        // As the state updates, write to saved state handle for retrieval after process death and\n        // restoration.\n        stateFlow\n            .onEach { savedStateHandle[KEY_STATE] = it }\n            .launchIn(viewModelScope)\n\n        exampleRepository\n            .exampleDataStateFlow\n            // Asynchronously received data is converted to an internal action and sent in order to\n            // be handled by `handleAction`.\n            .map { ExampleAction.Internal.ExampleDataReceive(it) }\n            .onEach(::sendAction)\n            .launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        when (action) {\n            is ExampleAction.ContinueButtonClick -> handleContinueButtonClick()\n            is ExampleAction.ToggleValueUpdate -> handleToggleValueUpdate(action)\n            is ExampleAction.Internal -> handleInternalAction(action)\n        }\n    }\n\n    private fun handleContinueButtonClick() {\n        // Update the state to show a dialog\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Loading,\n            )\n        }\n\n        // Run a suspending call in a coroutine to fetch data and post the result back as an\n        // internal action for further processing so that all state changes and event emissions\n        // happen synchronously in `handleAction`.\n        viewModelScope.launch {\n            val completionData = exampleRepository\n                .fetchCompletionData(isToggleEnabled = state.isToggleEnabled)\n\n            sendAction(\n                ExampleAction.Internal.CompletionDataReceive(\n                    completionData = completionData,\n                )\n            )\n        }\n    }\n\n    private fun handleToggleValueUpdate(action: ExampleAction.ToggleValueUpdate) {\n        // Update the state\n        mutableStateFlow.update {\n            it.copy(\n                isToggleEnabled = action.isToggleEnabled,\n            )\n        }\n    }\n\n    private fun handleInternalAction(action: ExampleAction.Internal) {\n        when (action) {\n          is ExampleAction.Internal.CompletionDataReceive -> handleCompletionDataReceive(action)\n          is ExampleAction.Internal.ExampleDataReceive -> handleExampleDataReceive(action)\n        }\n    }\n\n    private fun handleCompletionDataReceive(action: ExampleAction.Internal.CompletionDataReceive) {\n        // Update the state to clear the dialog\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = null,\n            )\n        }\n\n        // Send event with data from the action to navigate\n        sendEvent(\n            ExampleEvent.NavigateToNextScreen(\n                completionData = action.completionData,\n            )\n        )\n    }\n\n    private fun handleExampleDataReceive(action: ExampleAction.Internal.ExampleDataReceive) {\n        // Update the state\n        mutableStateFlow.update {\n            it.copy(\n                exampleData = action.exampleData,\n            )\n        }\n    }\n}\n\n@Parcelize\ndata class ExampleState(\n    val exampleData: String,\n    val isToggleEnabled: Boolean,\n    val dialogState: DialogState?,\n) : Parcelable {\n\n    sealed class DialogState : Parcelable {\n        @Parcelize\n        data object Loading : DialogState()\n    }\n}\n\nsealed class ExampleEvent {\n    data class NavigateToNextScreen(\n        val completionData: CompletionData,\n    ) : ExampleEvent()\n}\n\nsealed class ExampleAction {\n    data object ContinueButtonClick : ExampleAction()\n\n    data class ToggleValueUpdate(\n        val isToggleEnabled: Boolean,\n    ) : ExampleAction()\n\n    sealed class Internal : ExampleAction() {\n        data class CompletionDataReceive(\n          val completionData: CompletionData,\n        ) : Internal()\n\n        data class ExampleDataReceive(\n            val exampleData: String,\n        ) : Internal()\n    }\n}\n```\n</details>\n\n\n### Screens / Compose\n\nEach unique screen destination is represented by a composable `...Screen` function annotated with `@Composable`. The responsibilities of this layer include:\n\n- Receiving \"state\" data from the associated `ViewModel` and rendering the UI accordingly.\n- Receiving \"events\" from the associated `ViewModel` and taking the corresponding action, which is typically to trigger a passed-in callback function for navigation purposes. An [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) should be used to simplify this process.\n- Sending \"actions\" to the `ViewModel` indicating user interactions or UI-layer events as necessary.\n- Interacting with purely UI-layer \"manager\" classes that are not appropriate or possible to be injected into the `ViewModel` (such as those concerning permissions or camera access).\n\nIn order to both provide consistency to the app and to simplify the development of new screens, an extensive system of reusable composable components has been developed and may be found in the [components package](../app/src/main/java/com/x8bit/bitwarden/ui/platform/components). These tend to bear the prefix `Bitwarden...` to easily distinguish them from similar OS-level components. If there is any new unique UI component that is added, it should always be considered if it should be made a shareable component.\n\nRefer to the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--jetpack-compose) for additional information on best practices when using Compose.\n\n#### State-hoisting\n\nJetpack Compose is built around the idea that the state required to render any given composable function can be \"hoisted\" to higher levels that may need access to that state. This means that the responsibility for keeping track of the current state of a component may not typically reside with the component itself. It is important, therefore, to understand where the best place is to manage any given piece of UI state.\n\nThis is discussed in detail in [Google's state-hoisting documentation](https://developer.android.com/develop/ui/compose/state-hoisting) and can be summarized roughly as follows: **state should be hoisted as high as is necessary to perform any relevant logic and no higher.** This is a pattern that is followed in the Bitwarden app and tends to lead to the following:\n\n- Any state that will eventually need to be used by the VM should be hoisted to the VM. For example, any text input, toggle values, etc. that may be updated by user interactions should be completely controlled by the VM. These state changes are communicated via the \"actions\" that the Compose layer sends, which trigger corresponding updates the VM's total \"state\".\n- Any UI state that will not be used by logic inside the VM should remain in the UI layer. For example, visibility toggle states for password inputs should typically remain out of the VM.\n\nThese rules can lead to some notable differences in how certain dialogs are handled. For example, loading dialogs are controlled by events that occur in the VM, such as when a network request is started and when it completes. This requires the VM to be in charge of managing the visibility state of the dialog. However, some dialogs appear as the result of clicking on an item in the UI in order to simply display information or to ask for the user's confirmation before some action is taken. Because there is no logic in the VM that depends on the visibility of these particular dialogs, their visibility state can be controlled by the UI. Note, however, that any user interaction that results in a navigation to a new screen should always be routed to the VM first, as the VM should always be in charge of triggering changes at the per-screen level.\n\n#### Example\n\nThe following shows off the basic structure of a composable `...Screen` implementation. Note that:\n\n- The VM is \"injected\" using the `hiltViewModel()` helper function. This will correctly create, cache, and scope the VM in production code while making it easier to pass in mock/fake implementations in test code.\n- An `onNavigateToNextScreen` function is also passed in, which can be called to trigger a navigation via a call to a [NavController](https://developer.android.com/reference/androidx/navigation/NavController) in the outer layers of the navigation graph. Passing in a function rather than the `NavController` itself decouples the screen code from the navigation framework and greatly simplifies the testing of screens.\n- The VM \"state\" is consumed using `viewModel.stateFlow.collectAsStateWithLifecycle()`. This will cause the composable to \"recompose\" and update whenever updates are pushed to the `viewModel.stateFlow`.\n- The VM \"events\" are consumed using an [EventsEffect](../app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/EventsEffect.kt) and demonstrate how the `onNavigateToNextScreen` may be triggered.\n- The current state of the text, switch, and button are hoisted to the VM and pulled out from the `state`. User interactions with the switch and button result in \"actions\" being sent to the VM using `viewModel.trySendAction`.\n- Reusable components (`BitwardenLoadingDialog`, `BitwardenSwitch`, and `BitwardenFilledButton`) are used where possible in order to build the screen using the correct theming and reduce code duplication. When this is not possible (such as when rending the `Text` composable) all colors and styles are pulled from the BitwardenTheme object.\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\n@Composable\nfun ExampleScreen(\n    onNavigateToNextScreen: (CompletionData) -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    // Collect state\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    // Handle events\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            is ExampleEvent.NavigateToNextScreen -> {\n                onNavigateToNextScreen(event.completionData)\n            }\n        }\n    }\n\n    // Show state-based dialogs if necessary\n    when (state.dialogState) {\n        ExampleState.DialogState.Loading -> {\n            BitwardenLoadingDialog(\n                visibilityState = LoadingDialogState.Shown(\n                    text = R.string.loading.asText(),\n                )\n            )\n        }\n\n        else -> Unit\n    }\n\n    // Render the remaining state\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = Modifier\n            .verticalScroll(rememberScrollState()),\n    ) {\n        Text(\n            text = state.exampleData,\n            textAlign = TextAlign.Center,\n            style = BitwardenTheme.typography.headlineSmall,\n            color = BitwardenTheme.colorScheme.textColors.primary,\n            modifier = Modifier\n                .padding(horizontal = 24.dp)\n                .wrapContentHeight(),\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        BitwardenSwitch(\n            label = stringResource(id = R.string.toggle_label),\n            isChecked = state.isToggleEnabled,\n            onCheckedChange = { viewModel.trySendAction(ExampleAction.ToggleValueUpdate(it)) },\n            modifier = Modifier\n                .padding(horizontal = 16.dp)\n                .fillMaxWidth(),\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        BitwardenFilledButton(\n            label = stringResource(id = R.string.continue_text),\n            onClick = { viewModel.trySendAction(ExampleAction.ContinueButtonClick) },\n            modifier = Modifier\n                .padding(horizontal = 16.dp)\n                .fillMaxWidth(),\n        )\n    }\n}\n```\n</details>\n\n### Navigation\n\nNavigation in the app is achieved via the [Compose Navigation](https://developer.android.com/develop/ui/compose/navigation) component. This involves specifying \"destinations\" within a [NavHost](https://developer.android.com/reference/androidx/navigation/NavHost) and using a [NavController](https://developer.android.com/reference/androidx/navigation/NavController) to trigger actual navigation events. The app uses a mix of state-based navigation at the highest level (in order to determine the overall app flow the user is in) and event-based navigation (to handle navigation within a flow based on user interactions).\n\n#### State-based Navigation\n\nState-based navigation is handled by the [RootNavViewModel](../app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt) and [RootNavScreen](../app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt). The current navigation state is determined within the `RootNavViewModel` by monitoring the overall [UserState](../app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt) along with any [SpecialCircumstance](../app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt). The `UserState` encodes whether there are any user accounts, whether they are logged in, etc., while the `SpecialCircumstance` describes a particular circumstance under which the app may have been launched (such as being opened in order to manually select an autofill value). The `RootNavState` calculated by the `RootNavViewModel` is then used by the `RootNavScreen` in order to trigger a navigation only if the overall state has changed.\n\nThe benefit of state-based navigation at this level is that it allows for a variety of state changes that may happen deep within the data layer to automatically trigger navigation to the correct top-level flow without the rest of the UI needing to be concerned with these details. For example, when logging out the current user, the data layer can decide the new state and the `RootNavViewModel` can decide where to go next, rather than having the screen that triggered the logout action choose the next navigation state (which would then need to be redundantly handled in multiple places in the app).\n\nState-based navigation should be limited to specific cases that affect the overall flow of the app and are driven by data emitted from the data layer. All other navigation should use event-based navigation.\n\n#### Event-based Navigation\n\nEvent-based navigation is ultimately the result of some \"action\" sent to a VM, whether that comes directly from a user-interaction with the current screen or from some internal action of the VM sent as the result of some asynchronous call. The VM will send an \"event\" to the Compose layer, which will trigger a callback that will eventually make a call to a [NavController](https://developer.android.com/reference/androidx/navigation/NavController).\n\n#### Navigation Implementation\n\nAt its most basic level, Compose Navigation relies on constructing String-based URIs with a root path to specify a destination and both path and query parameters to handle the passing of required and optional parameters. This process can typically be very error prone. The Bitwarden app borrows a pattern best exemplified by the [Now In Android sample project](https://github.com/android/nowinandroid) in order to make this process more type-safe. This consists of:\n\n- A type-safe `...Destination` extension function on [NavGraphBuilder](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) for any single screen destination or a type-safe `...Graph` extension function on [NavGraphBuilder](https://developer.android.com/reference/kotlin/androidx/navigation/NavGraphBuilder) for any nested graph destination.\n- A type-safe `navigateTo...` extension function on [NavController](https://developer.android.com/reference/androidx/navigation/NavController) for any destination that is not simply the root of a graph.\n- A type-safe `...Args` class that pulls data from a `SavedStateHandle` for consumption by a VM.\n\nThese are all then grouped into a single `...Navigation.kt` file where all of the raw String-based details can be hidden and only the exposed type-safe functions must be used by the rest of the app.\n\n#### Example\n\nThe following example demonstrates a sample `ExampleNavigation.kt` file with:\n\n- An `ExampleArgs` class that wraps `SavedStateHandle` and allows for the `isToggleEnabledInitialValue` to be extracted in a type-safe way.\n- A `NavGraphBuilder.exampleDestination` extension function that can be used to safely add the `ExampleScreen` to the navigation graph.\n- A `NavController.navigateToExample` extension function that can be called to navigate to the `ExampleScreen` in a type-safe way.\n\nNote in particular how consumers of the above **do not need to know the details of the actual route or path parameter**.\n\n<details>\n<summary>Show example</summary>\n\n```kotlin\n@Serializable\ndata class ExampleRoute(\n    val isToggleEnabled: Boolean,\n)\n\ndata class ExampleArgs(\n    val isToggleEnabledInitialValue: Boolean,\n)\n\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(isToggleEnabledInitialValue = route.isToggleEnabled)\n}\n\nfun NavGraphBuilder.exampleDestination(\n    onNavigateToNextScreen: (CompletionData) -> Unit,\n) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(onNavigateToNextScreen = onNavigateToNextScreen)\n    }\n}\n\nfun NavController.navigateToExample(\n    isToggleEnabled: Boolean,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(\n        route = ExampleRoute(isToggleEnabled = isToggleEnabled),\n        navOptions = navOptions,\n    )\n}\n```\n\n</details>\n","isInternal":false,"tokens":7224,"sizeBytes":35215},{"name":".cursorrules","path":".cursorrules (android-jetpack-compose-cursorrules-prompt-file.mdc)","rawUrl":"https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/android-jetpack-compose-cursorrules-prompt-file.mdc","title":"Awesome Cursorrules: android jetpack compose","category":"cursor-rule","format":"markdown","content":"---\ndescription: \"Cursor rules for Android development with Jetpack Compose integration.\"\nglobs: **/*\nalwaysApply: false\n---\n// Android Jetpack Compose .cursorrules\n\n// Flexibility Notice\n\n// Note: This is a recommended project structure, but be flexible and adapt to existing project structures.\n// Do not enforce these structural patterns if the project follows a different organization.\n// Focus on maintaining consistency with the existing project architecture while applying Jetpack Compose best practices.\n\n// Project Architecture and Best Practices\n\nconst androidJetpackComposeBestPractices = [\n    \"Adapt to existing project architecture while maintaining clean code principles\",\n    \"Follow Material Design 3 guidelines and components\",\n    \"Implement clean architecture with domain, data, and presentation layers\",\n    \"Use Kotlin coroutines and Flow for asynchronous operations\",\n    \"Implement dependency injection using Hilt\",\n    \"Follow unidirectional data flow with ViewModel and UI State\",\n    \"Use Compose navigation for screen management\",\n    \"Implement proper state hoisting and composition\",\n];\n\n// Folder Structure\n\n// Note: This is a reference structure. Adapt to the project's existing organization\n\nconst projectStructure = `\napp/\n  src/\n    main/\n      java/com/package/\n        data/\n          repository/\n          datasource/\n          models/\n        domain/\n          usecases/\n          models/\n          repository/\n        presentation/\n          screens/\n          components/\n          theme/\n          viewmodels/\n        di/\n        utils/\n      res/\n        values/\n        drawable/\n        mipmap/\n    test/\n    androidTest/\n`;\n\n// Compose UI Guidelines\n\nconst composeGuidelines = `\n1. Use remember and derivedStateOf appropriately\n2. Implement proper recomposition optimization\n3. Use proper Compose modifiers ordering\n4. Follow composable function naming conventions\n5. Implement proper preview annotations\n6. Use proper state management with MutableState\n7. Implement proper error handling and loading states\n8. Use proper theming with MaterialTheme\n9. Follow accessibility guidelines\n10. Implement proper animation patterns\n`;\n\n// Testing Guidelines\n\nconst testingGuidelines = `\n1. Write unit tests for ViewModels and UseCases\n2. Implement UI tests using Compose testing framework\n3. Use fake repositories for testing\n4. Implement proper test coverage\n5. Use proper testing coroutine dispatchers\n`;\n\n// Performance Guidelines\n\nconst performanceGuidelines = `\n1. Minimize recomposition using proper keys\n2. Use proper lazy loading with LazyColumn and LazyRow\n3. Implement efficient image loading\n4. Use proper state management to prevent unnecessary updates\n5. Follow proper lifecycle awareness\n6. Implement proper memory management\n7. Use proper background processing\n`;\n","tokens":545,"sizeBytes":2817}],"systemPromptSnippet":"<yakaai_skills repo=\"bitwarden/android\">\n<!-- File: .claude/CLAUDE.md (Tokens: ~1595 | Category: claude-rule) -->\n# Bitwarden Android - Claude Code Configuration\n\nOfficial Android application for Bitwarden Password Manager and Bitwarden Authenticator, providing secure password management, two-factor authentication, and credential autofill services with zero-knowledge encryption.\n\n## Overview\n\n- Multi-module Android application: `:app` (Password Manager), `:authenticator` (2FA TOTP generator)\n- Zero-knowledge architecture: encryption/decryption happens client-side via Bitwarden SDK\n- Target users: End-users via Google Play Store and F-Droid\n\n### Key Concepts\n\n- **Zero-Knowledge Architecture**: Server never has access to unencrypted vault data or encryption keys\n- **Bitwarden SDK**: Rust-based cryptographic SDK handling all encryption/decryption operations\n- **DataState**: Wrapper for streaming data states (Loading, Loaded, Pending, Error, NoNetwork)\n- **Result Types**: Custom sealed classes for operation results (never throw exceptions from data layer)\n- **UDF (Unidirectional Data Flow)**: State flows down, actions flow up through ViewModels\n\n---\n\n## Architecture\n\n```\nUser Request (UI Action)\n         |\n    Screen (Compose)\n         |\n    ViewModel (State/Action/Event)\n         |\n    Repository (Business Logic)\n         |\n    +----+----+----+\n    |    |    |    |\n  Disk  Network  SDK\n   |      |      |\n Room  Retrofit  Bitwarden\n  DB    APIs     Rust SDK\n```\n\n### Key Principles\n\n1. **No Exceptions from Data Layer**: All suspending functions return `Result<T>` or custom sealed classes\n2. **State Hoisting to ViewModel**: All state that affects behavior must live in the ViewModel's state\n3. **Interface-Based DI**: All implementations use interface/`...Impl` pairs with Hilt injection\n4. **Encryption by Default**: All sensitive data encrypted via SDK before storage\n\n### Core Patterns\n\n- **BaseViewModel**: Enforces UDF with State/Action/Event pattern. See `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt`.\n- **Repository Result Pattern**: Type-safe error handling using custom sealed classes for discrete operations and `DataState<T>` wrapper for streaming data.\n- **Common Patterns**: Flow collection via `Internal` actions, error handling via `when` branches, `DataState` streaming with `.map { }` and `.stateIn()`.\n\n> For complete architecture patterns, code templates, and module organization, see `docs/ARCHITECTURE.md`.\n\n---\n\n## Development Guide\n\n### Workflow Skills\n\n> **Quick start**: Use the `bitwarden-tech-lead:bitwarden-tech-lead` agent (or `/plan-android-work <task>`) to refine\n> requirements and plan,\n> then the `bitwarden-software-engineer:bitwarden-software-engineer` agent (or `/work-on-android <task>`) for implementation,\n> then `/review-android <PR#>` to review the result.\n\n## Skills & Commands\n\n| Skill | Triggers |\n|-------|---------|\n| `build-test-verify` | \"build\", \"run tests\", \"lint\", \"format\", \"verify build\" |\n| `implementing-android-code` | \"implement\", \"write code\", \"add screen\", \"create feature\" |\n| `planning-android-implementation` | \"plan implementation\", \"architecture design\", \"phased task breakdown\" |\n| `refining-android-requirements` | \"refine requirements\", \"analyze ticket\", \"gap analysis\" |\n| `reviewing-changes` | \"review\", \"code review\", \"check PR\" |\n| `testing-android-code` | \"write tests\", \"add test coverage\", \"unit test\" |\n\n| Command | Usage |\n|---------|-------|\n| `/plan-android-work <task>` | Fetch ticket → refine requirements → design implementation approach |\n| `/work-on-android <task>` | Full workflow: implement → test → verify → preflight → commit → review → PR |\n| `/review-android <PR#>` | Full review workflow: PR context gathering → Android checklist → output |\n\n---\n\n## Security Rules\n\n**MANDATORY - These rules have no exceptions:**\n\n1. **Zero-Knowledge Architecture**: Never transmit unencrypted vault data or master passwords to the server. All encryption happens client-side via the Bitwarden SDK.\n2. **No Plaintext Key Storage**: Encryption keys must be stored using Android Keystore (biometric unlock) or encrypted with PIN/master password.\n3. **Sensitive Data Cleanup**: On logout, all sensitive data must be cleared from memory and storage via `UserLogoutManager.logout()`.\n4. **Input Validation**: Validate all user inputs before processing, especially URLs and credentials.\n5. **SDK Isolation**: Use scoped SDK sources (`ScopedVaultSdkSource`) to prevent cross-user crypto context leakage.\n\n---\n\n## Code Style & Standards\n\n- **Formatter**: Android Studio with `bitwarden-style.xml` | **Line Limit**: 100 chars | **Detekt**: Enabled\n- **Naming**: `camelCase` (vars/fns), `PascalCase` (classes), `SCREAMING_SNAKE_CASE` (constants), `...Impl` (implementations)\n- **KDoc**: Required for all public APIs\n- **String Resources**: Add new strings to `:ui` module (`ui/src/main/res/values/strings.xml`). Use typographic quotes/apostrophes (`\"` `\"` `'`) not escaped ASCII (`\\\"` `\\'`). Name each resource from its own text content in `snake_case` — not with generic suffixes (`_message`, `_title`). E.g., `one_or_more_email_addresses_are_incorrect`, not `invalid_email_addresses_message`.\n\n> For complete style rules (imports, formatting, documentation, Compose conventions), see `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n---\n\n## Anti-Patterns\n\nIn addition to the Key Principles above, follow these rules:\n\n### DO\n- Map async results to internal actions before updating state\n- Inject `Clock` for time-dependent operations\n- Return early to reduce nesting\n\n### DON'T\n- Update state directly inside coroutines (use internal actions)\n- Use `any` types or suppress null safety\n- Catch generic `Exception` (catch specific types)\n- Use `e.printStackTrace()` (use Timber logging)\n- Create new patterns when established ones exist\n- Skip KDoc for public APIs\n\n---\n\n## Quick Reference\n\n- **Code style**: Full rules: `docs/STYLE_AND_BEST_PRACTICES.md`\n- **Before writing code**: Use `implementing-android-code` skill for Bitwarden-specific patterns, gotchas, and templates\n- **Before writing tests**: Use `testing-android-code` skill for test patterns and templates\n- **Building/testing**: Use `build-test-verify` skill | App tests: `./gradlew app:testStandardDebugUnitTest`\n- **Before committing**: Use `bitwarden-delivery-tools:perform-preflight` skill, then `bitwarden-delivery-tools:committing-changes` skill for message format\n- **Code review**: Use `/review-android` for the full review workflow; `reviewing-changes` skill for checklist-only\n- **Creating PRs**: Use `bitwarden-delivery-tools:creating-pull-request` skill for PR workflow and templates\n- **Troubleshooting**: See `docs/TROUBLESHOOTING.md`\n- **Architecture**: `docs/ARCHITECTURE.md` | [Bitwarden SDK](https://github.com/bitwarden/sdk) | [Jetpack Compose](https://developer.android.com/jetpack/compose) | [Hilt DI](https://dagger.dev/hilt/)\n\n<!-- File: .claude/skills/build-test-verify/SKILL.md (Tokens: ~1484 | Category: anthropic-skill) -->\n---\nname: build-test-verify\nversion: 0.1.0\ndescription: Build, test, lint, and deploy commands for the Bitwarden Android project. Use when running tests, building APKs/AABs, running lint/detekt, deploying, using fastlane, or discovering codebase structure. Triggered by \"run tests\", \"build\", \"gradle\", \"lint\", \"detekt\", \"deploy\", \"fastlane\", \"assemble\", \"verify\", \"coverage\".\n---\n\n# Build, Test & Verify\n\n## Environment Setup\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `GITHUB_TOKEN` | Yes (CI) | GitHub Packages auth for SDK (`read:packages` scope) |\n| Build flavors | - | `standard` (Play Store), `fdroid` (no Google services) |\n| Build types | - | `debug`, `beta`, `release` |\n\nIf builds fail resolving the Bitwarden SDK, verify `GITHUB_TOKEN` in `user.properties` or environment and check connectivity to `maven.pkg.github.com`.\n\n---\n\n## Building\n\n```bash\n# Debug builds\n./gradlew app:assembleDebug\n./gradlew authenticator:assembleDebug\n\n# Release builds (requires signing keys)\n./gradlew app:assembleStandardRelease\n./gradlew app:bundleStandardRelease\n\n# F-Droid builds\n./gradlew app:assembleFdroidRelease\n```\n\n---\n\n## Running Tests\n\n**IMPORTANT**: The app module uses the `standard` flavor. Always use `testStandardDebugUnitTest`, NOT `testDebugUnitTest`.\n\n**IMPORTANT**: Always pipe test output through a filter that captures failures on the first run. Gradle suppresses detailed failure output by default, so use `2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30` to see pass/fail results and assertion details without needing a second run.\n\n```bash\n# App module tests (correct flavor!)\n./gradlew app:testStandardDebugUnitTest 2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30\n\n# Run specific test classes\n./gradlew app:testStandardDebugUnitTest --tests \"com.x8bit.bitwarden.SomeTest\" 2>&1 | grep -E \"FAILED|BUILD|expected:|actual:|AssertionError|failures\" | head -30\n\n# Run all unit tests across all modules\n./gradlew test\n\n# Individual shared modules (no flavor needed)\n./gradlew :core:test\n./gradlew :data:test\n./gradlew :network:test\n./gradlew :ui:test\n\n# Authenticator module\n./gradlew authenticator:testStandardDebugUnitTest\n```\n\n### Reading Test Reports\n\nIf you need full failure details beyond what grep captures, check the HTML test report:\n\n```bash\n# After a test run, open the report at:\n# app/build/reports/tests/testStandardDebugUnitTest/index.html\n# Or read individual failure XML:\nfind app/build/test-results -name \"*.xml\" -exec grep -l \"failure\" {} \\;\n```\n\n### Test Structure\n\n```\napp/src/test/                    # App unit tests\napp/src/testFixtures/            # App test utilities\ncore/src/testFixtures/           # Core test utilities (FakeDispatcherManager)\ndata/src/testFixtures/           # Data test utilities (FakeSharedPreferences)\nnetwork/src/testFixtures/        # Network test utilities (BaseServiceTest)\nui/src/testFixtures/             # UI test utilities (BaseViewModelTest, BaseComposeTest)\n```\n\n### Test Quick Reference\n\n- **Dispatcher Control**: `FakeDispatcherManager` from `:core:testFixtures`\n- **MockK**: `mockk<T> { every { } returns }`, `coEvery { }` for suspend\n- **Flow Testing**: Turbine with `stateEventFlow()` helper from `BaseViewModelTest`\n- **Time Control**: Inject `Clock` for deterministic time testing\n\n---\n\n## Lint & Static Analysis\n\n**IMPORTANT**: Prefer running detekt on modified files only — a full project scan is slow and unnecessary during development. The project supports a `-Pprecommit=true` flag that limits detekt to staged files.\n\n**IMPORTANT**: Always pipe detekt output through a filter to capture errors on the first run. Detekt prints violation details to stderr/stdout but Gradle can obscure them. Use the grep pattern below to see violations immediately.\n\n```bash\n# Detekt on staged files only (preferred during development)\ngit add -u && ./gradlew -Pprecommit=true detekt 2>&1 | grep -E \"FAILED|BUILD|Line |Rule |Signature|detekt\" | head -40\n\n# Detekt on all files (full scan, use sparingly)\n./gradlew detekt 2>&1 | grep -E \"FAILED|BUILD|Line |Rule |Signature|detekt\" | head -40\n\n# Android Lint\n./gradlew lint\n\n# Full validation suite (detekt + lint + tests + coverage)\n./fastlane check\n```\n\n### How `-Pprecommit=true` Works\n\nThe root `build.gradle.kts` configures detekt tasks to use `git diff --name-only --cached` when this property is set, limiting analysis to staged files only. This is the same mechanism used by the project's pre-commit hook. Stage your changes with `git add` before running.\n\n---\n\n## Codebase Discovery\n\n```bash\n# Find existing Bitwarden UI components\nfind ui/src/main/kotlin/com/bitwarden/ui/platform/components/ -name \"Bitwarden*.kt\" | sort\n\n# Find all ViewModels\ngrep -rl \"BaseViewModel<\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Find all Navigation files with @Serializable routes\nfind app/src/main/kotlin/ -name \"*Navigation.kt\" | sort\n\n# Find all Hilt modules\nfind app/src/main/kotlin/ -name \"*Module.kt\" -path \"*/di/*\" | sort\n\n# Find all repository interfaces\nfind app/src/main/kotlin/ -name \"*Repository.kt\" -not -name \"*Impl.kt\" -path \"*/repository/*\" | sort\n\n# Find encrypted disk source examples\ngrep -rl \"EncryptedPreferences\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Find Clock injection usage\ngrep -rl \"private val clock: Clock\" app/src/main/kotlin/ --include=\"*.kt\"\n\n# Search existing strings before adding new ones\ngrep -n \"search_term\" ui/src/main/res/values/strings.xml\n```\n\n---\n\n## Deployment & Versioning\n\n**Version location**: `gradle/libs.versions.toml`\n```toml\nappVersionCode = \"1\"\nappVersionName = \"2025.11.1\"\n```\nPattern: `YEAR.MONTH.PATCH`\n\n**Publishing channels**:\n- **Play Store**: GitHub Actions workflow with signed AAB\n- **F-Droid**: Dedicated workflow with F-Droid signing keys\n- **Firebase App Distribution**: Beta testing\n\n<!-- File: .claude/skills/evaluating-sdk-internal-updates/SKILL.md (Tokens: ~1232 | Category: anthropic-skill) -->\n---\nname: evaluating-sdk-internal-updates\ndescription: Evaluates a bitwarden/android \"Update SDK to\" PR against the sdk-internal commit range for compile-time and runtime breaking changes, maps affected symbols to Android call sites, and applies clear in-scope fixes. Use when reviewing an SDK bump PR, a bitwardenSdk version change, or triaging sdk-internal breaking changes.\nallowed-tools:\n  - Bash(gh pr diff:*)\n  - Bash(git -C * log *)\n  - Bash(git -C * show *)\n  - Bash(git add:*)\n  - Bash(git commit:*)\n  - Bash(grep:*)\n  - Bash(./gradlew *)\n  - Read\n  - Grep\n  - Glob\n  - Edit\n  - Write\n  - Skill(implementing-android-code)\n  - Skill(bitwarden-delivery-tools:committing-changes)\n---\n\n# Evaluating sdk-internal Updates\n\n**Identify both compile-time and runtime breaks before fixing anything — fixing the first break found is not finishing.** Steps 3-7 always cover the entire commit range before step 8 starts, no matter how obvious or urgent an early compile-time break looks.\n\n## Identify\n\nBinding surface facts specific to this SDK: `#[uniffi::export]` / `derive(uniffi::...)` annotations are scattered across many crates, not just `crates/bitwarden-uniffi`; `crates/bitwarden-ffi` is unrelated (\"do not use\"). No `.udl` files. UniFFI emits one Kotlin package per crate (`com.bitwarden.core`, `com.bitwarden.vault`, `com.bitwarden.crypto`, etc.) — only the top-level `Client`/`AuthClient`/`GeneratorClients` actually live under `com.bitwarden.sdk`. Both `app` and `authenticator` depend on the SDK; neither is optional to search.\n\nA hunk that only touches a macro invocation (e.g. `state_bridge! { ... }`) doesn't show the binding surface — the expansion lives in the macro's definition, often in a different crate (e.g. `bitwarden-state-bridge-macro`). `#[uniffi::export(with_foreign)]` marks a callback interface: a trait Kotlin must implement, where adding a field/method is never additive-safe for the implementor.\n\n1. Locate the local `bitwarden/sdk-internal` clone (check sibling directories to this repo). If none exists, stop and tell the user it's a required prerequisite for this skill — do not clone it yourself.\n2. `gh pr diff <PR> -R bitwarden/android | grep bitwardenSdk` → old/new `bitwardenSdk` string. Everything after the second `-` is the git ref — a commit SHA or a branch name (`.dev` SDK builds use both); resolve a branch name as `origin/<branch>` in the clone.\n3. Attempt `./gradlew app:compileStandardDebugKotlin authenticator:compileDebugKotlin` at the current checkout before crawling sdk-internal. A failure confirms a compile-time break directly, with a more precise location than any git search — note it and continue to steps 4-7 for the full commit range; do not fix it yet. A clean build only rules out compile-time breaks, not runtime ones.\n4. `git -C <sdk-internal-path> log --oneline OLD..NEW -G'uniffi::export|derive\\(uniffi|#\\[uniffi' -- '*.rs'` → candidate binding-surface commits.\n5. Classify per hunk, not per commit — a commit with one additive headline change can still have a second, unrelated breaking hunk. If a hunk only touches a macro invocation, read the macro's definition before classifying. `git -C <sdk-internal-path> show <sha> -- '*.rs'`.\n6. For every distinct symbol/type touched (every hunk, not just the commit's headline change), grep the whole repo for the bare symbol name to find Android call sites — a fixed module list or a `com.bitwarden.sdk.<Symbol>` import-prefix check both miss real consumers.\n7. Report: compile-time breaks, runtime breaks, safe/no-call-site — each with commit, symbol, and call sites. Cite sdk-internal commits and PRs as `bitwarden/sdk-internal#<NNN>` or a full commit URL/SHA — never a bare `#<NNN>` copied from a commit subject line. A bare reference auto-links within whatever repo the report is posted to (e.g. `bitwarden/android`), tagging an unrelated issue or PR there.\n\n## Resolve\n\nResolve the findings from Step 7 by deciding on a fix (step 8), implementing and committing it directly (step 9), then verifying (step 10).\n\n8. Decide the fix for anything found, compile-time or runtime, whenever the correct behavior is clear and within scope. For a new required method, grep for the underlying concept, not the new method/type name (it won't exist yet) — no existing consumer means stub it: a `// no-op` comment or a null/default return that satisfies the compiler, not a behavioral decision. Never `TODO()` — it throws at runtime, which is a crash, not a stub. A sibling's structure (naming, placement, style) is a template; its behavior (storage, defaulting, error handling, side effects) is not evidence for yours. If unsure, report it instead of guessing, along with anything needing a product decision.\n9. Implement every fix from step 8 directly: invoke `Skill(implementing-android-code)` first if the fix isn't purely mechanical, then commit with `Skill(bitwarden-delivery-tools:committing-changes)`.\n10. Verify with the same compile task used in step 3.\n\n<!-- File: .claude/skills/implementing-android-code/SKILL.md (Tokens: ~4751 | Category: anthropic-skill) -->\n---\nname: implementing-android-code\nversion: 0.1.4\ndescription: This skill should be used when implementing Android code in Bitwarden. Covers critical patterns, gotchas, and anti-patterns unique to this codebase. Triggered by \"How do I implement a ViewModel?\", \"Create a new screen\", \"Add navigation\", \"Write a repository\", \"BaseViewModel pattern\", \"State-Action-Event\", \"type-safe navigation\", \"@Serializable route\", \"SavedStateHandle persistence\", \"process death recovery\", \"handleAction\", \"sendAction\", \"Hilt module\", \"Repository pattern\", \"implementing a screen\", \"adding a data source\", \"handling navigation\", \"encrypted storage\", \"security patterns\", \"Clock injection\", \"DataState\", or any questions about implementing features, screens, ViewModels, data sources, or navigation in the Bitwarden Android app.\n---\n\n# Implementing Android Code - Bitwarden Quick Reference\n\n**This skill provides tactical guidance for Bitwarden-specific patterns.** For comprehensive architecture decisions and complete code style rules, consult `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n---\n\n## Critical Patterns Reference\n\n### A. ViewModel Implementation (State-Action-Event Pattern)\n\nAll ViewModels follow the **State-Action-Event (SAE)** pattern via `BaseViewModel<State, Event, Action>`.\n\n**Key Requirements:**\n- Annotate with `@HiltViewModel`\n- State class MUST be `@Parcelize data class : Parcelable`\n- Implement `handleAction(action: A)` - MUST be synchronous\n- Post internal actions from coroutines using `sendAction()`\n- Save/restore state via `SavedStateHandle[KEY_STATE]`\n- Private action handlers: `private fun handle*` naming convention\n\n**Template**: See [ViewModel template](templates.md#viewmodel-template-state-action-event-pattern)\n\n**Pattern Summary:**\n```kotlin\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val repository: ExampleRepository,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    initialState = savedStateHandle[KEY_STATE] ?: ExampleState(),\n) {\n    init {\n        stateFlow.onEach { savedStateHandle[KEY_STATE] = it }.launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        // Synchronous dispatch only\n        when (action) {\n            is Action.Click -> handleClick()\n            is Action.Internal.DataReceived -> handleDataReceived(action)\n        }\n    }\n\n    private fun handleClick() {\n        viewModelScope.launch {\n            val result = repository.fetchData()\n            sendAction(Action.Internal.DataReceived(result))  // Post internal action\n        }\n    }\n\n    private fun handleDataReceived(action: Action.Internal.DataReceived) {\n        mutableStateFlow.update { it.copy(data = action.result) }\n    }\n}\n```\n\n**Reference:**\n- `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)\n- `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt` (see class declaration)\n\n**Critical Gotchas:**\n- ❌ **NEVER** update `mutableStateFlow` directly inside coroutines\n- ✅ **ALWAYS** post internal actions from coroutines, update state in `handleAction()`\n- ❌ **NEVER** forget `@IgnoredOnParcel` for sensitive data (causes security leak)\n- ✅ **ALWAYS** use `@Parcelize` on state classes for process death recovery\n- ✅ State restoration happens automatically if properly saved to `SavedStateHandle`\n\n---\n\n### B. Navigation Implementation (Type-Safe)\n\nAll navigation uses **type-safe routes** with kotlinx.serialization.\n\n**Pattern Structure:**\n1. `@Serializable` route data class with parameters\n2. `...Args` helper class for extracting from `SavedStateHandle`\n3. `NavGraphBuilder.{screen}Destination()` extension for adding screen to graph\n4. `NavController.navigateTo{Screen}()` extension for navigation calls\n\n**Template**: See [Navigation template](templates.md#navigation-template-type-safe-routes)\n\n**Pattern Summary:**\n```kotlin\n@Serializable\ndata class ExampleRoute(val userId: String, val isEditMode: Boolean = false)\n\ndata class ExampleArgs(val userId: String, val isEditMode: Boolean)\n\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(userId = route.userId, isEditMode = route.isEditMode)\n}\n\nfun NavController.navigateToExample(\n    userId: String,\n    isEditMode: Boolean = false,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(route = ExampleRoute(userId, isEditMode), navOptions = navOptions)\n}\n\nfun NavGraphBuilder.exampleDestination(onNavigateBack: () -> Unit) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(onNavigateBack = onNavigateBack)\n    }\n}\n```\n\n**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt` (see `LoginRoute` and extensions)\n\n**Key Benefits:**\n- ✅ Type safety: Compile-time errors for missing parameters\n- ✅ No string literals in navigation code\n- ✅ Automatic serialization/deserialization\n- ✅ Clear contract for screen dependencies\n\n---\n\n### C. Screen/Compose Implementation\n\nAll screens follow consistent Compose patterns.\n\n**Template**: See [Screen/Compose template](templates.md#screencompose-template)\n\n**Key Patterns:**\n```kotlin\n@Composable\nfun ExampleScreen(\n    onNavigateBack: () -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            ExampleEvent.NavigateBack -> onNavigateBack()\n        }\n    }\n\n    BitwardenScaffold(\n        topBar = {\n            BitwardenTopAppBar(\n                title = stringResource(R.string.title),\n                navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_back),\n                onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },\n            )\n        },\n    ) {\n        // UI content\n    }\n}\n```\n\n**Reference:** `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt` (see `LoginScreen` composable)\n\n**Essential Requirements:**\n- ✅ Use `hiltViewModel()` for dependency injection\n- ✅ Use `collectAsStateWithLifecycle()` for state (not `collectAsState()`)\n- ✅ Use `EventsEffect(viewModel)` for one-shot events\n- ✅ Use `Bitwarden*` prefixed components from `:ui` module\n\n**State Hoisting Rules:**\n- **ViewModel state**: Data that needs to survive process death or affects business logic\n- **UI-only state**: Temporary UI state (scroll position, text field focus) using `remember` or `rememberSaveable`\n\n---\n\n### D. Data Layer Implementation\n\nThe data layer follows strict patterns for repositories, managers, and data sources.\n\n**Interface + Implementation Separation (ALWAYS)**\n\n**Template**: See [Data Layer template](templates.md#data-layer-template-repository--hilt-module)\n\n**Pattern Summary:**\n```kotlin\n// Interface (injected via Hilt)\ninterface ExampleRepository {\n    suspend fun fetchData(id: String): ExampleResult\n    val dataFlow: StateFlow<DataState<ExampleData>>\n}\n\n// Implementation (NOT directly injected)\nclass ExampleRepositoryImpl(\n    private val exampleDiskSource: ExampleDiskSource,\n    private val exampleService: ExampleService,\n) : ExampleRepository {\n    override suspend fun fetchData(id: String): ExampleResult {\n        // NO exceptions thrown - return Result or sealed class\n        return exampleService.getData(id).fold(\n            onSuccess = { ExampleResult.Success(it.toModel()) },\n            onFailure = { ExampleResult.Error(it.message) },\n        )\n    }\n}\n\n// Sealed result class (domain-specific)\nsealed class ExampleResult {\n    data class Success(val data: ExampleData) : ExampleResult()\n    data class Error(val message: String?) : ExampleResult()\n}\n\n// Hilt Module\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleRepositoryModule {\n    @Provides\n    @Singleton\n    fun provideExampleRepository(\n        exampleDiskSource: ExampleDiskSource,\n        exampleService: ExampleService,\n    ): ExampleRepository = ExampleRepositoryImpl(exampleDiskSource, exampleService)\n}\n```\n\n**Reference:**\n- `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt`\n- `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`\n\n**Three-Layer Data Architecture:**\n1. **Data Sources** - Raw data access (network, disk, SDK). Return `Result<T>`, never throw.\n2. **Managers** - Single responsibility business logic. Wrap OS/external services.\n3. **Repositories** - Aggregate sources/managers. Return domain-specific sealed classes.\n\n**Critical Rules:**\n- ❌ **NEVER** throw exceptions in data layer\n- ✅ **ALWAYS** use interface + `...Impl` pattern\n- ✅ **ALWAYS** inject interfaces, never implementations\n- ✅ Data sources return `Result<T>`, repositories return domain sealed classes\n- ✅ Use `StateFlow` for continuously observed data\n\n**KDoc on Interfaces vs. Implementations**\n\nKDoc on an interface member describes the **contract** — what the caller can rely on. It must not describe **how** the implementation fulfills that contract. Implementation details (which data source is consulted, what is cached, what is logged, which manager is delegated to, ordering of internal calls, etc.) belong on the `...Impl` member — and only when the *why* is non-obvious from the code itself.\n\nThis applies to every interface in the codebase: repositories, managers, data sources, validators, and UI-layer interfaces alike. The rule is the same one CLAUDE.md states for comments in general — describe the contract or the non-obvious *why*, never the *what* a well-named identifier already conveys.\n\n❌ **Wrong** — interface KDoc leaks the implementation:\n```kotlin\ninterface ExampleRepository {\n    /**\n     * Fetches data by reading from [ExampleDiskSource] first, falling back to\n     * [ExampleService] on cache miss and writing the network result back to disk.\n     */\n    suspend fun fetchData(id: String): ExampleResult\n}\n```\n\n✅ **Right** — interface KDoc describes the contract; implementation details (if needed at all) live on the override:\n```kotlin\ninterface ExampleRepository {\n    /** Returns the [ExampleData] for [id], or an error result on failure. */\n    suspend fun fetchData(id: String): ExampleResult\n}\n\nclass ExampleRepositoryImpl(...) : ExampleRepository {\n    // No KDoc needed — the disk-then-network pattern is visible in the code.\n    override suspend fun fetchData(id: String): ExampleResult { ... }\n}\n```\n\nRed flags that an interface KDoc has drifted into implementation territory:\n- Names a concrete collaborator (`...DiskSource`, `...Service`, `...Manager`, `...Impl`)\n- Describes ordering of internal calls (\"first ... then ...\", \"falls back to ...\")\n- Mentions caching, retries, logging, or threading behavior that callers don't depend on\n- Restates the method signature in prose (\"Suspends until X returns Y\")\n\nIf callers genuinely depend on a behavior (e.g., \"this method is safe to call before vault unlock\", \"result is cached for the session\"), that *is* part of the contract and belongs on the interface. The test: would a different valid implementation be free to change this? If yes, it's implementation detail — strip it.\n\n---\n\n### E. UI Components\n\n**Use Existing Components First:**\n\nThe `:ui` module provides reusable `Bitwarden*` prefixed components. Search before creating new ones.\n\n**Common Components:**\n- `BitwardenFilledButton` - Primary action buttons\n- `BitwardenOutlinedButton` - Secondary action buttons\n- `BitwardenTextField` - Text input fields\n- `BitwardenPasswordField` - Password input with show/hide\n- `BitwardenSwitch` - Toggle switches\n- `BitwardenTopAppBar` - Toolbar/app bar\n- `BitwardenScaffold` - Screen container with scaffold\n- `BitwardenBasicDialog` - Simple dialogs\n- `BitwardenLoadingDialog` - Loading indicators\n\n**Component Discovery:**\nSearch `ui/src/main/kotlin/com/bitwarden/ui/platform/components/` for existing `Bitwarden*` components. For build, test, and codebase discovery commands, use the **`build-test-verify`** skill.\n\n**When to Create New Reusable Components:**\n- Component used in 3+ places\n- Component needs consistent theming across app\n- Component has semantic meaning (accessibility)\n- Component has complex state management\n\n**New Component Requirements:**\n- Prefix with `Bitwarden`\n- Accept themed colors/styles from `BitwardenTheme`\n- Include preview composables for testing\n- Support accessibility (content descriptions, semantics)\n\n**String Resources:**\n\nNew strings belong in the `:ui` module: `ui/src/main/res/values/strings.xml`\n\n- Use typographic apostrophes and quotes to avoid escape characters: `you’ll` not `you\\'ll`, `“word”` not `\\\"word\\\"`\n- Reference strings via generated `BitwardenString` resource IDs\n- Do not add strings to other modules unless explicitly instructed\n\n---\n\n### F. Security Patterns\n\n**Encrypted vs Unencrypted Storage:**\n\n**Template**: See [Security templates](templates.md#security-templates)\n\n**Pattern Summary:**\n```kotlin\nclass ExampleDiskSourceImpl(\n    @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,\n    @UnencryptedPreferences sharedPreferences: SharedPreferences,\n) : BaseEncryptedDiskSource(\n    encryptedSharedPreferences = encryptedSharedPreferences,\n    sharedPreferences = sharedPreferences,\n),\n    ExampleDiskSource {\n    fun storeAuthToken(token: String) {\n        putEncryptedString(KEY_TOKEN, token)  // Sensitive — uses base class method\n    }\n\n    fun storeThemePreference(isDark: Boolean) {\n        putBoolean(KEY_THEME, isDark)  // Non-sensitive — uses base class method\n    }\n}\n```\n\n**Android Keystore (Biometric Keys):**\n- User-scoped encryption keys: `BiometricsEncryptionManager`\n- Keys stored in Android Keystore (hardware-backed when available)\n- Integrity validation on biometric state changes\n\n**Input Validation:**\n```kotlin\n// Validation returns boolean, NEVER throws\ninterface RequestValidator {\n    fun validate(request: Request): Boolean\n}\n\n// Sanitization removes dangerous content\nfun String?.sanitizeTotpUri(issuer: String?, username: String?): String? {\n    if (this.isNullOrBlank()) return null\n    // Sanitize and return safe value\n}\n```\n\n**Security Checklist:**\n- ✅ Use `@EncryptedPreferences` for credentials, keys, tokens\n- ✅ Use `@UnencryptedPreferences` for UI state, preferences\n- ✅ Use `@IgnoredOnParcel` for sensitive ViewModel state\n- ❌ **NEVER** log sensitive data (passwords, tokens, vault items)\n- ✅ Validate all user input before processing\n- ✅ Use Timber for non-sensitive logging only\n\n---\n\n### G. Testing Patterns\n\n**ViewModel Testing:**\n\n**Template**: See [Testing templates](templates.md#testing-templates)\n\n**Pattern Summary:**\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n    private val mockRepository: ExampleRepository = mockk()\n\n    @Test\n    fun `ButtonClick should fetch data and update state`() = runTest {\n        val expectedResult = ExampleResult.Success(data = \"test\")\n        coEvery { mockRepository.fetchData(any()) } returns expectedResult\n\n        val viewModel = createViewModel()\n        viewModel.trySendAction(ExampleAction.ButtonClick)\n\n        viewModel.stateFlow.test {\n            assertEquals(EXPECTED_STATE.copy(data = \"test\"), awaitItem())\n        }\n    }\n\n    private fun createViewModel(): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to EXPECTED_STATE)),\n        repository = mockRepository,\n    )\n}\n```\n\n**Reference:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n**Key Testing Patterns:**\n- ✅ Extend `BaseViewModelTest` for proper dispatcher management\n- ✅ Use `runTest` from `kotlinx.coroutines.test`\n- ✅ Use Turbine's `.test { awaitItem() }` for Flow assertions\n- ✅ Use MockK: `coEvery` for suspend functions, `every` for sync\n- ✅ Test both state changes and event emissions\n- ✅ Test both success and failure Result paths\n\n**Flow Testing with Turbine:**\n```kotlin\n// Test state and events simultaneously\nviewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n    viewModel.trySendAction(ExampleAction.Submit)\n    assertEquals(ExpectedState.Loading, stateFlow.awaitItem())\n    assertEquals(ExampleEvent.ShowSuccess, eventFlow.awaitItem())\n}\n```\n\n**MockK Quick Reference:**\n```kotlin\ncoEvery { repository.fetchData(any()) } returns Result.success(\"data\")  // Suspend\nevery { diskSource.getData() } returns \"cached\"  // Sync\ncoVerify { repository.fetchData(\"123\") }  // Verify\n```\n\n---\n\n### H. Clock/Time Handling\n\nAll code needing current time must inject `Clock` for testability.\n\n**Key Requirements:**\n- ✅ Inject `Clock` via Hilt in ViewModels\n- ✅ Pass `Clock` as parameter in extension functions\n- ✅ Use `clock.instant()` to get current time\n- ❌ Never call `Instant.now()` or `DateTime.now()` directly\n- ❌ Never use `mockkStatic` for datetime classes in tests\n\n**Pattern Summary:**\n```kotlin\n// ViewModel with Clock\nclass MyViewModel @Inject constructor(\n    private val clock: Clock,\n) {\n    val timestamp = clock.instant()\n}\n\n// Extension function with Clock parameter\nfun State.getTimestamp(clock: Clock): Instant =\n    existingTime ?: clock.instant()\n\n// Test with fixed clock\nval FIXED_CLOCK = Clock.fixed(\n    Instant.parse(\"2023-10-27T12:00:00Z\"),\n    ZoneOffset.UTC,\n)\n```\n\n**Reference:**\n- `docs/STYLE_AND_BEST_PRACTICES.md` (see Time and Clock Handling section)\n- `core/src/main/kotlin/com/bitwarden/core/di/CoreModule.kt` (see `provideClock` function)\n\n**Critical Gotchas:**\n- ❌ `Instant.now()` creates hidden dependency, non-testable\n- ❌ `mockkStatic(Instant::class)` is fragile, can leak between tests\n- ✅ `Clock.fixed(...)` provides deterministic test behavior\n\n---\n\n### I. Kotlin Style Rules\n\nProject-specific style conventions enforced in code review. These supplement (not replace) `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n**`when` branches with wrapped right-hand side require curly braces.**\n\nWhen a `when` branch's expression is too long to fit on the same line as the arrow and is wrapped to the next line, wrap the body in `{ }`. A bare `->` followed by an indented expression on its own line is rejected in review.\n\n❌ **Wrong** — wrapped body without braces:\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT ->\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    VaultItemCipherType.DRIVERS_LICENSE ->\n        VaultAddEditState.ViewState.Content.ItemType.DriversLicense()\n}\n```\n\n✅ **Right** — wrapped body with braces:\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT -> {\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    }\n    VaultItemCipherType.DRIVERS_LICENSE -> {\n        VaultAddEditState.ViewState.Content.ItemType.DriversLicense()\n    }\n}\n```\n\nSingle-line branches (body fits on the same line as `->`) do **not** need braces.\n\n---\n\n## Bitwarden-Specific Anti-Patterns\n\n**General anti-patterns are documented in CLAUDE.md.** This section covers violations specific to Bitwarden's State-Action-Event, navigation, and data layer patterns:\n\n❌ **NEVER update ViewModel state directly in coroutines**\n- Post internal actions, update state synchronously in `handleAction()`\n\n❌ **NEVER inject `...Impl` classes**\n- Only inject interfaces via Hilt\n\n❌ **NEVER create navigation without `@Serializable` routes**\n- No string-based navigation, always type-safe\n\n❌ **NEVER use raw `Result<T>` in repositories**\n- Use domain-specific sealed classes for better error handling\n\n❌ **NEVER make state classes without `@Parcelize`**\n- All ViewModel state must survive process death\n\n❌ **NEVER skip `SavedStateHandle` persistence for ViewModels**\n- Users lose form progress on process death\n\n❌ **NEVER forget `@IgnoredOnParcel` for passwords/tokens**\n- Causes security vulnerability (sensitive data in parcel)\n\n❌ **NEVER use generic `Exception` catching**\n- Catch specific exceptions only (`RemoteException`, `IOException`)\n\n❌ **NEVER call `Instant.now()` or `DateTime.now()` directly**\n- Inject `Clock` via Hilt, use `clock.instant()` for testability\n\n❌ **NEVER put implementation details in interface KDoc**\n- Interface KDoc describes the contract; concrete collaborators, call ordering, caching, and fallback behavior belong on the `...Impl` override (and only when the *why* is non-obvious). See section D for examples.\n\n---\n\n## Quick Reference\n\nFor build, test, and codebase discovery commands, use the **`build-test-verify`** skill.\n\n**File Reference Format:**\nWhen pointing to specific code, use: `file_path:line_number`\n\nExample: `ui/src/main/kotlin/com/bitwarden/ui/platform/base/BaseViewModel.kt` (see `handleAction` method)\n\n<!-- File: .claude/skills/implementing-android-code/templates.md (Tokens: ~3567 | Category: anthropic-skill) -->\n# Code Templates - Bitwarden Android\n\nCopy-pasteable templates derived from actual codebase patterns. Replace `Example` with your feature name.\n\n---\n\n## ViewModel Template (State-Action-Event Pattern)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt`\n\n### State Class\n\n```kotlin\n@Parcelize\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    @IgnoredOnParcel val sensitiveInput: String = \"\", // Sensitive data excluded from parcel\n    val dialogState: DialogState? = null,\n) : Parcelable {\n\n    /**\n     * Dialog states for the Example screen.\n     */\n    sealed class DialogState : Parcelable {\n        @Parcelize\n        data class Error(\n            val title: Text? = null,\n            val message: Text,\n            val error: Throwable? = null,\n        ) : DialogState()\n\n        @Parcelize\n        data class Loading(val message: Text) : DialogState()\n    }\n}\n```\n\n### Event Sealed Class\n\n```kotlin\n/**\n * One-shot UI events for the Example screen.\n */\nsealed class ExampleEvent {\n    data object NavigateBack : ExampleEvent()\n\n    data class ShowToast(val message: Text) : ExampleEvent()\n}\n```\n\n### Action Sealed Class (with Internal)\n\n```kotlin\n/**\n * User and system actions for the Example screen.\n */\nsealed class ExampleAction {\n    data object BackClick : ExampleAction()\n\n    data object SubmitClick : ExampleAction()\n\n    data class InputChanged(val input: String) : ExampleAction()\n\n    data object ErrorDialogDismiss : ExampleAction()\n\n    /**\n     * Internal actions dispatched by the ViewModel from coroutines.\n     */\n    sealed class Internal : ExampleAction() {\n        data class ReceiveDataState(\n            val dataState: DataState<ExampleData>,\n        ) : Internal()\n\n        data class ReceiveDataResult(\n            val result: ExampleResult,\n        ) : Internal()\n    }\n}\n```\n\n### ViewModel\n\n```kotlin\nprivate const val KEY_STATE = \"state\"\n\n/**\n * ViewModel for the Example screen.\n */\n@HiltViewModel\nclass ExampleViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    private val exampleRepository: ExampleRepository,\n) : BaseViewModel<ExampleState, ExampleEvent, ExampleAction>(\n    initialState = savedStateHandle[KEY_STATE]\n        ?: run {\n            val args = savedStateHandle.toExampleArgs()\n            ExampleState(\n                data = args.itemId,\n            )\n        },\n) {\n\n    init {\n        // Persist state for process death recovery\n        stateFlow\n            .onEach { savedStateHandle[KEY_STATE] = it }\n            .launchIn(viewModelScope)\n\n        // Collect repository flows as internal actions\n        exampleRepository.dataFlow\n            .map { ExampleAction.Internal.ReceiveDataState(it) }\n            .onEach(::sendAction)\n            .launchIn(viewModelScope)\n    }\n\n    override fun handleAction(action: ExampleAction) {\n        when (action) {\n            ExampleAction.BackClick -> handleBackClick()\n            ExampleAction.SubmitClick -> handleSubmitClick()\n            ExampleAction.ErrorDialogDismiss -> handleErrorDialogDismiss()\n            is ExampleAction.InputChanged -> handleInputChanged(action)\n            is ExampleAction.Internal.ReceiveDataState -> {\n                handleReceiveDataState(action)\n            }\n            is ExampleAction.Internal.ReceiveDataResult -> {\n                handleReceiveDataResult(action)\n            }\n        }\n    }\n\n    private fun handleBackClick() {\n        sendEvent(ExampleEvent.NavigateBack)\n    }\n\n    private fun handleErrorDialogDismiss() {\n        mutableStateFlow.update { it.copy(dialogState = null) }\n    }\n\n    private fun handleSubmitClick() {\n        viewModelScope.launch {\n            val result = exampleRepository.submitData(state.data.orEmpty())\n            sendAction(ExampleAction.Internal.ReceiveDataResult(result))\n        }\n    }\n\n    private fun handleInputChanged(action: ExampleAction.InputChanged) {\n        mutableStateFlow.update { it.copy(sensitiveInput = action.input) }\n    }\n\n    private fun handleReceiveDataState(\n        action: ExampleAction.Internal.ReceiveDataState,\n    ) {\n        when (action.dataState) {\n            is DataState.Loaded -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        data = action.dataState.data.toString(),\n                    )\n                }\n            }\n\n            is DataState.Loading -> {\n                mutableStateFlow.update { it.copy(isLoading = true) }\n            }\n\n            is DataState.Error -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        dialogState = ExampleState.DialogState.Error(\n                            message = BitwardenString.generic_error_message.asText(),\n                            error = action.dataState.error,\n                        ),\n                    )\n                }\n            }\n\n            else -> Unit\n        }\n    }\n\n    private fun handleReceiveDataResult(\n        action: ExampleAction.Internal.ReceiveDataResult,\n    ) {\n        when (val result = action.result) {\n            is ExampleResult.Success -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        data = result.data,\n                    )\n                }\n            }\n\n            is ExampleResult.Error -> {\n                mutableStateFlow.update {\n                    it.copy(\n                        isLoading = false,\n                        dialogState = ExampleState.DialogState.Error(\n                            message = result.message?.asText()\n                                ?: BitwardenString.generic_error_message.asText(),\n                        ),\n                    )\n                }\n            }\n        }\n    }\n}\n```\n\n---\n\n## Navigation Template (Type-Safe Routes)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt`\n\n```kotlin\n@file:OmitFromCoverage\n\npackage com.x8bit.bitwarden.ui.feature.example\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.navigation.NavController\nimport androidx.navigation.NavGraphBuilder\nimport androidx.navigation.NavOptions\nimport androidx.navigation.toRoute\nimport com.bitwarden.annotation.OmitFromCoverage\nimport com.bitwarden.ui.platform.base.util.composableWithSlideTransitions\nimport kotlinx.serialization.Serializable\n\n/**\n * Route for the Example screen.\n */\n@Serializable\n@OmitFromCoverage\ndata class ExampleRoute(\n    val itemId: String,\n    val isEditMode: Boolean = false,\n)\n\n/**\n * Args extracted from [SavedStateHandle] for the Example screen.\n */\n@OmitFromCoverage\ndata class ExampleArgs(\n    val itemId: String,\n    val isEditMode: Boolean,\n)\n\n/**\n * Extracts [ExampleArgs] from the [SavedStateHandle].\n */\nfun SavedStateHandle.toExampleArgs(): ExampleArgs {\n    val route = this.toRoute<ExampleRoute>()\n    return ExampleArgs(\n        itemId = route.itemId,\n        isEditMode = route.isEditMode,\n    )\n}\n\n/**\n * Navigate to the Example screen.\n */\nfun NavController.navigateToExample(\n    itemId: String,\n    isEditMode: Boolean = false,\n    navOptions: NavOptions? = null,\n) {\n    this.navigate(\n        route = ExampleRoute(\n            itemId = itemId,\n            isEditMode = isEditMode,\n        ),\n        navOptions = navOptions,\n    )\n}\n\n/**\n * Add the Example screen destination to the navigation graph.\n */\nfun NavGraphBuilder.exampleDestination(\n    onNavigateBack: () -> Unit,\n) {\n    composableWithSlideTransitions<ExampleRoute> {\n        ExampleScreen(\n            onNavigateBack = onNavigateBack,\n        )\n    }\n}\n```\n\n---\n\n## Screen/Compose Template\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt`\n\n```kotlin\npackage com.x8bit.bitwarden.ui.feature.example\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.bitwarden.ui.platform.base.util.EventsEffect\nimport com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar\nimport com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold\nimport com.bitwarden.ui.platform.components.util.rememberVectorPainter\nimport com.bitwarden.ui.platform.resource.BitwardenDrawable\nimport com.bitwarden.ui.platform.resource.BitwardenString\n\n/**\n * The Example screen.\n */\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ExampleScreen(\n    onNavigateBack: () -> Unit,\n    viewModel: ExampleViewModel = hiltViewModel(),\n) {\n    val state by viewModel.stateFlow.collectAsStateWithLifecycle()\n\n    EventsEffect(viewModel = viewModel) { event ->\n        when (event) {\n            ExampleEvent.NavigateBack -> onNavigateBack()\n            is ExampleEvent.ShowToast -> {\n                // Handle toast\n            }\n        }\n    }\n\n    // Dialogs\n    ExampleDialogs(\n        dialogState = state.dialogState,\n        onDismissRequest = { viewModel.trySendAction(ExampleAction.ErrorDialogDismiss) },\n    )\n\n    val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())\n    BitwardenScaffold(\n        modifier = Modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            BitwardenTopAppBar(\n                title = stringResource(id = BitwardenString.example),\n                scrollBehavior = scrollBehavior,\n                navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),\n                onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },\n            )\n        },\n    ) {\n        ExampleScreenContent(\n            state = state,\n            onInputChanged = { viewModel.trySendAction(ExampleAction.InputChanged(it)) },\n            onSubmitClick = { viewModel.trySendAction(ExampleAction.SubmitClick) },\n            modifier = Modifier\n                .fillMaxSize(),\n        )\n    }\n}\n```\n\n---\n\n## Data Layer Template (Repository + Hilt Module)\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/di/GeneratorRepositoryModule.kt`\n\n### Interface\n\n```kotlin\n/**\n * Provides data operations for the Example feature.\n */\ninterface ExampleRepository {\n    /**\n     * Submits data and returns a typed result.\n     */\n    suspend fun submitData(input: String): ExampleResult\n\n    /**\n     * Continuously observed data stream.\n     */\n    val dataFlow: StateFlow<DataState<ExampleData>>\n}\n```\n\n### Sealed Result Class\n\n```kotlin\n/**\n * Domain-specific result for Example operations.\n */\nsealed class ExampleResult {\n    data class Success(val data: String) : ExampleResult()\n    data class Error(val message: String?) : ExampleResult()\n}\n```\n\n### Implementation\n\n```kotlin\n/**\n * Default implementation of [ExampleRepository].\n */\nclass ExampleRepositoryImpl(\n    private val exampleDiskSource: ExampleDiskSource,\n    private val exampleService: ExampleService,\n    private val dispatcherManager: DispatcherManager,\n) : ExampleRepository {\n\n    override val dataFlow: StateFlow<DataState<ExampleData>>\n        get() = // ...\n\n    override suspend fun submitData(input: String): ExampleResult {\n        return exampleService\n            .postData(input)\n            .fold(\n                onSuccess = { ExampleResult.Success(it.toModel()) },\n                onFailure = { ExampleResult.Error(it.message) },\n            )\n    }\n}\n```\n\n### Hilt Module\n\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleRepositoryModule {\n\n    @Provides\n    @Singleton\n    fun provideExampleRepository(\n        exampleDiskSource: ExampleDiskSource,\n        exampleService: ExampleService,\n        dispatcherManager: DispatcherManager,\n    ): ExampleRepository = ExampleRepositoryImpl(\n        exampleDiskSource = exampleDiskSource,\n        exampleService = exampleService,\n        dispatcherManager = dispatcherManager,\n    )\n}\n```\n\n---\n\n## Security Templates\n\n**Based on**: `app/src/main/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/di/AuthDiskModule.kt` and `AuthDiskSourceImpl.kt`\n\n### Encrypted Disk Source (Module)\n\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nobject ExampleDiskModule {\n\n    @Provides\n    @Singleton\n    fun provideExampleDiskSource(\n        @EncryptedPreferences encryptedSharedPreferences: SharedPreferences,\n        @UnencryptedPreferences sharedPreferences: SharedPreferences,\n        json: Json,\n    ): ExampleDiskSource = ExampleDiskSourceImpl(\n        encryptedSharedPreferences = encryptedSharedPreferences,\n        sharedPreferences = sharedPreferences,\n        json = json,\n    )\n}\n```\n\n### Encrypted Disk Source (Implementation)\n\n```kotlin\n/**\n * Disk source for Example data using encrypted and unencrypted storage.\n */\nclass ExampleDiskSourceImpl(\n    encryptedSharedPreferences: SharedPreferences,\n    sharedPreferences: SharedPreferences,\n    private val json: Json,\n) : BaseEncryptedDiskSource(\n    encryptedSharedPreferences = encryptedSharedPreferences,\n    sharedPreferences = sharedPreferences,\n),\n    ExampleDiskSource {\n\n    private companion object {\n        const val ENCRYPTED_TOKEN_KEY = \"exampleToken\"\n        const val UNENCRYPTED_PREF_KEY = \"examplePreference\"\n    }\n\n    override var authToken: String?\n        get() = getEncryptedString(ENCRYPTED_TOKEN_KEY)\n        set(value) { putEncryptedString(ENCRYPTED_TOKEN_KEY, value) }\n\n    override var uiPreference: Boolean\n        get() = getBoolean(UNENCRYPTED_PREF_KEY) ?: false\n        set(value) { putBoolean(UNENCRYPTED_PREF_KEY, value) }\n}\n```\n\n---\n\n## Testing Templates\n\n**Based on**: `app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n### ViewModel Test\n\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n\n    // Mock dependencies\n    private val mockRepository = mockk<ExampleRepository>()\n    private val mutableDataFlow = MutableStateFlow<DataState<ExampleData>>(DataState.Loading)\n\n    @BeforeEach\n    fun setup() {\n        every { mockRepository.dataFlow } returns mutableDataFlow\n    }\n\n    @Test\n    fun `initial state should be correct when there is no saved state`() {\n        val viewModel = createViewModel(state = null)\n        assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)\n    }\n\n    @Test\n    fun `initial state should be correct when there is a saved state`() {\n        val savedState = DEFAULT_STATE.copy(data = \"saved\")\n        val viewModel = createViewModel(state = savedState)\n        assertEquals(savedState, viewModel.stateFlow.value)\n    }\n\n    @Test\n    fun `SubmitClick should call repository and update state on success`() = runTest {\n        val expected = ExampleResult.Success(data = \"result\")\n        coEvery { mockRepository.submitData(any()) } returns expected\n\n        val viewModel = createViewModel()\n        viewModel.stateFlow.test {\n            // Initial state\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n\n            // Updated state after result\n            assertEquals(\n                DEFAULT_STATE.copy(data = \"result\", isLoading = false),\n                awaitItem(),\n            )\n        }\n    }\n\n    @Test\n    fun `SubmitClick should show error dialog on failure`() = runTest {\n        val expected = ExampleResult.Error(message = \"Network error\")\n        coEvery { mockRepository.submitData(any()) } returns expected\n\n        val viewModel = createViewModel()\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n\n            val errorState = awaitItem()\n            assertTrue(errorState.dialogState is ExampleState.DialogState.Error)\n        }\n    }\n\n    @Test\n    fun `BackClick should emit NavigateBack event`() = runTest {\n        val viewModel = createViewModel()\n        viewModel.eventFlow.test {\n            viewModel.trySendAction(ExampleAction.BackClick)\n            assertEquals(ExampleEvent.NavigateBack, awaitItem())\n        }\n    }\n\n    // Helper to create ViewModel with optional saved state\n    private fun createViewModel(\n        state: ExampleState? = DEFAULT_STATE,\n    ): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle(\n            mapOf(KEY_STATE to state),\n        ),\n        exampleRepository = mockRepository,\n    )\n\n    companion object {\n        private val DEFAULT_STATE = ExampleState(\n            isLoading = false,\n            data = null,\n        )\n    }\n}\n```\n\n### Flow Testing with stateEventFlow\n\n```kotlin\n@Test\nfun `SubmitClick should update state and emit event`() = runTest {\n    coEvery { mockRepository.submitData(any()) } returns ExampleResult.Success(\"data\")\n\n    val viewModel = createViewModel()\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        viewModel.trySendAction(ExampleAction.SubmitClick)\n\n        // Assert state change\n        assertEquals(\n            DEFAULT_STATE.copy(data = \"data\"),\n            stateFlow.awaitItem(),\n        )\n\n        // Assert event emission\n        assertEquals(\n            ExampleEvent.ShowToast(\"Success\".asText()),\n            eventFlow.awaitItem(),\n        )\n    }\n}\n```\n\n<!-- File: .claude/skills/interacting-with-android-device/SKILL.md (Tokens: ~1889 | Category: anthropic-skill) -->\n---\nname: interacting-with-android-device\ndescription: Instructions for capturing UI state, comparing with mocks, and interacting with an Android device using MCP tools backed by ADB.\nallowed-tools: mcp__android-device__capture, mcp__android-device__find_element, mcp__android-device__tap_at, mcp__android-device__tap_element, mcp__android-device__navigate, mcp__android-device__input_text, Bash(adb:*), Bash(sleep:*), Bash(./gradlew install*:*), Read, Glob\n---\n\n# Interacting with Android Device\n\n## Quick Start: MCP Tools\n\nThe `android-device` MCP server provides 6 tools for device interaction. These replace the previous shell scripts with proper XML parsing, structured dumpsys parsing, and native obstruction detection.\n\n**Available tools:**\n- `capture` — Capture UI hierarchy XML and/or screenshot. Params: `{ xml?: boolean, screenshot?: boolean }`. Default: both.\n- `find_element` — Find element by `text` or `content-desc`, return coordinates with **obstruction detection**. Params: `{ text: string }`. Returns JSON with coordinates, bounds, and obstruction status.\n- `tap_at` — Tap at specific coordinates, wait, capture screenshot. Params: `{ x, y, waitSeconds? }`.\n- `tap_element` — Find, tap, and capture in one call (recommended). Params: `{ text, waitSeconds? }`. Auto-adjusts coordinates when obstructed.\n- `navigate` — Navigation actions: home, back, app-drawer. Params: `{ action, waitSeconds? }`. Captures screenshot after action.\n- `input_text` — Type text into the focused field. Params: `{ text, clear? }`. Set `clear: true` to erase existing content first.\n\n**Use these MCP tools instead of raw ADB commands** to save tokens, get structured results, and benefit from automatic obstruction detection.\n\n## 1. Capturing Current State\nTo understand what is currently on the device, use the `capture` tool:\n*   It saves `view.xml` (UI hierarchy) and `screen.png` (screenshot) to the working directory\n*   Read `view.xml` to find coordinates (`bounds`) and properties (like `text` or `resource-id`) of UI elements\n*   Use `screen.png` for visual verification against design mocks\n\n## 2. Interacting with the Device\n\n### Using MCP Tools (Recommended)\n\n*   **Find and tap an element by text** — use `tap_element`:\n    This finds the element, detects obstructions, taps (with adjusted coordinates if needed), and captures a screenshot — all in one call.\n\n*   **Tap at specific coordinates** — use `tap_at`:\n    When you already have coordinates from `find_element` or manual inspection.\n\n*   **Navigate (home, back, app-drawer)** — use `navigate`:\n    Performs the action and captures a screenshot.\n\n*   **Find element without tapping** — use `find_element`:\n    Returns coordinates and full element info. Useful when you need to inspect before acting.\n\n*   **Type text into a field** — use `input_text`:\n    Types text into the currently focused field. Set `clear: true` to erase existing content first. Tap the field before calling this if it isn't already focused.\n\n### Raw ADB Commands (When MCP Tools Aren't Sufficient)\n*   **Key Events**:\n    *   Back: `adb shell input keyevent 4`\n    *   Home: `adb shell input keyevent 3`\n    *   Enter: `adb shell input keyevent 66`\n*   **Scrolling/Swiping**: Use `adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms>` where:\n    *   `(x1, y1)` = starting point\n    *   `(x2, y2)` = ending point\n    *   `duration_ms` = duration in milliseconds (1000ms is typical; adjust for speed/distance)\n    *   **Note**: For expanding containers/drawers, use large distances (e.g., 2400->300 for a 2992px tall screen)\n\n## 3. Obstruction Detection\n\nThe `find_element` and `tap_element` tools automatically detect when another element would intercept the tap. This catches:\n*   **System overlays** (Layer 1): TalkBack floating menu, PiP windows, accessibility services — detected via `dumpsys window windows` touchable regions\n*   **In-app elements** (Layer 2): FABs, dialogs, bottom sheets, snackbars — detected by finding the topmost clickable element at the tap point in the UI hierarchy\n\nWhen obstruction is detected:\n*   Coordinates are **auto-adjusted** to the center of the largest unobstructed strip (top/bottom/left/right of the obstructor)\n*   The response includes the obstructor identity, bounds, and visible region info\n*   If fully obscured (no visible region), the original center is returned as best-effort\n*   **Compose parent wrapper** pattern (identical bounds) is recognized as non-obstruction\n\n## 4. Verification Workflow\nFollow these steps for a complete UI test:\n1.  **Build and Install**: Ensure the latest version of the app is running: `./gradlew installDebug`.\n2.  **Inspect**: Use `capture` to dump the UI hierarchy and take a screenshot.\n3.  **Compare**: Check the current UI against any mock image files in the project.\n4.  **Interact**: Use `tap_element` to tap a UI element by text. The tool handles coordinate calculation and obstruction detection automatically.\n5.  **Verify**: Use `capture` again to confirm the UI has updated as expected (e.g., a new screen is shown, or a success message appeared).\n\n## 5. Examples\n\n### Example: Navigate to Settings and Check for Updates\n```\n# Go to home screen\nnavigate({ action: \"home\" })\n\n# Open app drawer\nnavigate({ action: \"app-drawer\" })\n\n# Find and tap through settings\ntap_element({ text: \"Settings\", waitSeconds: 2 })\ntap_element({ text: \"System\", waitSeconds: 2 })\ntap_element({ text: \"Software updates\", waitSeconds: 2 })\ntap_element({ text: \"Check for update\", waitSeconds: 5 })\n```\n\n### Example: Swiping\nFor swipe gestures not covered by the navigate tool, use raw ADB:\n```bash\nadb shell input swipe 672 2800 672 500 1000 && sleep 1 && adb shell screencap -p /sdcard/screen.png && adb pull /sdcard/screen.png .\n```\n\n## 6. Best Practices\n\n### Coordinate Calculation\n*   Prefer `find_element` or `tap_element` over manual coordinate calculation — they handle bounds parsing, center computation, and obstruction detection automatically\n*   When multiple instances of an element exist (e.g., in prediction row and full list), check the `find_element` response to verify you're targeting the correct one\n\n### Navigation and State Evaluation\n*   **Verify after each interaction**: Don't assume an action succeeded — use `capture` after interactions to confirm the UI changed as expected\n*   **Check both visual and structural state**: Use screenshot for visual verification, XML dump for structural confirmation (element presence, text content, state changes)\n*   **Identify navigation failures early**: If a tap opened the wrong screen, use `navigate({ action: \"back\" })` to recover immediately\n\n### Interaction Patterns\n*   **Scrolling before interaction**: When looking for an element, check if it's visible on screen first. If not, scroll using swipe gestures to reveal it\n*   **Use consistent scroll direction**: For vertical scrolling in lists/settings, use downward swipes (higher Y -> lower Y) to scroll down\n*   **Handle app crashes gracefully**: Don't retry the same action — use back button and try an alternative approach\n*   **Check Accessibility**: Use the `content-desc` and `text` properties in the UI hierarchy to ensure the UI is accessible for screen readers\n\n## 7. Troubleshooting\n\n### Device Not Connected\nIf tools report ADB errors:\n*   Check USB connection or emulator status\n*   Enable USB debugging on the device (Settings > Developer Options > USB Debugging)\n*   Accept the RSA key prompt on the device if asked\n*   Restart the device or disconnect/reconnect the USB cable\n*   Run `adb devices` to verify the device is visible\n\n### MCP Server Not Available\nIf tools are not listed in `/mcp`:\n*   Ensure Node.js 18+ is installed\n*   The server auto-builds on first use via `.mcp.json` at the project root\n*   Check `.claude/mcp/android-device-server/` exists with `package.json`\n*   Try manual build: `cd .claude/mcp/android-device-server && npm install && npm run build`\n\n<!-- File: .claude/skills/planning-android-implementation/SKILL.md (Tokens: ~1543 | Category: anthropic-skill) -->\n---\nname: planning-android-implementation\nversion: 0.1.0\ndescription: Architecture design and phased implementation planning for Bitwarden Android. Use when planning implementation, designing architecture, creating file inventories, or breaking features into phases. Triggered by \"plan implementation\", \"architecture design\", \"implementation plan\", \"break this into phases\", \"what files do I need\", \"design the architecture\".\n---\n\n# Implementation Planning\n\nThis skill takes a refined specification (ideally from the `refining-android-requirements` skill) and produces a phased implementation plan with architecture design, file inventory, and risk assessment.\n\n**Prerequisite**: A clear set of requirements. If requirements are vague or incomplete, invoke the `refining-android-requirements` skill first.\n\n---\n\n## Step 1: Classify Change\n\nDetermine the change type to guide scope and planning depth:\n\n| Type | Description | Typical Scope |\n|------|-------------|---------------|\n| **New Feature** | Entirely new functionality, screens, or flows | New files + modifications, multi-phase |\n| **Enhancement** | Extending existing feature with new capabilities | Mostly modifications, 1-2 phases |\n| **Bug Fix** | Correcting incorrect behavior | Targeted modifications, single phase |\n| **Refactoring** | Restructuring without behavior change | Modifications only, migration-aware |\n| **Infrastructure** | Build, CI, tooling, or dependency changes | Config files, minimal code changes |\n\nState the classification and rationale before proceeding.\n\n---\n\n## Step 2: Codebase Exploration\n\nSearch the codebase to find reference implementations and integration points. Use the discovery commands from the `build-test-verify` skill as needed.\n\n### Find Pattern Anchors\n\nIdentify 2-3 existing files that serve as templates for the planned work:\n\n```\n**Pattern Anchors:**\n1. [file path] — [why this is a good reference]\n2. [file path] — [why this is a good reference]\n3. [file path] — [why this is a good reference]\n```\n\n### Map Integration Points\n\nIdentify files that must be modified to integrate the new work:\n\n- **Navigation**: Nav graph registrations, route definitions\n- **Dependency Injection**: Hilt modules, `@Provides` / `@Binds` functions\n- **Data Layer**: Repository interfaces, data source interfaces, Room DAOs\n- **API Layer**: Retrofit service interfaces, request/response models\n- **Feature Flags**: Feature flag definitions and checks\n- **Managers**: Single-responsibility data layer classes (see `docs/ARCHITECTURE.md` Managers section)\n- **Test Fixtures**: Shared test utilities in `src/testFixtures/` directories\n- **Product Flavor Source Sets**: Code in `src/standard/` vs `src/main/` for Play Services dependencies\n\n### Document Existing Patterns\n\nNote the specific patterns used by the pattern anchors:\n- State class structure (sealed class, data class fields)\n- Action/Event naming conventions\n- Repository method signatures and return types\n- Test structure and assertion patterns\n\n---\n\n## Step 3: Architecture Design\n\nProduce an ASCII diagram showing component relationships for the planned work:\n\n```\n┌─────────────────┐\n│   Screen        │ ← Compose UI\n│  (Composable)   │\n└────────┬────────┘\n         │ State / Action / Event\n┌────────▼────────┐\n│   ViewModel     │ ← Business logic orchestration\n└────────┬────────┘\n         │ Repository calls\n┌────────▼────────┐\n│   Repository    │ ← Data coordination (sealed class results)\n└───┬────┬────┬───┘\n    │    │    │\n┌───▼───┐ │ ┌─▼──────┐\n│Manager│ │ │Manager │ ← Single-responsibility (optional)\n└───┬───┘ │ └─┬──────┘\n    │     │   │\n┌───▼─────▼───▼────┐\n│   Data Sources   │ ← Raw data (Result<T>, never throw)\n└─┬────┬────┬──────┘\n  │    │    │\n Room Retrofit SDK\n```\n\nAdapt the diagram to show the actual components planned. _Consult `docs/ARCHITECTURE.md` for full data layer patterns and conventions._\n\n### Design Decisions\n\nDocument key architectural decisions in a table:\n\n| Decision | Resolution | Rationale |\n|----------|-----------|-----------|\n| [What needed deciding] | [What was chosen] | [Why] |\n\n---\n\n## Step 4: File Inventory\n\n### Files to Create\n\n| File Path | Type | Pattern Reference |\n|-----------|------|-------------------|\n| [full path] | [ViewModel / Screen / Repository / etc.] | [pattern anchor file] |\n\n**Include in file inventory:**\n- `...Navigation.kt` files for new screens\n- `...Module.kt` Hilt module files for new DI bindings\n- Paired test files (`...Test.kt`) for each new class\n\n### Files to Modify\n\n| File Path | Change Description | Risk Level |\n|-----------|-------------------|------------|\n| [full path] | [what changes] | Low / Medium / High |\n\n**Risk levels:**\n- **Low**: Additive changes (new entries in nav graph, new bindings in Hilt module)\n- **Medium**: Modifying existing logic (adding parameters, new branches)\n- **High**: Changing interfaces, data models, or shared utilities\n\n---\n\n## Step 5: Implementation Phases\n\nBreak the work into sequential phases. Each phase should be independently testable and committable.\n\n**Phase ordering principle**: Foundation → SDK/Data → Network → UI (tests accompany each phase)\n\nFor each phase:\n\n```markdown\n### Phase N: [Name]\n\n**Goal**: [What this phase accomplishes]\n\n**Files**:\n- Create: [list]\n- Modify: [list]\n\n**Tasks**:\n1. [Specific implementation task]\n2. [Specific implementation task]\n3. ...\n\n**Verification**:\n- [Test command or manual verification step]\n\n**Skills**: [Which workflow skills apply — e.g., `implementing-android-code`, `testing-android-code`]\n```\n\n### Phase Guidelines\n\n- Each phase should be small enough to be independently testable and committable\n- Tests are written within the same phase as the code they verify (not deferred to a \"testing phase\")\n- UI phases come after their data dependencies are in place\n- If a phase has more than 5 tasks, consider splitting it\n\n---\n\n## Step 6: Risk & Verification\n\n### Risk Assessment\n\n| Risk | Likelihood | Impact | Mitigation |\n|------|-----------|--------|------------|\n| [What could go wrong] | Low/Med/High | Low/Med/High | [How to prevent or handle] |\n\n### Verification Plan\n\n**Automated Verification:**\n- Unit test commands (from `build-test-verify` skill)\n- Lint/detekt commands\n- Build verification\n\n**Manual Verification:**\n- [Specific manual test scenarios]\n- [Edge cases to manually verify]\n- Verify ViewModel state survives process death (test via `SavedStateHandle` persistence and `Don't keep activities` developer option)\n\n<!-- File: .claude/skills/refining-android-requirements/SKILL.md (Tokens: ~1728 | Category: anthropic-skill) -->\n---\nname: refining-android-requirements\nversion: 0.1.0\ndescription: Requirements gap analysis and structured specification for Bitwarden Android. Use when refining requirements, analyzing specs, identifying gaps, or producing structured specifications from tickets or descriptions. Triggered by \"refine requirements\", \"gap analysis\", \"spec review\", \"requirements analysis\", \"what's missing from this spec\", \"analyze this ticket\".\n---\n\n# Requirements Refinement\n\nThis skill takes raw requirements (from Jira tickets, Confluence pages, or free-text descriptions) and produces a structured, implementation-ready specification through systematic gap analysis.\n\n**Key principle**: This skill identifies gaps and produces specifications. It does NOT propose solutions or architecture — that is the responsibility of the `planning-android-implementation` skill.\n\n---\n\n## Step 1: Source Consolidation\n\nCombine all input sources into a single working document. For each requirement, note its source:\n\n```\n- [Source: PM-12345] User must be able to configure timeout\n- [Source: Confluence] Timeout range is 1-60 minutes\n- [Source: User] Default timeout should be 15 minutes\n```\n\nFlag any contradictions between sources for immediate resolution.\n\n---\n\n## Step 2: Gap Analysis\n\nEvaluate the consolidated requirements against the following 5-category rubric. For each category, check every item and note whether it is **covered**, **partially covered**, or **missing**.\n\n### A. Functional Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| User actions defined? | What specific user actions trigger this feature? |\n| All states covered? (empty, loading, error, success) | What should the user see in [empty/loading/error] state? |\n| Edge cases identified? | What happens when [boundary condition]? |\n| Cancellation/back navigation flows? | Can the user cancel mid-flow? What happens to partial data? |\n| Input validation rules? | What are the valid ranges/formats for [input]? |\n| Success/failure criteria? | How does the user know the operation succeeded or failed? |\n| Offline behavior? | What happens if this is attempted offline? |\n\n### B. Technical Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Module scope identified? (`:app`, `:authenticator`, shared) | Which module(s) does this feature belong to? |\n| SDK dependencies? | Does this require Bitwarden SDK operations? Which ones? |\n| Data storage approach? (Room, DataStore, in-memory) | Where is the data for this feature persisted? |\n| Network API endpoints? | Which API endpoints are involved? Are they existing or new? |\n| Process death handling? | What state needs to survive process death? |\n| Migration requirements? | Does existing data need migration? |\n| Feature flag needed? | Should this be behind a feature flag for staged rollout? |\n| Product flavors (standard vs fdroid)? | Does this feature depend on Google Play Services? Available on F-Droid? |\n| Data layer tier? | Does this need a new Manager (single-responsibility) or only Repository/DataSource? Consult `docs/ARCHITECTURE.md` Data Layer section. |\n| Streaming vs discrete data? | Is data continuously observed (`DataState<T>` + `StateFlow`) or a one-shot operation (custom sealed class)? See `docs/ARCHITECTURE.md` Repositories section. |\n\n### C. Security Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Data sensitivity classified? | What sensitivity level does this data have? (vault-level, account-level, non-sensitive) |\n| Storage encryption required? | Must this data be encrypted at rest? Via SDK or Android Keystore? |\n| Logout cleanup behavior? | What must be cleared when the user logs out? |\n| Auth-gating? | Does accessing this feature require active authentication? |\n| Input sanitization? | Are there URL or credential inputs that need validation? |\n| Sensitive data in ViewModel state? | Will passwords, tokens, or keys appear in state? Must use `@IgnoredOnParcel`. See `implementing-android-code` skill Section F. |\n| SDK crypto context isolation? | Does this use vault encryption? Must use `ScopedVaultSdkSource` for multi-account safety. See CLAUDE.md Security Rules. |\n\n### D. UX/UI Requirements\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| UI copy/strings defined? | What text should appear for [label/button/message]? |\n| Error messages specified? | What should the error message say when [failure case]? |\n| Loading states designed? | Should loading show a spinner, skeleton, or shimmer? |\n| Navigation flow clear? | Where does the user go after [action]? Back stack behavior? |\n| Accessibility considerations? | Are there content descriptions or focus order requirements? |\n| Toast/snackbar/dialog for feedback? | What feedback mechanism for [action result]? |\n\n### E. Cross-Cutting Concerns\n\n| Check | Question to Ask If Missing |\n|-------|---------------------------|\n| Multi-account behavior? | How does this behave with multiple accounts? Per-account or global? |\n| Backwards compatibility? | Does this affect existing users? Migration path? |\n| Feature flag strategy? | Is this behind a server-side or local feature flag? |\n| Analytics/logging? | Are there analytics events to track? |\n| Bitwarden Authenticator impact? | Does this affect the `:authenticator` module? |\n| F-Droid compatibility? | Does this degrade gracefully without Google Play Services (no push notifications, no Play Integrity)? |\n\n---\n\n## Step 3: Present Gaps\n\nOrganize all identified gaps into two categories:\n\n### Blocking Questions\n\nQuestions that **must** be answered before implementation can begin because they change the architecture, data model, or core flow.\n\nFormat each question as:\n\n```\n**G[N]** ([Category]) — [Question text]\n  Context: [Why this matters / what depends on the answer]\n```\n\n### Non-Blocking Questions\n\nQuestions that have **reasonable defaults** and can be resolved during implementation. Note the assumed default.\n\nFormat each question as:\n\n```\n**G[N]** ([Category]) — [Question text]\n  Default assumption: [What we'll assume if not answered]\n  Context: [Why this matters]\n```\n\n---\n\n## Step 4: Produce Specification\n\nAfter the user answers blocking questions (and optionally non-blocking ones), produce a structured specification:\n\n```markdown\n## Overview\n\n[1-2 paragraph summary of the feature, its purpose, and scope]\n\n## Functional Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| FR1 | [requirement] | [source] | [any notes] |\n| FR2 | ... | ... | ... |\n\n## Technical Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| TR1 | [requirement] | [source] | [any notes] |\n| TR2 | ... | ... | ... |\n\n## Security Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| SR1 | [requirement] | [source] | [any notes] |\n\n## UX Requirements\n\n| ID | Requirement | Source | Notes |\n|----|------------|--------|-------|\n| UX1 | [requirement] | [source] | [any notes] |\n\n## Open Items\n\nNon-blocking items with assumed defaults that may be revisited:\n\n| ID | Question | Assumed Default | Category |\n|----|----------|----------------|----------|\n| G[N] | [question] | [default] | [category] |\n\n## Source Documentation\n\n| Source | Type | Link |\n|--------|------|------|\n| [name] | Jira / Confluence / User-provided | [link if available] |\n```\n\n### Output Guidelines\n\n- Requirements use numbered IDs (FR1, TR1, SR1, UX1) for traceability through implementation\n- Each requirement cites its source (ticket, page, or user-provided)\n- Technical requirements use table format for structured key/value data\n- Interface signatures are included as fenced code blocks when applicable\n- Open items preserve the gap ID (G[N]) for cross-referencing\n\n<!-- File: .claude/skills/reviewing-changes/checklists/bug-fix.md (Tokens: ~865 | Category: anthropic-skill) -->\n# Bug Fix Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Bug\n\n**1. Understand root cause:**\n- What was the broken behavior?\n- What caused it?\n- How does this fix address the root cause?\n\n**2. Assess scope:**\n- How many files changed?\n- Is this a targeted fix or broader refactoring?\n- Does this affect multiple features?\n\n**3. Check for side effects:**\n- Could this break other features?\n- Are there edge cases not considered?\n\n### Second Pass: Verify the Fix\n\n**4. Code changes:**\n- Does the fix make sense?\n- Is it the simplest solution?\n- Any unnecessary changes included?\n\n**5. Testing:**\n- Is there a regression test?\n- Does test verify the bug is fixed?\n- Are edge cases covered?\n\n**6. Related code:**\n- Same pattern in other places that might have same bug?\n- Should other similar code be fixed too?\n\n## What to CHECK\n\n✅ **Root Cause Analysis**\n- Does the fix address the root cause or just symptoms?\n- Is the explanation in PR/commit clear?\n\n✅ **Regression Testing**\n- Is there a new test that would fail without this fix?\n- Does test cover the reported bug scenario?\n- Are related edge cases tested?\n\n✅ **Side Effects**\n- Could this break existing functionality?\n- Are there similar code paths that need checking?\n- Does this change behavior in unexpected ways?\n\n✅ **Fix Scope**\n- Is the fix appropriately scoped (not too broad, not too narrow)?\n- Are all instances of the bug fixed?\n- Any related bugs discovered during investigation?\n\n## What to SKIP\n\n❌ **Full Architecture Review** - Unless fix reveals architectural problems\n❌ **Comprehensive Testing Review** - Focus on regression tests, not entire test suite\n❌ **Major Refactoring Suggestions** - Unless directly related to preventing similar bugs\n\n## Red Flags\n\n🚩 **No test for the bug** - How will we prevent regression?\n🚩 **Fix doesn't match root cause** - Is this fixing symptoms?\n🚩 **Broad changes beyond the bug** - Should this be split into separate PRs?\n🚩 **Similar patterns elsewhere** - Should those be fixed too?\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"Can we add a test that would fail without this fix?\"\n- \"I see this pattern in [other file] - does it have the same issue?\"\n- \"Is this fixing the root cause or masking the symptom?\"\n- \"Could this change affect [related feature]?\"\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n**Overall Assessment:** APPROVE\n\nSee inline comments for suggested improvements.\n```\n\n**Inline comment examples:**\n\n```\n**data/auth/BiometricRepository.kt:120** - SUGGESTED: Extract null handling\n\n<details>\n<summary>Details</summary>\n\nRoot cause analysis: BiometricPrompt result was nullable but code assumed non-null, causing crash on cancellation (PM-12345).\n\nConsider extracting null handling pattern:\n\n\\```kotlin\nprivate fun handleBiometricResult(result: BiometricPrompt.AuthenticationResult?): AuthResult {\n    return result?.let { AuthResult.Success(it) } ?: AuthResult.Cancelled\n}\n\\```\n\nThis pattern could be reused if we add other biometric auth points.\n</details>\n```\n\n```\n**app/auth/BiometricViewModel.kt:89** - SUGGESTED: Add regression test\n\n<details>\n<summary>Details</summary>\n\nAdd test for cancellation scenario to prevent regression:\n\n\\```kotlin\n@Test\nfun `when biometric cancelled then returns cancelled state`() = runTest {\n    coEvery { repository.authenticate() } returns Result.failure(CancelledException())\n    viewModel.onBiometricAuth()\n    assertEquals(AuthState.Cancelled, viewModel.state.value)\n}\n\\```\n\nThis prevents regression of the bug just fixed.\n</details>\n```\n\n<!-- File: .claude/skills/reviewing-changes/checklists/dependency-update.md (Tokens: ~981 | Category: anthropic-skill) -->\n# Dependency Update Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Identify and Assess\n\n**1. Identify the change:**\n- Which library? Old version → New version?\n- Major (X.0.0), Minor (0.X.0), or Patch (0.0.X) version change?\n- Single dependency or multiple?\n\n**2. Check compilation safety:**\n- Any imports in codebase that might break?\n- Any deprecated APIs we're currently using?\n- Check if this is a breaking change version\n\n### Second Pass: Deep Analysis\n\n**3. Review release notes** (if available):\n- Breaking changes mentioned?\n- Security fixes included?\n- New features we should know about?\n- Deprecations that affect our usage?\n\n**4. Verify consistency:**\n- If updating androidx library, are related libraries updated consistently?\n- BOM (Bill of Materials) consistency if applicable?\n- Test dependencies updated alongside main dependencies?\n\n## What to CHECK\n\n✅ **Compilation Safety**\n- Look for API deprecations in our codebase\n- Check if import statements still valid\n- Major version bumps require extra scrutiny\n- Beta/alpha versions need stability assessment\n\n✅ **Security Implications** (if applicable)\n- Security-related libraries (crypto, auth, networking)?\n- Check for CVEs addressed in release notes\n- Review security advisories for this library\n\n✅ **Testing Implications**\n- Does this affect test utilities?\n- Are there breaking changes in test APIs?\n- Do existing tests still cover the same scenarios?\n\n✅ **Changelog Review**\n- Read release notes for breaking changes\n- Note any behavioral changes\n- Check migration guides if major version\n\n## What to SKIP\n\n❌ **Full Architecture Review** - No code changed, patterns unchanged\n❌ **Code Style Review** - No code to review\n❌ **New Test Requirements** - Unless API changed significantly\n❌ **Security Deep-Dive** - Unless crypto/auth/networking library\n❌ **Performance Analysis** - Unless release notes mention performance changes\n\n## Red Flags (Escalate to Full Review)\n\n🚩 **Major version bump** (e.g., 1.x → 2.0) - Read `checklists/feature-addition.md`\n🚩 **Security/crypto library** - Read `reference/architectural-patterns.md` and `docs/ARCHITECTURE.md#security`\n🚩 **Breaking changes in release notes** - Read relevant code sections carefully\n🚩 **Multiple dependency updates at once** - Check for interaction risks\n🚩 **Beta/Alpha versions** - Assess stability concerns and rollback plan\n\nIf any red flags present, escalate to more comprehensive review using appropriate checklist.\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Reviews\n\n### Example 1: Simple Patch Version (No Critical Issues)\n\n```markdown\n**Overall Assessment:** APPROVE\n\nSee inline comments for all issue details.\n```\n\n**Inline comment example:**\n```\n**libs.versions.toml:45** - SUGGESTED: Beta version in production\n\n<details>\n<summary>Details</summary>\n\nandroidx.credentials updated from 1.5.0 to 1.6.0-beta03\n\nMonitor for stability issues - beta releases may have unexpected behavior in production.\n\nChangelog: Adds support for additional credential types, internal bug fixes.\n</details>\n```\n\n### Example 2: Major Version with Breaking Changes (With Critical Issues)\n\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Breaking API changes in Retrofit 3.0.0 (network/api/BitwardenApiService.kt)\n- Breaking API changes in Retrofit 3.0.0 (network/api/VaultApiService.kt)\n\nSee inline comments for migration details.\n```\n\n**Inline comment example:**\n```\n**network/api/BitwardenApiService.kt:15** - CRITICAL: Breaking API changes\n\n<details>\n<summary>Details and fix</summary>\n\nRetrofit 3.0.0 removes `Call<T>` return type. Migration required:\n\n\\```kotlin\n// Before\nfun getUser(): Call<UserResponse>\n\n// After\nsuspend fun getUser(): Response<UserResponse>\n\\```\n\nUpdate all API service interfaces to use suspend functions, update call sites to use coroutines instead of enqueue/execute, and update tests accordingly.\n\nConsider creating a separate PR for this migration due to scope.\n\nReference: https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-300\n</details>\n```\n\n<!-- File: .claude/skills/reviewing-changes/checklists/feature-addition.md (Tokens: ~1509 | Category: anthropic-skill) -->\n# Feature Addition Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: High-Level Assessment\n\n**1. Understand the feature:**\n- Read PR description - what problem does this solve?\n- Identify user-facing changes vs internal changes\n- Note any security implications (auth, encryption, data handling)\n\n**2. Scan file structure:**\n- Which modules affected? (app, data, network, ui, core?)\n- Are files organized correctly per module structure?\n- Any new public APIs introduced?\n\n**3. Initial risk assessment:**\n- Does this touch sensitive data or security-critical paths?\n- Does this affect existing features or only add new ones?\n- Are there obvious compilation or null safety issues?\n\n### Second Pass: Architecture Deep-Dive\n\n**4. MVVM + UDF Pattern Compliance:**\n- ViewModels properly structured?\n- State management using StateFlow?\n- Business logic in correct layer?\n\n**5. Dependency Injection:**\n- Hilt DI used correctly?\n- Dependencies injected, not manually instantiated?\n- Proper scoping applied?\n\n**6. Module Organization:**\n- Code placed in correct modules?\n- No circular dependencies introduced?\n- Proper separation of concerns?\n\n**7. Error Handling:**\n- Using Result types, not exception-based handling?\n- Errors propagated correctly through layers?\n\n### Third Pass: Details and Quality\n\n**8. Testing:**\n- Unit tests for ViewModels and repositories?\n- Test coverage for edge cases and error scenarios?\n- Tests verify behavior, not implementation?\n\n**9. Code Quality:**\n- Null safety handled properly?\n- Public APIs have KDoc documentation?\n- Naming follows project conventions?\n\n**10. Security:**\n- Sensitive data encrypted properly?\n- Authentication/authorization handled correctly?\n- Zero-knowledge architecture preserved?\n\n## Architecture Review\n\nRead `reference/architectural-patterns.md` for full patterns and code examples.\n\n**Check these four areas:**\n- **MVVM/UDF**: ViewModel exposes `StateFlow` (not `MutableStateFlow`), business logic in Repository, UI is stateless\n- **Hilt DI**: `@HiltViewModel` + `@Inject constructor`, inject interfaces not implementations, no manual instantiation\n- **Module placement**: UI in `:ui`/`:app`, data in `:data`, network in `:network`, no circular dependencies\n- **Error handling**: `Result<T>` / `runCatching` throughout — no thrown exceptions from data layer\n\n## Security Review\n\nReference: `docs/ARCHITECTURE.md#security`\n\n**Critical Security Checks:**\n\n- **Sensitive data encrypted**: Passwords, keys, tokens use Android Keystore or EncryptedSharedPreferences\n- **No plaintext secrets**: No passwords/keys in logs, memory dumps, or SharedPreferences\n- **Input validation**: All user-provided data validated and sanitized\n- **Authentication tokens**: Securely stored and transmitted\n- **Zero-knowledge architecture**: Encryption happens client-side, server never sees plaintext\n\n**Red Flags:**\n```kotlin\n// ❌ CRITICAL - Plaintext storage\nsharedPreferences.edit {\n    putString(\"pin\", userPin)  // Must use EncryptedSharedPreferences\n}\n\n// ❌ CRITICAL - Logging sensitive data\nLog.d(\"Auth\", \"Password: $password\")  // Never log sensitive data\n\n// ❌ CRITICAL - Weak encryption\nval cipher = Cipher.getInstance(\"DES\")  // Use AES-256-GCM\n\n// ✅ GOOD - Keystore encryption\nval encryptedData = keystoreManager.encrypt(sensitiveData)\nsecureStorage.store(encryptedData)\n```\n\n**If security concerns found, classify as CRITICAL using `reference/priority-framework.md`**\n\n## Testing Review\n\nReference: `reference/testing-patterns.md`\n\n**Required Test Coverage:**\n\n- **ViewModels**: Unit tests for state transitions, actions, error scenarios\n- **Repositories**: Unit tests for data transformations, error handling\n- **Business logic**: Unit tests for complex algorithms, calculations\n- **Edge cases**: Null inputs, empty states, network failures, concurrent operations\n\n**Test Quality:**\n```kotlin\n// ✅ GOOD - Tests behavior\n@Test\nfun `when login succeeds then state updates to success`() = runTest {\n    val viewModel = LoginViewModel(mockRepository)\n\n    coEvery { mockRepository.login(any(), any()) } returns Result.success(User())\n\n    viewModel.onLoginClicked(\"user\", \"pass\")\n\n    viewModel.state.test {\n        assertEquals(LoginState.Success, awaitItem())\n    }\n}\n\n// ❌ BAD - Tests implementation\n@Test\nfun `repository is called with correct parameters`() {\n    // This is testing internal implementation, not behavior\n}\n```\n\n**Testing Frameworks:**\n- JUnit 5 for test structure\n- MockK for mocking\n- Turbine for Flow testing\n- Kotlinx-coroutines-test for coroutine testing\n\n## Code Quality\n\n### Null Safety\n\n- No `!!` (non-null assertion) without clear safety guarantee\n- Platform types (from Java) handled with explicit nullability\n- Nullable types have proper null checks or use safe operators (`?.`, `?:`)\n\n```kotlin\n// ❌ BAD - Unsafe assertion\nval result = apiService.getData()!!  // Could crash\n\n// ✅ GOOD - Safe handling\nval result = apiService.getData() ?: return State.Error(\"No data\")\n\n// ❌ BAD - Platform type unchecked\nval intent: Intent = getIntent()  // Could be null from Java\nintent.getStringExtra(\"key\")  // Potential NPE\n\n// ✅ GOOD - Explicit nullability\nval intent: Intent? = getIntent()\nintent?.getStringExtra(\"key\")\n```\n\n### Documentation\n\n- **Public APIs**: Have KDoc comments explaining purpose, parameters, return values\n- **Complex algorithms**: Explained in comments\n- **Non-obvious behavior**: Documented with rationale\n\n```kotlin\n// ✅ GOOD - Documented public API\n/**\n * Encrypts the given data using AES-256-GCM with a key from Android Keystore.\n *\n * @param plaintext The data to encrypt\n * @return Result containing encrypted data or encryption error\n */\nsuspend fun encrypt(plaintext: ByteArray): Result<EncryptedData>\n```\n\n### Style Compliance\n\nReference: `docs/STYLE_AND_BEST_PRACTICES.md`\n\nOnly flag style issues if:\n- Not caught by linters (Detekt, ktlint)\n- Have architectural implications\n- Significantly impact readability\n\nSkip minor formatting (spaces, line breaks, etc.) - linters handle this.\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Providing Feedback\n\nUse `reference/review-psychology.md` for phrasing guidance.\n\n**Key principles:**\n- **Ask questions** for design decisions: \"Can we use the existing BitwardenTextField component here?\"\n- **Be prescriptive** for clear violations: \"Change MutableStateFlow to StateFlow (MVVM pattern requirement)\"\n- **Explain rationale**: \"This exposes mutable state, violating unidirectional data flow\"\n- **Use I-statements**: \"It's hard for me to understand this logic without comments\"\n- **Avoid condescension**: Don't use \"just\", \"simply\", \"obviously\"\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n<!-- File: .claude/skills/reviewing-changes/checklists/infrastructure.md (Tokens: ~1351 | Category: anthropic-skill) -->\n# Infrastructure Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Change\n\n**1. Identify the goal:**\n- What problem does this solve?\n- Is this optimization, fix, or new capability?\n- What's the expected impact?\n\n**2. Assess risk:**\n- Does this affect production builds?\n- Could this break CI/CD pipelines?\n- Impact on developer workflow?\n\n**3. Performance implications:**\n- Will builds be faster or slower?\n- CI time impact?\n- Resource usage changes?\n\n### Second Pass: Verify Implementation\n\n**4. Configuration correctness:**\n- Syntax valid?\n- References correct?\n- Secrets/credentials handled securely?\n\n**5. Impact analysis:**\n- What workflows/builds are affected?\n- Rollback plan if this breaks?\n- Documentation for team?\n\n**6. Testing strategy:**\n- How can this be tested before merge?\n- Canary/gradual rollout possible?\n- Monitoring for issues post-merge?\n\n## What to CHECK\n\n✅ **Configuration Correctness**\n- YAML/Groovy syntax valid\n- File references correct\n- Version numbers/tags valid\n- Conditional logic sound\n\n✅ **Security**\n- No hardcoded secrets or credentials\n- GitHub secrets used properly\n- Permissions appropriately scoped\n- No sensitive data in logs\n\n✅ **Performance Impact**\n- Build time implications understood\n- CI queue time impact assessed\n- Resource usage reasonable\n\n✅ **Rollback Plan**\n- Can this be reverted easily?\n- Dependencies on other changes?\n- Gradual rollout possible?\n\n✅ **Documentation**\n- Changes documented for team?\n- README or CONTRIBUTING updated?\n- Breaking changes clearly noted?\n\n## What to SKIP\n\n❌ **Bikeshedding Configuration** - Unless clear performance/maintenance benefit\n❌ **Over-Optimization** - Unless current system has proven problems\n❌ **Suggesting Major Rewrites** - Unless current approach is fundamentally broken\n\n## Red Flags\n\n🚩 **Hardcoded secrets** - Use GitHub secrets or secure storage\n🚩 **No rollback plan** - Critical infrastructure should be revertible\n🚩 **Untested changes** - CI changes should be validated\n🚩 **Breaking changes without notice** - Team needs advance warning\n🚩 **Performance regression** - Builds shouldn't get significantly slower\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"What's the rollback plan if this breaks CI?\"\n- \"Can we test this on a feature branch before main?\"\n- \"Will this impact build times? By how much?\"\n- \"Should this be documented in CONTRIBUTING.md?\"\n\n## Common Infrastructure Patterns\n\n### GitHub Actions\n\n```yaml\n# ✅ GOOD - Secure, clear, tested\nname: Build and Test\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30  # Prevent runaway builds\n    steps:\n      - uses: actions/checkout@v4\n      - name: Run tests\n        env:\n          API_KEY: ${{ secrets.API_KEY }}  # Secure secret usage\n        run: ./gradlew test\n\n# ❌ BAD - Insecure, unclear\nname: Build\non: push  # Too broad, runs on all branches\njobs:\n  build:\n    runs-on: ubuntu-latest\n    # No timeout - could run forever\n    steps:\n      - run: |\n          export API_KEY=\"hardcoded_key_here\"  # Hardcoded secret!\n          ./gradlew test\n```\n\n### Gradle Configuration\n\n```kotlin\n// ✅ GOOD - Clear, maintainable\ndependencies {\n    implementation(libs.androidx.core.ktx)  // Version catalog\n    implementation(libs.hilt.android)\n\n    testImplementation(libs.junit5)\n    testImplementation(libs.mockk)\n}\n\n// ❌ BAD - Hardcoded versions\ndependencies {\n    implementation(\"androidx.core:core-ktx:1.12.0\")  // Hardcoded version\n    implementation(\"com.google.dagger:hilt-android:2.48\")\n}\n```\n\n### Build Optimization\n\n```kotlin\n// ✅ GOOD - Parallel, cached\ntasks.register(\"checkAll\") {\n    dependsOn(\"detekt\", \"ktlintCheck\", \"testStandardDebug\")\n    group = \"verification\"\n    description = \"Run all checks in parallel\"\n\n    // Enable caching for faster builds\n    outputs.upToDateWhen { false }\n}\n\n// ❌ BAD - Sequential, no caching\ntasks.register(\"checkAll\") {\n    doLast {\n        exec { commandLine(\"./gradlew\", \"detekt\") }\n        exec { commandLine(\"./gradlew\", \"ktlintCheck\") }  // Sequential\n        exec { commandLine(\"./gradlew\", \"test\") }\n    }\n}\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n## Summary\nOptimizes CI build by parallelizing test execution and caching dependencies\n\nImpact: Estimated 40% reduction in CI time (12 min → 7 min per build)\n\n## Critical Issues\nNone\n\n## Suggested Improvements\n\n**.github/workflows/build.yml:23** - Add timeout for safety\n```yaml\njobs:\n  build:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30  # Prevent builds from hanging\n    steps:\n      # ...\n```\nThis prevents runaway builds if something goes wrong.\n\n**.github/workflows/build.yml:45** - Consider matrix strategy for module tests\nCan we run module tests in parallel using a matrix strategy?\n```yaml\nstrategy:\n  matrix:\n    module: [app, data, network, ui]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - run: ./gradlew :${{ matrix.module }}:test\n```\nThis could further reduce CI time.\n\n**build.gradle.kts:12** - Document caching strategy\nCan we add a comment explaining the caching configuration?\nFuture maintainers will appreciate understanding why these specific cache keys are used.\n\n## Rollback Plan\nIf CI breaks:\n- Revert commit: `git revert [commit-hash]`\n- Previous workflow available at: `.github/workflows/build.yml@main^`\n- Monitor CI times at: https://github.com/[org]/[repo]/actions\n```\n\n<!-- File: .claude/skills/reviewing-changes/checklists/refactoring.md (Tokens: ~1575 | Category: anthropic-skill) -->\n# Refactoring Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Understand the Refactoring\n\n**1. Understand the goal:**\n- What pattern is being improved?\n- Why is this refactoring needed?\n- What's the scope of changes?\n\n**2. Assess completeness:**\n- Are all instances refactored or just some?\n- Are there related areas that should also change?\n- Is the migration complete or partial?\n\n**3. Risk assessment:**\n- Does this change behavior?\n- How many files affected?\n- Are tests updated to reflect changes?\n\n### Second Pass: Verify Consistency\n\n**4. Pattern consistency:**\n- Is the new pattern applied consistently throughout?\n- Are there missed instances of the old pattern?\n- Does this match established project patterns?\n\n**5. Migration completeness:**\n- Old pattern fully removed or deprecated?\n- All usages updated?\n- Documentation updated?\n\n**6. Test coverage:**\n- Do tests still pass?\n- Are tests refactored to match?\n- Does behavior remain unchanged?\n\n## What to CHECK\n\n✅ **Pattern Consistency**\n- New pattern applied consistently across all touched code\n- Follows established project patterns (MVVM, DI, error handling)\n- No mix of old and new patterns\n\n✅ **Migration Completeness**\n- All instances of old pattern updated?\n- Deprecated methods removed or marked @Deprecated?\n- Related code also updated (tests, docs)?\n\n✅ **Behavior Preservation**\n- Refactoring doesn't change behavior\n- Tests still pass\n- Edge cases still handled\n\n✅ **Deprecation Strategy** (if applicable)\n- Old APIs marked @Deprecated with migration guidance\n- Replacement clearly documented\n- Timeline for removal specified\n\n## What to SKIP\n\n❌ **Suggesting Additional Refactorings** - Unless directly related to current changes\n❌ **Scope Creep** - Don't request refactoring of untouched code\n❌ **Perfection** - Better code is better than perfect code\n\n## Red Flags\n\n🚩 **Incomplete migration** - Mix of old and new patterns\n🚩 **Behavior changes** - Refactoring shouldn't change behavior\n🚩 **Broken tests** - Tests should be updated to match refactoring\n🚩 **Undocumented pattern** - New pattern should be clear to team\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"I see the old pattern still used in [file:line] - should that be updated too?\"\n- \"Can we add @Deprecated to the old method with migration guidance?\"\n- \"How do we ensure this behavior remains the same?\"\n- \"Should this pattern be documented in ARCHITECTURE.md?\"\n\n## Common Refactoring Patterns\n\n### Extract Interface/Repository\n\n```kotlin\n// ✅ GOOD - Complete migration\ninterface FeatureRepository {\n    suspend fun getData(): Result<Data>\n}\n\nclass FeatureRepositoryImpl @Inject constructor(\n    private val apiService: FeatureApiService\n) : FeatureRepository {\n    override suspend fun getData(): Result<Data> = runCatching {\n        apiService.fetchData()\n    }\n}\n\n// All usages updated to inject interface\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository  // Interface\n) : ViewModel()\n\n// ❌ BAD - Incomplete migration\n// Some files still inject FeatureRepositoryImpl directly\n```\n\n### Modernize Error Handling\n\n```kotlin\n// ✅ GOOD - Complete migration\n// Old exception-based removed\nsuspend fun fetchData(): Result<Data> = runCatching {\n    apiService.getData()\n}\n\n// All call sites updated\nrepository.fetchData().fold(\n    onSuccess = { /* handle */ },\n    onFailure = { /* handle */ }\n)\n\n// ❌ BAD - Mixed patterns\n// Some functions use Result, others still throw exceptions\n```\n\n### Extract Reusable Component\n\n```kotlin\n// ✅ GOOD - Complete extraction\n// Component moved to :ui module\n@Composable\nfun BitwardenButton(\n    text: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier\n)\n\n// All usages updated to use new component\n// Old inline button implementations removed\n\n// ❌ BAD - Incomplete extraction\n// Some screens use new component, others still have inline implementation\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Reviews\n\n### Example 1: Refactoring with Incomplete Migration\n\n**Context**: Refactoring authentication to Repository pattern, but one ViewModel still uses old pattern\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Incomplete migration (app/vault/VaultViewModel.kt:89)\n\nSee inline comments for details.\n```\n\n**Inline Comment 1** (on `app/vault/VaultViewModel.kt:89`):\n```markdown\n**IMPORTANT**: Incomplete migration\n\n<details>\n<summary>Details and fix</summary>\n\nThis ViewModel still injects AuthManager directly. Should it use AuthRepository like the other 11 ViewModels?\n\n\\```kotlin\n// Current (old pattern)\nclass VaultViewModel @Inject constructor(\n    private val authManager: AuthManager\n)\n\n// Should be (new pattern)\nclass VaultViewModel @Inject constructor(\n    private val authRepository: AuthRepository\n)\n\\```\n\nThis is the only ViewModel still using the old pattern.\n</details>\n```\n\n**Inline Comment 2** (on `data/auth/AuthManager.kt:1`):\n```markdown\n**SUGGESTED**: Add deprecation notice\n\n<details>\n<summary>Details</summary>\n\nCan we add @Deprecated to AuthManager to guide future development?\n\n\\```kotlin\n@Deprecated(\n    message = \"Use AuthRepository interface instead\",\n    replaceWith = ReplaceWith(\"AuthRepository\"),\n    level = DeprecationLevel.WARNING\n)\nclass AuthManager @Inject constructor(...)\n\\```\n\nThis helps prevent new code from using the old pattern.\n</details>\n```\n\n---\n\n### Example 2: Clean Refactoring (No Issues)\n\n**Context**: Refactoring with complete migration, all patterns followed correctly, tests passing\n\n**Review Comment:**\n```markdown\n**Overall Assessment:** APPROVE\n\nClean refactoring moving ExitManager to :ui module. Follows established patterns, eliminates duplication, tests updated correctly.\n```\n\n**Token count:** ~30 tokens (vs ~800 for verbose format)\n\n**Why this works:**\n- 3 lines total\n- Clear approval decision\n- Briefly notes what was done\n- No elaborate sections, checkmarks, or excessive praise\n- Author gets immediate green light to merge\n\n**What NOT to do for clean refactorings:**\n```markdown\n❌ DO NOT create these sections:\n\n## Summary\nThis PR successfully refactors ExitManager into shared code...\n\n## Key Strengths\n- ✅ Follows established module organization patterns\n- ✅ Removes code duplication between apps\n- ✅ Improves test coverage\n- ✅ Maintains consistent behavior\n[...20 more checkmarks...]\n\n## Code Quality & Architecture\n**Architectural Compliance:** ✅\n- Correctly places manager in :ui module\n- Follows established pattern for UI-layer managers\n[...detailed analysis...]\n\n## Changes\n- ✅ Moved ExitManager interface from app → ui module\n- ✅ Moved ExitManagerImpl from app → ui module\n[...listing every file...]\n```\n\nThis is excessive. **For clean PRs: 2-3 lines maximum.**\n\n<!-- File: .claude/skills/reviewing-changes/checklists/ui-refinement.md (Tokens: ~1375 | Category: anthropic-skill) -->\n# UI Refinement Review Checklist\n\n## Multi-Pass Strategy\n\n### First Pass: Visual Changes\n\n**1. Understand the changes:**\n- What visual/UX problem is being solved?\n- Are there designs or screenshots to reference?\n- Is this a bug fix or enhancement?\n\n**2. Component usage:**\n- Using existing components from `:ui` module?\n- Any new custom components created?\n- Could existing components be reused?\n\n### Second Pass: Implementation Review\n\n**3. Compose best practices:**\n- Composables properly structured?\n- State hoisted correctly?\n- Preview composables included?\n\n**4. Accessibility:**\n- Content descriptions for images/icons?\n- Semantic properties for screen readers?\n- Touch targets meet minimum size (48dp)?\n\n**5. Design consistency:**\n- Using theme colors, spacing, typography?\n- Consistent with other screens?\n- Responsive to different screen sizes?\n\n## What to CHECK\n\n✅ **Compose Best Practices**\n- Composables are stateless where possible\n- State hoisting follows patterns\n- Side effects (LaunchedEffect, DisposableEffect) used correctly\n- Preview composables provided for development\n\n✅ **Component Reuse**\n- Using existing BitwardenButton, BitwardenTextField, etc.?\n- Could custom UI be replaced with existing components?\n- New reusable components placed in `:ui` module?\n\n✅ **Accessibility**\n- `contentDescription` for icons and images\n- `semantics` for custom interactions\n- Sufficient contrast ratios\n- Touch targets ≥ 48dp minimum\n\n✅ **Design Consistency**\n- Using `BitwardenTheme` colors (not hardcoded)\n- Using `BitwardenTheme` spacing (16.dp, 8.dp, etc.)\n- Using `BitwardenTheme` typography styles\n- Consistent with existing screen patterns\n\n✅ **Responsive Design**\n- Handles different screen sizes?\n- Scrollable content where appropriate?\n- Landscape orientation considered?\n\n## What to SKIP\n\n❌ **Deep Architecture Review** - Unless ViewModel changes are substantial\n❌ **Business Logic Review** - Focus is on presentation, not logic\n❌ **Security Review** - Unless UI exposes sensitive data improperly\n\n## Red Flags\n\n🚩 **Duplicating existing components** - Should reuse from `:ui` module\n🚩 **Hardcoded colors/dimensions** - Should use theme\n🚩 **Missing accessibility properties** - Critical for screen readers\n🚩 **State management in UI** - Should be hoisted to ViewModel\n\n## Key Questions to Ask\n\nUse `reference/review-psychology.md` for phrasing:\n\n- \"Can we use BitwardenButton here instead of this custom button?\"\n- \"Should this color come from BitwardenTheme instead of being hardcoded?\"\n- \"How will this look on a small screen?\"\n- \"Is there a contentDescription for this icon?\"\n\n## Common Patterns\n\n### Composable Structure\n\n```kotlin\n// ✅ GOOD - Stateless, hoisted state\n@Composable\nfun FeatureScreen(\n    state: FeatureState,\n    onActionClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    // UI rendering only\n}\n\n// ❌ BAD - Business state in composable\n@Composable\nfun FeatureScreen() {\n    var userData by remember { mutableStateOf<User?>(null) }  // Business state should be in ViewModel\n    var isLoading by remember { mutableStateOf(false) }  // App state should be in ViewModel\n    // ...\n}\n\n// ✅ OK - UI-local state in composable\n@Composable\nfun LoginForm(onSubmit: (String, String) -> Unit) {\n    var username by remember { mutableStateOf(\"\") }  // UI-local input state is fine\n    var password by remember { mutableStateOf(\"\") }\n    // Hoist only as high as needed\n}\n```\n\n### Theme Usage\n\n```kotlin\n// ✅ GOOD - Using theme\nText(\n    text = \"Title\",\n    style = BitwardenTheme.typography.titleLarge,\n    color = BitwardenTheme.colorScheme.primary\n)\n\n// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)\nSpacer(modifier = Modifier.height(16.dp))\n\n// ❌ BAD - Hardcoded\nText(\n    text = \"Title\",\n    style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold),  // Should use theme\n    color = Color(0xFF0000FF)  // Should use theme color\n)\n\nSpacer(modifier = Modifier.height(17.dp))  // Non-standard spacing\n```\n\n### Accessibility\n\n```kotlin\n// ✅ GOOD - Interactive element with description\nIcon(\n    painter = painterResource(R.drawable.ic_password),\n    contentDescription = \"Password visibility toggle\",\n    modifier = Modifier.clickable { onToggle() }\n)\n\n// ✅ GOOD - Decorative icon with explicit null\nIcon(\n    painter = painterResource(R.drawable.ic_check),\n    contentDescription = null,  // Decorative icon next to descriptive text\n    tint = BitwardenTheme.colorScheme.success\n)\n\n// ❌ BAD - Interactive element missing description\nIcon(\n    painter = painterResource(R.drawable.ic_delete),\n    contentDescription = null,  // Interactive elements need descriptions\n    modifier = Modifier.clickable { onDelete() }\n)\n```\n\n## Prioritizing Findings\n\nUse `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.\n\n## Output Format\n\nSee `examples/review-outputs.md` for the required output format and inline comment structure.\n\n## Example Review\n\n```markdown\n## Summary\nUpdates login screen layout for improved visual hierarchy and touch targets\n\n## Critical Issues\nNone\n\n## Suggested Improvements\n\n**app/auth/LoginScreen.kt:67** - Can we use BitwardenTextField?\nThis custom text field looks very similar to `ui/components/BitwardenTextField.kt:89`.\nWould using the existing component maintain consistency?\n\n**app/auth/LoginScreen.kt:123** - Add contentDescription\n```kotlin\nIcon(\n    painter = painterResource(R.drawable.ic_visibility),\n    contentDescription = \"Show password\",  // Add for accessibility\n    modifier = Modifier.clickable { onToggleVisibility() }\n)\n```\n\n**app/auth/LoginScreen.kt:145** - Use design system spacing\n```kotlin\n// Current\nSpacer(modifier = Modifier.height(17.dp))\n\n// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)\nSpacer(modifier = Modifier.height(16.dp))\n```\n```\n\n<!-- File: .claude/skills/reviewing-changes/examples/review-outputs.md (Tokens: ~3027 | Category: anthropic-skill) -->\n# Review Output Examples\n\nWell-structured code reviews demonstrating appropriate depth, tone, and formatting for different change types.\n\n## Table of Contents\n\n**Format Reference:**\n- [Quick Format Reference](#quick-format-reference)\n  - [Inline Comment Format](#inline-comment-format-required)\n  - [Summary Comment Format](#summary-comment-format)\n\n**Examples:**\n- [Example 1: Clean PR (No Issues)](#example-1-clean-pr-no-issues)\n- [Example 2: Dependency Update with Breaking Changes](#example-2-dependency-update-with-breaking-changes)\n- [Example 3: Feature Addition with Critical Issues](#example-3-feature-addition-with-critical-issues)\n\n**Anti-Patterns:**\n- [❌ Anti-Patterns to Avoid](#-anti-patterns-to-avoid)\n  - [Problem: Verbose Summary with Multiple Sections](#problem-verbose-summary-with-multiple-sections)\n  - [Problem: Praise-Only Inline Comments](#problem-praise-only-inline-comments)\n  - [Problem: Missing `<details>` Tags](#problem-missing-details-tags)\n\n**Summary:**\n- [Summary](#summary)\n\n---\n\n## Quick Format Reference\n\n### Inline Comment Format (REQUIRED)\n\n**MUST use `<details>` tags.** Only severity + description visible; all other content collapsed.\n\n```\n[emoji] **[SEVERITY]**: [One-line issue description]\n\n<details>\n<summary>Details and fix</summary>\n\n[Code example or specific fix]\n\n[Rationale explaining why]\n\nReference: [docs link if applicable]\n</details>\n```\n\n**Severity Levels:**\n- ❌ **CRITICAL** - Blocking, must fix (security, crashes, architecture violations)\n- ⚠️ **IMPORTANT** - Should fix (missing tests, quality issues)\n- ♻️ **DEBT** - Technical debt (duplication, convention violations, future rework needed)\n- 🎨 **SUGGESTED** - Nice to have (refactoring, improvements)\n- ❓ **QUESTION** - Seeking clarification (requirements, design decisions)\n\n### Summary Comment Format\n\nUses the agent's `posting-review-summary` skill format. Surface ❌ CRITICAL issues at the top level for immediate visibility, wrap the full findings list in `<details>` for scannability.\n\n```\n**Overall Assessment:** APPROVE / REQUEST CHANGES\n\n[1-2 neutral sentences describing what was reviewed]\n\n**Critical Issues** (if any):\n- ❌ [One-line summary with file:line]\n\n<details>\n<summary>All findings</summary>\n\n- ❌ **CRITICAL**: [description] (`file:line`)\n- ⚠️ **IMPORTANT**: [description] (`file:line`)\n- ♻️ **DEBT**: [description] (`file:line`)\n- 🎨 **SUGGESTED**: [description] (`file:line`)\n- ❓ **QUESTION**: [description] (`file:line`)\n</details>\n```\n\nFor clean PRs with no findings, omit both sections entirely — verdict + 1-2 sentences is sufficient.\n\n**GitHub pitfall**: Never use `#` followed by a number in comment text (e.g., `#42`, `#PR123`). GitHub autolinks these to issues/PRs. Use `Finding 1:` or `item 42` instead.\n\n---\n\n## Example 1: Clean PR (No Issues)\n\n**Context**: Moving shared code to common module, complete migration, all patterns followed\n\n**Review Comment:**\n```markdown\n**Overall Assessment:** APPROVE\n\nClean refactoring that moves ExitManager to :ui module, eliminating duplication between apps.\n```\n\n**Why this works:**\n- Immediate approval visible (2-3 lines)\n- One sentence acknowledging the work\n- No unnecessary sections or elaborate praise\n- Author gets quick feedback and can proceed\n\n---\n\n## Example 2: Dependency Update with Breaking Changes\n\n**Context**: Major version update requiring code migration\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- API migration required for Retrofit 3.0 breaking changes (network/api/BitwardenApiService.kt:34)\n\nSee inline comments for migration details.\n```\n\n**Inline Comment 1** (on `network/api/BitwardenApiService.kt:34`):\n```markdown\n❌ **CRITICAL**: API migration required for Retrofit 3.0\n\n<details>\n<summary>Details and fix</summary>\n\nRetrofit 3.0 removes the `Call<T>` return type. All 12 API methods in this file need migration:\n\n```kotlin\n// Current (deprecated in Retrofit 3.0)\n@GET(\"api/accounts/profile\")\nfun getProfile(): Call<ProfileResponse>\n\n// Must migrate to\n@GET(\"api/accounts/profile\")\nsuspend fun getProfile(): Response<ProfileResponse>\n```\n\nBreaking API change affects:\n- 12 methods in BitwardenApiService\n- 8 methods in VaultApiService\n- All call sites using enqueue/execute\n- Test utilities\n\nConsider creating separate PR for this migration given the scope.\n\nReference: [Retrofit 3.0 migration guide](https://square.github.io/retrofit/changelogs/changelog-3.x/)\n</details>\n```\n\n**Key Features:**\n- Minimal summary (2-3 lines)\n- Full details in collapsed inline comment\n- Specific file:line references\n- Code examples in <details>\n- Migration guidance and scope assessment\n\n---\n\n## Example 3: Feature Addition with Critical Issues\n\n**Context**: Implements PIN unlock for vault access\n\n**Summary Comment:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Exposes mutable state violating MVVM (UnlockViewModel.kt:78)\n- PIN stored without encryption - SECURITY ISSUE (UnlockRepository.kt:145)\n\nSee inline comments for all issues and suggestions.\n```\n\n**Inline Comment 1** (on `app/vault/unlock/UnlockViewModel.kt:78`):\n```markdown\n❌ **CRITICAL**: Exposes mutable state\n\n<details>\n<summary>Details and fix</summary>\n\nChange `MutableStateFlow<State>` to `StateFlow<State>`:\n\n```kotlin\n// Current (problematic)\nval unlockState: MutableStateFlow<UnlockState>\n\n// Should be\nprivate val _unlockState = MutableStateFlow<UnlockState>()\nval unlockState: StateFlow<UnlockState> = _unlockState.asStateFlow()\n```\n\nExposing MutableStateFlow allows external mutation, violating MVVM unidirectional data flow.\n\nReference: docs/ARCHITECTURE.md#mvvm-pattern\n</details>\n```\n\n**Inline Comment 2** (on `data/vault/UnlockRepository.kt:145`):\n```markdown\n❌ **CRITICAL**: PIN stored without encryption - SECURITY ISSUE\n\n<details>\n<summary>Details and fix</summary>\n\nStoring PIN in plaintext SharedPreferences exposes it to backup systems and rooted devices.\n\n```kotlin\n// Current (CRITICAL SECURITY ISSUE)\nsharedPreferences.edit {\n    putString(KEY_PIN, pin)\n}\n\n// Must use Android Keystore encryption\nsuspend fun storePin(pin: String): Result<Unit> = runCatching {\n    val encrypted = keystoreManager.encrypt(pin.toByteArray())\n    encryptedPrefs.putBytes(KEY_PIN, encrypted)\n}\n```\n\nUse Android Keystore encryption or EncryptedSharedPreferences per security architecture.\n\nReference: docs/ARCHITECTURE.md#security\n</details>\n```\n\n**Inline Comment 3** (on `app/vault/unlock/UnlockViewModel.kt:92`):\n```markdown\n⚠️ **IMPORTANT**: Missing error handling test\n\n<details>\n<summary>Details and fix</summary>\n\nAdd test to prevent regression if error handling changes:\n\n```kotlin\n@Test\nfun `when incorrect PIN entered then returns error state`() = runTest {\n    val viewModel = UnlockViewModel(mockRepository)\n    coEvery { mockRepository.validatePin(\"1234\") }\n        returns Result.failure(InvalidPinException())\n\n    viewModel.onPinEntered(\"1234\")\n\n    assertEquals(UnlockState.Error(\"Invalid PIN\"), viewModel.state.value)\n}\n```\n\nEnsures error flow remains robust across refactorings.\n</details>\n```\n\n**Inline Comment 4** (on `app/vault/unlock/UnlockViewModel.kt:105`):\n```markdown\n🎨 **SUGGESTED**: Consider rate limiting for PIN attempts\n\n<details>\n<summary>Details and fix</summary>\n\nCurrently allows unlimited attempts, which could enable brute force attacks.\n\n```kotlin\nprivate var attemptCount = 0\nprivate var lockoutUntil: Instant? = null\n\nfun onPinEntered(pin: String) {\n    if (isLockedOut()) {\n        _state.value = UnlockState.LockedOut(lockoutUntil!!)\n        return\n    }\n    // ... validate PIN ...\n    if (invalid) {\n        attemptCount++\n        if (attemptCount >= MAX_ATTEMPTS) {\n            lockoutUntil = clock.millis() + 15.minutes\n        }\n    }\n}\n```\n\nWould add security layer against brute force. Consider discussing threat model with security team.\n</details>\n```\n\n**Inline Comment 5** (on `app/vault/unlock/UnlockScreen.kt:134`):\n```markdown\n❓ **QUESTION**: Can we use BitwardenTextField?\n\n<details>\n<summary>Details</summary>\n\nThis custom PIN input field looks similar to `ui/components/BitwardenTextField.kt:67`.\n\nWould using the existing component maintain consistency and reduce custom UI code?\n</details>\n```\n\n**Key Features:**\n- Minimal summary (3-4 lines) with critical issues only\n- Each issue gets separate inline comment with `<details>` tag\n- Multiple severity levels demonstrated (CRITICAL, IMPORTANT, SUGGESTED, QUESTION)\n- Mix of prescriptive fixes and collaborative questions\n- Code examples collapsed in <details>\n- No \"Good Practices\" or \"Action Items\" sections\n\n---\n\n## ❌ Anti-Patterns to Avoid\n\n### Problem: Verbose Summary with Multiple Sections\n\n**What NOT to do:**\n```markdown\n### Review Complete ✅\n\n## Summary\n[Lengthy description of what the PR does]\n\n### Strengths 👍\n1. **Excellent documentation** - KDoc comments are comprehensive\n2. **Proper fail-closed design** - Security defaults to rejection\n3. **Defense in depth** - Multiple validation layers\n[7 total items with elaboration]\n\n### Critical Issues ⚠️\n- Missing test coverage for security-critical code (with full details)\n- [More issues with full explanations]\n\n### Recommendations 🎨\n- [Multiple recommendations]\n\n### Test Coverage Status 📊\n- [Analysis]\n\n### Architecture Compliance ✅\n- [Analysis]\n\n## Recommendation\n**Conditional approval** with follow-up...\n```\n\n**Why this is wrong:**\n- 800+ tokens for a summary comment\n- Multiple sections (Strengths, Recommendations, Test Coverage, Architecture)\n- Elaborates on positive aspects (\"Excellent documentation...\")\n- Duplicates critical issues (summary has details + inline comments have same details)\n- Creates visual clutter in PR conversation\n\n**Correct approach:**\n```markdown\n**Overall Assessment:** REQUEST CHANGES\n\n**Critical Issues:**\n- Missing test coverage for security-critical code (PasswordManagerSignatureVerifierImpl.kt:47)\n\nSee inline comments for details.\n```\n\n**Key differences:**\n- 3-5 lines vs 800+ tokens\n- Verdict + critical issues only\n- All details belong in inline comments\n- No positive commentary sections\n- Scales with PR complexity, not analysis thoroughness\n\n### Problem: Praise-Only Inline Comments\n\n**What NOT to do:**\n\nCreating inline comment on `AuthenticatorBridgeManagerImpl.kt:73`:\n```markdown\n👍 **Excellent integration of signature verification**\n\nThe signature verification is properly integrated into the connection flow:\n- Checked during initialization (line 73)\n- Checked before binding (line 134)\n- Ensures only validated apps can connect\n\nThis is exactly the right approach for fail-safe security.\n```\n\n**Why this is wrong:**\n- Entire comment is positive feedback with no actionable issue\n- Takes up space in PR conversation\n- Distracts from actual issues\n- Violates \"focus on actionable feedback\" principle\n\n**Correct approach:**\n- Do not create this comment at all\n- Reserve inline comments exclusively for issues requiring attention\n\n### Problem: Missing `<details>` Tags\n\n**What NOT to do:**\n\n```markdown\n❌ **CRITICAL**: Missing test coverage for security-critical code\n\nThe `@OmitFromCoverage` annotation excludes this entire class from test coverage.\n\n**Problems:**\n1. No validation that certificate hashes match actual Bitwarden certificates\n2. No verification of fail-closed behavior on edge cases\n3. No tests for multiple signer rejection logic\n4. Certificate hash typos would go undetected until production\n\n**Recommendation:**\nReplace `@OmitFromCoverage` with proper unit tests.\n\nExample test structure:\n[long code block]\n\nSecurity-critical code should have the highest test coverage, not be omitted.\n```\n\n**Why this is wrong:**\n- All content visible immediately (code examples, problems list, rationale)\n- Creates visual clutter in PR conversation\n- Makes it hard to scan multiple issues quickly\n\n**Correct approach:**\n```markdown\n❌ **CRITICAL**: Missing test coverage for security-critical code\n\n<details>\n<summary>Details and fix</summary>\n\nThe `@OmitFromCoverage` annotation excludes this entire class from test coverage.\n\n**Problems:**\n1. No validation that certificate hashes match actual Bitwarden certificates\n2. No verification of fail-closed behavior on edge cases\n3. No tests for multiple signer rejection logic\n4. Certificate hash typos would go undetected until production\n\n**Recommendation:**\nReplace `@OmitFromCoverage` with proper unit tests.\n\nExample test structure:\n[code block]\n\nSecurity-critical code should have the highest test coverage, not be omitted.\n</details>\n```\n\n**Key difference:** Only severity + one-line description visible. All details collapsed.\n\n---\n\n## Summary\n\n**Always use:**\n- Minimal summary (verdict + critical issues)\n- Separate inline comments with `<details>` tags\n- Hybrid emoji + text severity prefixes\n- Focus exclusively on actionable feedback\n\n**Never use:**\n- Multiple summary sections (Strengths, Recommendations, etc.)\n- Praise-only inline comments\n- Duplication between summary and inline comments\n- Verbose analysis in summary (belongs in inline comments)\n\n<!-- File: .claude/skills/reviewing-changes/reference/architectural-patterns.md (Tokens: ~2166 | Category: anthropic-skill) -->\n# Architectural Patterns Quick Reference\n\nQuick reference for Bitwarden Android architectural patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## Table of Contents\n\n**Core Patterns:**\n- [MVVM + UDF Pattern](#mvvm--udf-pattern)\n  - [ViewModel Structure](#viewmodel-structure)\n  - [UI Layer (Compose)](#ui-layer-compose)\n- [Hilt Dependency Injection](#hilt-dependency-injection)\n  - [ViewModels](#viewmodels)\n  - [Repositories and Managers](#repositories-and-managers)\n  - [Clock/Time Handling](#clocktime-handling)\n- [Module Organization](#module-organization)\n- [Error Handling](#error-handling)\n  - [Use Result Types, Not Exceptions](#use-result-types-not-exceptions)\n- [Quick Checklist](#quick-checklist)\n\n---\n\n## MVVM + UDF Pattern\n\n### ViewModel Structure\n\n**✅ GOOD - Proper state encapsulation**:\n```kotlin\n@HiltViewModel\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository\n) : ViewModel() {\n    // Private mutable state\n    private val _state = MutableStateFlow<FeatureState>(FeatureState.Initial)\n\n    // Public immutable state\n    val state: StateFlow<FeatureState> = _state.asStateFlow()\n\n    // Actions as functions, state updated via internal action\n    fun onActionClicked() {\n        viewModelScope.launch {\n            val result = repository.performAction()\n            sendAction(FeatureAction.Internal.ActionComplete(result))\n        }\n    }\n\n    // The ViewModel has a handler that processes the internal action\n    private fun handleInternalAction(action: FeatureAction.Internal) {\n        when (action) {\n            is FeatureAction.Internal.ActionComplete -> {\n                // The action handler evaluates the result and updates state\n                action.result.fold(\n                    onSuccess = { _state.value = State.Success },\n                    onFailure = { _state.value = State.Error(it) }\n                )\n            }\n        }\n    }\n}\n```\n\n**❌ BAD - Common violations**:\n```kotlin\nclass FeatureViewModel : ViewModel() {\n    // ❌ Exposes mutable state\n    val state: MutableStateFlow<FeatureState>\n\n    // ❌ Business logic in ViewModel\n    fun onSubmit() {\n        val encrypted = encryptionManager.encrypt(data)  // Should be in Repository\n        _state.value = FeatureState.Success\n    }\n\n    // ❌ Direct Android framework dependency\n    fun onCreate(context: Context) {  // ViewModels shouldn't depend on Context\n        // ...\n    }\n}\n```\n\n**Key Rules**:\n- Expose `StateFlow<T>`, never `MutableStateFlow<T>`\n- Delegate business logic to Repository/Manager\n- No direct Android framework dependencies (except ViewModel, SavedStateHandle)\n- Use `viewModelScope` for coroutines\n\nReference: `docs/ARCHITECTURE.md#mvvm-pattern`\n\n---\n\n### UI Layer (Compose)\n\n**✅ GOOD - Stateless, observes only**:\n```kotlin\n@Composable\nfun FeatureScreen(\n    state: FeatureState,\n    onActionClick: () -> Unit,\n    modifier: Modifier = Modifier\n) {\n    Column(modifier = modifier) {\n        when (state) {\n            is FeatureState.Loading -> LoadingIndicator()\n            is FeatureState.Success -> SuccessContent(state.data)\n            is FeatureState.Error -> ErrorMessage(state.error)\n        }\n\n        BitwardenButton(\n            text = \"Action\",\n            onClick = onActionClick  // Sends event to ViewModel\n        )\n    }\n}\n```\n\n**❌ BAD - Stateful, modifies state**:\n```kotlin\n@Composable\nfun FeatureScreen(viewModel: FeatureViewModel) {\n    var localState by remember { mutableStateOf(...) }  // ❌ State in UI\n\n    Button(onClick = {\n        viewModel._state.value = FeatureState.Loading  // ❌ Directly modifying ViewModel state\n    })\n}\n```\n\n**Key Rules**:\n- Compose screens observe state, never modify\n- User actions passed as events/callbacks to ViewModel\n- No business logic in UI layer\n- Use existing components from `:ui` module\n\n---\n\n## Hilt Dependency Injection\n\n### ViewModels\n\n**✅ GOOD - Interface injection**:\n```kotlin\n@HiltViewModel\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepository,  // Interface, not implementation\n    private val authManager: AuthManager,\n    savedStateHandle: SavedStateHandle\n) : ViewModel()\n```\n\n**❌ BAD - Common violations**:\n```kotlin\n// ❌ No @HiltViewModel annotation\nclass FeatureViewModel @Inject constructor(...)\n\n// ❌ Injecting implementation instead of interface\nclass FeatureViewModel @Inject constructor(\n    private val repository: FeatureRepositoryImpl  // Should inject interface\n)\n\n// ❌ Manual instantiation\nclass FeatureViewModel : ViewModel() {\n    private val repository = FeatureRepositoryImpl()  // Should use @Inject\n}\n```\n\n**Key Rules**:\n- Annotate with `@HiltViewModel`\n- Use `@Inject constructor`\n- Inject interfaces, not implementations\n- Use `SavedStateHandle` for process death survival\n\nReference: `docs/ARCHITECTURE.md#dependency-injection`\n\n---\n\n### Repositories and Managers\n\n**✅ GOOD - Implementation with @Inject**:\n```kotlin\ninterface FeatureRepository {\n    suspend fun fetchData(): Result<Data>\n}\n\nclass FeatureRepositoryImpl @Inject constructor(\n    private val apiService: FeatureApiService,\n    private val database: FeatureDao\n) : FeatureRepository {\n    override suspend fun fetchData(): Result<Data> = runCatching {\n        apiService.getData()\n    }\n}\n```\n\n**Module provides interface**:\n```kotlin\n@Module\n@InstallIn(SingletonComponent::class)\nabstract class DataModule {\n    @Binds\n    @Singleton\n    abstract fun bindFeatureRepository(\n        impl: FeatureRepositoryImpl\n    ): FeatureRepository\n}\n```\n\n**Key Rules**:\n- Define interface for abstraction\n- Implementation uses `@Inject constructor`\n- Module binds implementation to interface\n- Appropriate scoping (`@Singleton`, `@ViewModelScoped`)\n\n---\n\n### Clock/Time Handling\n\nTime-dependent code must use injected `Clock` rather than direct `Instant.now()` or `DateTime.now()` calls. This follows the same DI principle as other dependencies.\n\n**✅ GOOD - Injected Clock**:\n```kotlin\n// ViewModel with Clock injection\nclass MyViewModel @Inject constructor(\n    private val clock: Clock,\n) {\n    fun save() {\n        val timestamp = clock.instant()\n    }\n}\n\n// Extension function with Clock parameter\nfun State.getTimestamp(clock: Clock): Instant =\n    existingTime ?: clock.instant()\n```\n\n**❌ BAD - Static/direct calls**:\n```kotlin\n// Hidden dependency, non-testable\nval timestamp = Instant.now()\nval dateTime = DateTime.now()\n```\n\n**Key Rules**:\n- Inject `Clock` via Hilt constructor (like other dependencies)\n- Pass `Clock` as parameter to extension functions\n- `Clock` is provided via `CoreModule` as singleton\n- Enables deterministic testing with `Clock.fixed(...)`\n\nReference: `docs/STYLE_AND_BEST_PRACTICES.md#best-practices--time-and-clock-handling`\n\n---\n\n## Module Organization\n\n```\nandroid/\n├── core/           # Shared utilities (cryptography, analytics, logging)\n├── data/           # Repositories, database, domain models\n├── network/        # API clients, network utilities\n├── ui/             # Reusable Compose components, theme\n├── app/            # Application, feature screens, ViewModels\n└── authenticator/  # Authenticator app (separate from password manager)\n```\n\n**Correct Placement**:\n- UI screens and ViewModels → `:app`\n- Reusable Compose components → `:ui`\n- Data models and Repositories → `:data`\n- API services → `:network`\n- Cryptography, logging → `:core`\n\n**Check for**:\n- No circular dependencies\n- Correct module placement\n- Proper visibility (internal vs public)\n\nReference: `docs/ARCHITECTURE.md#module-structure`\n\n---\n\n## Error Handling\n\n### Use Result Types, Not Exceptions\n\n**✅ GOOD - Result-based**:\n```kotlin\n// Repository\nsuspend fun fetchData(): Result<Data> = runCatching {\n    apiService.getData()\n}\n\n// ViewModel\nfun onFetch() {\n    viewModelScope.launch {\n        val result = repository.fetchData()\n        sendAction(FeatureAction.Internal.FetchComplete(result))\n    }\n}\n```\n\n**❌ BAD - Exception-based in business logic**:\n```kotlin\n// ❌ Don't throw in business logic\nsuspend fun fetchData(): Data {\n    try {\n        return apiService.getData()\n    } catch (e: Exception) {\n        throw FeatureException(e)  // Don't throw in repositories\n    }\n}\n\n// ❌ Try-catch in ViewModel\nfun onFetch() {\n    viewModelScope.launch {\n        try {\n            val data = repository.fetchData()\n            sendAction(FeatureAction.Internal.FetchComplete(data))\n        } catch (e: Exception) {\n            sendAction(FeatureAction.Internal.FetchComplete(e))\n        }\n    }\n}\n```\n\n**Key Rules**:\n- Use `Result<T>` return types in repositories\n- Use `runCatching { }` to wrap API calls\n- Handle results with `.fold()` in ViewModels\n- Don't throw exceptions in business logic\n\nReference: `docs/ARCHITECTURE.md#error-handling`\n\n---\n\n## Quick Checklist\n\n### Architecture\n- [ ] ViewModels expose StateFlow, not MutableStateFlow?\n- [ ] Business logic in Repository, not ViewModel?\n- [ ] Using Hilt DI (@HiltViewModel, @Inject constructor)?\n- [ ] Injecting interfaces, not implementations?\n- [ ] Time-dependent code uses injected `Clock` (not `Instant.now()`)?\n- [ ] Correct module placement?\n\n### Error Handling\n- [ ] Using Result types, not exceptions in business logic?\n- [ ] Errors handled with .fold() in ViewModels?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n\n<!-- File: .claude/skills/reviewing-changes/reference/priority-framework.md (Tokens: ~2997 | Category: anthropic-skill) -->\n# Finding Priority Framework\n\nUse this framework to classify findings during code review. Clear prioritization helps authors triage and address issues effectively.\n\n## Table of Contents\n\n**Severity Categories:**\n- [❌ CRITICAL (Blocker - Must Fix Before Merge)](#critical-blocker---must-fix-before-merge)\n- [⚠️ IMPORTANT (Should Fix)](#important-should-fix)\n- [♻️ DEBT (Technical Debt)](#debt-technical-debt)\n- [🎨 SUGGESTED (Nice to Have)](#suggested-nice-to-have)\n- [❓ QUESTION (Seeking Clarification)](#question-seeking-clarification)\n- [Optional (Acknowledge But Don't Require)](#optional-acknowledge-but-dont-require)\n\n**Guidelines:**\n- [Classification Guidelines](#classification-guidelines)\n  - [When Something is Between Categories](#when-something-is-between-categories)\n  - [Context Matters](#context-matters)\n- [Examples by Change Type](#examples-by-change-type)\n- [Special Cases](#special-cases)\n- [Summary](#summary)\n\n---\n\n## ❌ **CRITICAL** (Blocker - Must Fix Before Merge)\n\nThese issues **must** be addressed before the PR can be merged. They pose immediate risks to security, stability, or architecture integrity.\n\n### Security\n- Data leaks or plaintext sensitive data (passwords, keys, tokens)\n- Weak encryption or insecure key storage\n- Missing authentication or authorization checks\n- Input injection vulnerabilities (SQL, XSS, command injection)\n- Sensitive data in logs or error messages\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption\nPIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.\nReference: docs/ARCHITECTURE.md#security\n```\n\n### Stability\n- Compilation errors or warnings\n- Null pointer exceptions in production paths\n- Resource leaks (file handles, network connections, memory)\n- Crashes or unhandled exceptions in critical paths\n- Thread safety violations\n\n**Example**:\n```\n**app/auth/BiometricRepository.kt:120** - CRITICAL: Missing null safety check\nbiometricPrompt result can be null. Add explicit null check to prevent crash.\n```\n\n### Architecture\n- Mutable state exposure in ViewModels (violates MVVM)\n- Exception-based error handling in business logic (should use Result)\n- Circular dependencies between modules\n- Violation of zero-knowledge principles\n- Direct dependency instantiation (should use DI)\n\n**Example**:\n```\n**app/login/LoginViewModel.kt:45** - CRITICAL: Exposes mutable state\nChange MutableStateFlow to StateFlow in public API to prevent external state mutation.\nThis violates MVVM encapsulation pattern.\n```\n\n---\n\n## ⚠️ **IMPORTANT** (Should Fix)\n\nThese issues should be addressed but don't block merge if there's a compelling reason. They improve code quality, maintainability, or robustness.\n\n### Testing\n- Missing tests for critical paths (authentication, encryption, data sync)\n- Missing tests for new public APIs\n- Tests that don't verify actual behavior (test implementation, not behavior)\n- Missing test coverage for error scenarios\n\n**Example**:\n```\n**data/auth/BiometricRepository.kt** - IMPORTANT: Missing test for cancellation\nAdd test for user cancellation scenario to prevent regression.\n```\n\n### Architecture\n- Inconsistent patterns within PR (mixing error handling approaches)\n- Poor separation of concerns\n- Tight coupling between components\n- Not following established project patterns\n\n**Example**:\n```\n**app/vault/VaultViewModel.kt:89** - IMPORTANT: Business logic in ViewModel\nEncryption logic should be in Repository, not ViewModel.\nReference: docs/ARCHITECTURE.md#mvvm-pattern\n```\n\n### Documentation\n- Undocumented public APIs (missing KDoc)\n- Missing documentation for complex algorithms\n- Unclear naming or confusing interfaces\n\n**Example**:\n```\n**core/crypto/EncryptionManager.kt:34** - IMPORTANT: Missing KDoc\nPublic encryption method should document parameters, return value, and exceptions.\n```\n\n### Performance\n- Inefficient algorithms in hot paths (with evidence from profiling)\n- Blocking main thread with I/O operations\n- Memory inefficient data structures (with evidence)\n\n**Example**:\n```\n**app/vault/VaultListViewModel.kt:78** - IMPORTANT: N+1 query pattern\nFetching items one-by-one in loop. Consider batch fetch to reduce database queries.\n```\n\n---\n\n## ♻️ **DEBT** (Technical Debt)\n\nCode that duplicates existing patterns, violates established conventions, or will require rework within 6 months. Introduces technical debt that should be tracked for future cleanup.\n\n### Duplication\n- Copy-pasted code blocks across files\n- Repeated validation or business logic\n- Multiple implementations of same pattern\n- Data transformation duplicated in multiple places\n\n**Example**:\n```\n**app/vault/VaultListViewModel.kt:156** - DEBT: Duplicates encryption logic\nSame encryption pattern exists in VaultRepository.kt:234 and SyncManager.kt:89.\nExtract to shared EncryptionUtil to reduce maintenance burden.\n```\n\n### Convention Violations\n- Inconsistent error handling approaches within same module\n- Mixing architectural patterns (MVVM + MVC)\n- Not following established DI patterns\n- Deviating from project code style significantly\n\n**Example**:\n```\n**data/auth/AuthRepository.kt:78** - DEBT: Exception-based error handling\nProject standard is Result<T> for error handling. This uses try-catch with throws.\nCreates inconsistency and makes testing harder.\nReference: docs/ARCHITECTURE.md#error-handling\n```\n\n### Future Rework Required\n- Hardcoded values that should be configurable\n- Temporary workarounds without TODO/FIXME\n- Code that will need changes when planned features arrive\n- Tight coupling that prevents future extensibility\n\n**Example**:\n```\n**app/settings/SettingsViewModel.kt:45** - DEBT: Hardcoded feature flags\nFeature flags should come from remote config for A/B testing.\nWill require rework when experimentation framework launches.\n```\n\n---\n\n## 🎨 **SUGGESTED** (Nice to Have)\n\nImprovements with measurable value only. A finding qualifies as SUGGESTED if it provides: security gain, cyclomatic complexity reduction, bug class prevention, or elimination of an O(n²) pattern. Subjective style preferences, vague simplifications, and naming nitpicks do not qualify — leave those out entirely or raise in conversation.\n\n### Code Quality\n- Extractable duplicated logic that reduces measurable complexity or improves testability\n- Patterns that would prevent a recurring bug class in this module\n- Architecture improvements that eliminate tight coupling with measurable impact\n\n**Example**:\n```\n**app/vault/VaultScreen.kt:145** - SUGGESTED: Consider extracting helper function\nThis 20-line block appears in 3 places. Consider extracting to reduce duplication.\n```\n\n### Testing\n- Additional test coverage for edge cases (beyond critical paths)\n- More comprehensive integration tests\n- Performance tests for non-critical paths\n\n**Example**:\n```\n**app/login/LoginViewModelTest.kt** - SUGGESTED: Add test for concurrent login attempts\nNot critical, but would increase confidence in edge case handling.\n```\n\n### Refactoring\n- Extracting reusable patterns\n- Modernizing old patterns (if touching related code)\n- Improving testability\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:200** - SUGGESTED: Consider extracting validation logic\nCould be extracted to separate validator class for reusability and testing.\n```\n\n---\n\n## ❓ **QUESTION** (Seeking Clarification)\n\nQuestions about requirements, unclear intent, or potential conflicts that require human knowledge to answer. Open inquiries that cannot be resolved through code inspection alone.\n\n### Requirements Clarification\n- Ambiguous acceptance criteria\n- Multiple valid implementation approaches\n- Unclear business rules or edge case handling\n- Conflicting requirements between specs and implementation\n\n**Example**:\n```\n**app/vault/ItemListViewModel.kt:67** - QUESTION: Expected sort behavior for equal timestamps?\nWhen items have identical timestamps, should secondary sort be by:\n- Name (alphabetical)\n- Creation order\n- Item type priority\n\nSpec doesn't specify tie-breaking logic.\n```\n\n### Design Decisions\n- Architecture choices that could go multiple ways\n- Trade-offs between approaches without clear winner\n- Feature flag strategy or rollout approach\n- API design with multiple valid patterns\n\n**Example**:\n```\n**data/sync/SyncManager.kt:134** - QUESTION: Should sync failures retry automatically?\nCurrent implementation fails immediately. Options:\n- Exponential backoff (3 retries)\n- User-triggered retry only\n- Background retry on network restore\n\nWhat's the expected UX?\n```\n\n### System Integration\n- Unclear contracts with external systems\n- Potential conflicts with other features/modules\n- Assumptions about third-party API behavior\n- Cross-team coordination needs\n\n**Example**:\n```\n**app/auth/BiometricPrompt.kt:89** - QUESTION: Compatibility with pending device credentials PR?\nPR #1234 is refactoring device credentials. Should this:\n- Merge first and adapt later\n- Wait for #1234 to land\n- Coordinate with that author\n\nTiming unclear.\n```\n\n### Testing Strategy\n- Uncertainty about test scope or approach\n- Questions about mocking external dependencies\n- Edge cases that need product input\n- Performance testing requirements\n\n**Example**:\n```\n**data/vault/EncryptionTest.kt:45** - QUESTION: Should we test against real Keystore?\nCurrently using mocked Keystore. Real Keystore testing would:\n+ Catch hardware-specific issues\n- Slow down CI significantly\n- Require API 23+ emulators\n\nWhat's the priority?\n```\n\n---\n\n## Optional (Acknowledge But Don't Require)\n\nNote good practices to reinforce positive patterns. Keep these **brief** - list only, no elaboration.\n\n### Good Practices\n\n**Format**: Simple bullet list, no explanation\n\n```markdown\n## Good Practices\n- Proper Hilt DI usage throughout\n- Comprehensive unit test coverage\n- Clear separation of concerns\n- Well-documented public APIs\n```\n\n**Don't do this** (too verbose):\n```markdown\n## Good Practices\n- Proper Hilt DI usage throughout: Great job using @Inject constructor and injecting interfaces! This follows our established patterns perfectly and makes the code very testable. Really excellent work here! 👍\n```\n\n---\n\n## Classification Guidelines\n\n### When Something is Between Categories\n\n**If unsure between Critical and Important**:\n- Ask: \"Could this cause production incidents, data loss, or security breaches?\"\n- If yes → Critical\n- If no → Important\n\n**If unsure between Important and Debt**:\n- Ask: \"Is this a bug/defect or just duplication/inconsistency?\"\n- If bug/defect → Important\n- If duplication/inconsistency → Debt\n\n**If unsure between Important and Suggested**:\n- Ask: \"Would I block merge over this?\"\n- If yes → Important\n- If no → Suggested\n\n**If unsure between Debt and Suggested**:\n- Ask: \"Will this require rework within 6 months?\"\n- If yes → Debt\n- If no → Suggested\n\n**If unsure between Suggested and Question**:\n- Ask: \"Am I requesting a change or asking for clarification?\"\n- If requesting change → Suggested\n- If seeking clarification → Question\n\n**If unsure between Suggested and Optional**:\n- Ask: \"Is this actionable feedback or just acknowledgment?\"\n- If actionable → Suggested\n- If acknowledgment → Optional\n\n### Context Matters\n\n**Same issue, different contexts**:\n\n```\n// Critical for production code\nMissing null safety check in auth flow → CRITICAL\n\n// Suggested for internal test utility\nMissing null safety check in test helper → SUGGESTED\n```\n\n**Same pattern, different risk levels**:\n\n```\n// Critical for new feature\nMissing tests for new auth method → CRITICAL\n\n// Important for bug fix\nMissing regression test → IMPORTANT\n\n// Suggested for refactoring\nMissing tests for refactored helper → SUGGESTED\n```\n\n---\n\n## Examples by Change Type\n\n### Dependency Update\n- **Critical**: Known CVEs in old version not addressed\n- **Important**: Breaking changes that need migration\n- **Suggested**: Beta/alpha version stability concerns\n\n### Bug Fix\n- **Critical**: Fix doesn't address root cause\n- **Important**: Missing regression test\n- **Suggested**: Similar bugs in related code\n\n### Feature Addition\n- **Critical**: Security vulnerabilities, architecture violations\n- **Important**: Missing tests for critical paths\n- **Suggested**: Additional test coverage, minor refactoring\n\n### UI Refinement\n- **Critical**: Missing accessibility for key actions\n- **Important**: Not using theme (hardcoded colors)\n- **Suggested**: Minor spacing/alignment improvements\n\n### Refactoring\n- **Critical**: Changes behavior (should be behavior-preserving)\n- **Important**: Incomplete migration (mix of old/new patterns)\n- **Suggested**: Additional instances that could be refactored\n\n### Infrastructure\n- **Critical**: Hardcoded secrets, no rollback plan\n- **Important**: Performance regression in build times\n- **Suggested**: Further optimization opportunities\n\n---\n\n## Special Cases\n\n### Technical Debt\n- Acknowledge existing tech debt but don't require fixing in unrelated PR\n- Exception: If change makes tech debt worse, it's Important to address\n\n### Scope Creep\n- Don't request changes outside PR scope\n- Can note as \"Future consideration\" but not required for this PR\n\n### Linter-Catchable Issues\n- Don't flag issues that automated tools handle\n- Exception: If linter is misconfigured and missing real issues\n\n### Personal Preferences\n- Don't flag unless grounded in project standards or architectural principles\n- Use \"I-statements\" if suggesting alternative approaches\n\n---\n\n## Summary\n\n**Critical**: Block merge, must fix (security, stability, architecture)\n**Important**: Should fix before merge (testing, quality, performance)\n**Debt**: Technical debt introduced, track for future cleanup\n**Suggested**: Nice to have, consider effort vs benefit\n**Question**: Seeking clarification on requirements or design\n**Optional**: Acknowledge good practices, keep brief\n\n<!-- File: .claude/skills/reviewing-changes/reference/review-psychology.md (Tokens: ~868 | Category: anthropic-skill) -->\n# Review Psychology: Constructive Feedback Phrasing\n\nEffective code review feedback is clear, actionable, and constructive. This guide provides phrasing patterns for inline comments.\n\n## Table of Contents\n\n**Guidelines:**\n- [Phrasing Templates](#phrasing-templates)\n  - [Critical Issues (Prescriptive)](#critical-issues-prescriptive)\n  - [Suggested Improvements (Exploratory)](#suggested-improvements-exploratory)\n  - [Questions (Collaborative)](#questions-collaborative)\n  - [Test Suggestions](#test-suggestions)\n- [When to Be Prescriptive vs Ask Questions](#when-to-be-prescriptive-vs-ask-questions)\n- [Special Cases](#special-cases)\n\n---\n\n## Phrasing Templates\n\n### Critical Issues (Prescriptive)\n\n**Pattern**: State problem + Provide solution + Explain why\n\n```\n**[file:line]** - CRITICAL: [Issue description]\n\n[Specific fix with code example if applicable]\n\n[Rationale explaining why this is critical]\n\nReference: [docs link if applicable]\n```\n\n**Example**:\n```\n**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption\n\nPIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.\nPlaintext storage exposes the PIN to backup systems and rooted devices.\n\nReference: docs/ARCHITECTURE.md#security\n```\n\n---\n\n### Suggested Improvements (Exploratory)\n\n**Pattern**: Observe + Suggest + Explain benefit\n\n```\n**[file:line]** - Consider [alternative approach]\n\n[Current observation]\nCan we [specific suggestion]?\n\n[Benefit or rationale]\n```\n\n**Example**:\n```\n**app/login/LoginScreen.kt:89** - Consider using existing BitwardenButton\n\nThis custom button implementation looks similar to `ui/components/BitwardenButton.kt:45`.\nCan we use the existing component to maintain consistency across the app?\n```\n\n---\n\n### Questions (Collaborative)\n\n**Pattern**: Ask + Provide context (optional)\n\n```\n**[file:line]** - [Question about intent or approach]?\n\n[Optional context or observation]\n```\n\n**Example**:\n```\n**data/sync/SyncManager.kt:234** - How does this handle concurrent sync attempts?\n\nIt looks like multiple coroutines could call `startSync()` simultaneously.\nIs there a mechanism to prevent race conditions, or is that handled elsewhere?\n```\n\n---\n\n### Test Suggestions\n\n**Pattern**: Observe gap + Suggest specific test + Provide skeleton\n\n```\n**[file:line]** - Consider adding test for [scenario]\n\n[Rationale]\n\n```kotlin\n@Test\nfun `test description`() = runTest {\n    // Test skeleton\n}\n```\n```\n\n**Example**:\n```\n**data/auth/BiometricRepository.kt** - Consider adding test for cancellation scenario\n\nThis would prevent regression of the bug you just fixed:\n\n```kotlin\n@Test\nfun `when biometric cancelled then returns cancelled state`() = runTest {\n    coEvery { biometricPrompt.authenticate() } returns null\n\n    val result = repository.authenticate()\n\n    assertEquals(AuthResult.Cancelled, result)\n}\n```\n```\n\n---\n\n## When to Be Prescriptive vs Ask Questions\n\n**Be Prescriptive** (Tell them what to do):\n- Security issues\n- Architecture pattern violations\n- Null safety problems\n- Compilation errors\n- Documented project standards\n\n**Ask Questions** (Seek explanation):\n- Design decisions with multiple valid approaches\n- Performance trade-offs without data\n- Unclear intent or reasoning\n- Scope decisions (this PR vs future work)\n- Patterns not documented in project guidelines\n\n---\n\n## Special Cases\n\n**Nitpicks** - For truly minor suggestions, use \"Nit:\" prefix:\n```\n**Nit**: Extra blank line at line 145\n```\n\n**Uncertainty** - If unsure, acknowledge it:\n```\nI'm not certain, but this might be called frequently.\nHas this been profiled?\n```\n\n**Positive Feedback** - Brief list only, no elaboration:\n```\n## Good Practices\n- Proper Hilt DI usage throughout\n- Comprehensive unit test coverage\n- Clear separation of concerns\n```\n\n<!-- File: .claude/skills/reviewing-changes/reference/security-patterns.md (Tokens: ~595 | Category: anthropic-skill) -->\n# Security Patterns Quick Reference\n\nQuick reference for Bitwarden Android security patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md#security`.\n\n## Encryption and Key Storage\n\n**✅ GOOD - Android Keystore**:\n```kotlin\n// Sensitive data encrypted with Keystore\nclass SecureStorage @Inject constructor(\n    private val keystoreManager: KeystoreManager\n) {\n    suspend fun storePin(pin: String): Result<Unit> = runCatching {\n        val encrypted = keystoreManager.encrypt(pin.toByteArray())\n        securePreferences.putBytes(KEY_PIN, encrypted)\n    }\n}\n\n// Or use EncryptedSharedPreferences\nval encryptedPrefs = EncryptedSharedPreferences.create(\n    context,\n    \"secure_prefs\",\n    masterKey,\n    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n)\n```\n\n**❌ BAD - Plaintext or weak encryption**:\n```kotlin\n// ❌ CRITICAL - Plaintext storage\nsharedPreferences.edit {\n    putString(\"pin\", userPin)  // Never store sensitive data in plaintext\n}\n\n// ❌ CRITICAL - Weak encryption\nval cipher = Cipher.getInstance(\"DES\")  // Use AES-256-GCM\n\n// ❌ CRITICAL - Hardcoded keys\nval key = \"my_secret_key_123\"  // Use Android Keystore\n```\n\n**Key Rules**:\n- Use Android Keystore for encryption keys\n- Use EncryptedSharedPreferences for simple key-value storage\n- Use AES-256-GCM for encryption\n- Never store sensitive data in plaintext\n- Never hardcode encryption keys\n\nReference: `docs/ARCHITECTURE.md#security`\n\n---\n\n## Logging Sensitive Data\n\n**✅ GOOD - No sensitive data**:\n```kotlin\nLog.d(TAG, \"Authentication attempt for user\")\nLog.d(TAG, \"Vault sync completed with ${items.size} items\")\n```\n\n**❌ BAD - Logs sensitive data**:\n```kotlin\n// ❌ CRITICAL\nLog.d(TAG, \"Password: $password\")\nLog.d(TAG, \"Auth token: $token\")\nLog.d(TAG, \"PIN: $pin\")\nLog.d(TAG, \"Encryption key: ${key.encoded}\")\n```\n\n**Key Rules**:\n- Never log passwords, PINs, tokens, keys\n- Never log encryption keys or sensitive data\n- Be careful with error messages (don't include sensitive context)\n\n---\n\n## Quick Checklist\n\n### Security\n- [ ] Sensitive data encrypted with Keystore?\n- [ ] No plaintext passwords/keys?\n- [ ] No sensitive data in logs?\n- [ ] Using AES-256-GCM for encryption?\n- [ ] No hardcoded encryption keys?\n\n---\n\nFor comprehensive security details, always refer to:\n- `docs/ARCHITECTURE.md#security` - Complete security architecture and zero-knowledge principles\n\n<!-- File: .claude/skills/reviewing-changes/reference/style-patterns.md (Tokens: ~258 | Category: anthropic-skill) -->\n# Style Patterns Quick Reference\n\nProject-specific Kotlin style rules to catch during code review. These supplement (not replace) `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## `when` branches with wrapped right-hand side require curly braces\n\nWhen a `when` branch's expression is too long to fit on the same line as `->` and is wrapped to its own line, the body must be wrapped in `{ }`. A bare `->` followed by an indented expression on the next line should be flagged.\n\n**Flag this:**\n\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT ->\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n}\n```\n\n**Accept this:**\n\n```kotlin\nwhen (type) {\n    VaultItemCipherType.LOGIN -> VaultAddEditState.ViewState.Content.ItemType.Login()\n    VaultItemCipherType.BANK_ACCOUNT -> {\n        VaultAddEditState.ViewState.Content.ItemType.BankAccount()\n    }\n}\n```\n\nSingle-line branches (body fits alongside `->`) do **not** require braces.\n\n**Suggested classification:** SUGGESTED (style consistency, not correctness).\n\n<!-- File: .claude/skills/reviewing-changes/reference/testing-patterns.md (Tokens: ~772 | Category: anthropic-skill) -->\n# Testing Patterns Quick Reference\n\nQuick reference for Bitwarden Android testing patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## ViewModel Tests\n\n**✅ GOOD - Tests behavior**:\n```kotlin\n@Test\nfun `when login succeeds then state updates to success`() = runTest {\n    // Arrange\n    val viewModel = LoginViewModel(mockRepository)\n    coEvery { mockRepository.login(any(), any()) } returns Result.success(User())\n\n    // Act\n    viewModel.onLoginClicked(\"user@example.com\", \"password\")\n\n    // Assert\n    viewModel.state.test {\n        assertEquals(LoginState.Loading, awaitItem())\n        assertEquals(LoginState.Success, awaitItem())\n    }\n}\n```\n\n**❌ BAD - Tests implementation**:\n```kotlin\n@Test\nfun `repository is called with correct parameters`() {\n    // ❌ This tests implementation details, not behavior\n    viewModel.onLoginClicked(\"user\", \"pass\")\n    coVerify { mockRepository.login(\"user\", \"pass\") }\n}\n```\n\n**Key Rules**:\n- Test behavior, not implementation\n- Use `runTest` for coroutine tests\n- Use Turbine for Flow testing\n- Use MockK for mocking\n\n---\n\n## Repository Tests\n\n**✅ GOOD - Tests data transformations**:\n```kotlin\n@Test\nfun `fetchItems maps API response to domain model`() = runTest {\n    // Arrange\n    val apiResponse = listOf(ApiItem(id = \"1\", name = \"Test\"))\n    coEvery { apiService.getItems() } returns apiResponse\n\n    // Act\n    val result = repository.fetchItems()\n\n    // Assert\n    assertTrue(result.isSuccess)\n    assertEquals(\n        listOf(DomainItem(id = \"1\", name = \"Test\")),\n        result.getOrThrow()\n    )\n}\n```\n\n**Key Rules**:\n- Test data transformations\n- Test error handling (network failures, API errors)\n- Test caching behavior if applicable\n- Mock API services and databases\n\nReference: Project uses JUnit 5, MockK, Turbine, kotlinx-coroutines-test\n\n---\n\n## Null Safety\n\n**✅ GOOD - Safe handling**:\n```kotlin\n// Safe call with elvis operator\nval result = apiService.getData() ?: return State.Error(\"No data\")\n\n// Let with safe call\nintent?.getStringExtra(\"key\")?.let { value ->\n    processValue(value)\n}\n\n// Require with message\nval data = requireNotNull(response.data) { \"Response data must not be null\" }\n```\n\n**❌ BAD - Unsafe assertions**:\n```kotlin\n// ❌ Unsafe - can crash\nval result = apiService.getData()!!\n\n// ❌ Platform type unchecked\nval intent: Intent = getIntent()  // Could be null from Java\nval value = intent.getStringExtra(\"key\")  // Potential NPE\n```\n\n**Key Rules**:\n- Avoid `!!` unless safety is guaranteed (rare)\n- Handle platform types with explicit nullability\n- Use safe calls (`?.`), elvis operator (`?:`), or explicit checks\n- Use `requireNotNull` with descriptive message if crash is acceptable\n\n---\n\n## Quick Checklist\n\n### Testing\n- [ ] ViewModels have unit tests?\n- [ ] Tests verify behavior, not implementation?\n- [ ] Edge cases covered?\n- [ ] Error scenarios tested?\n\n### Code Quality\n- [ ] Null safety handled properly (no `!!` without guarantee)?\n- [ ] Public APIs have KDoc?\n- [ ] Following naming conventions?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n\n<!-- File: .claude/skills/reviewing-changes/reference/ui-patterns.md (Tokens: ~550 | Category: anthropic-skill) -->\n# Compose UI Patterns Quick Reference\n\nQuick reference for Bitwarden Android Compose UI patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.\n\n## Component Reuse\n\n**✅ GOOD - Uses existing components**:\n```kotlin\nBitwardenButton(\n    text = \"Submit\",\n    onClick = onSubmit\n)\n\nBitwardenTextField(\n    value = text,\n    onValueChange = onTextChange,\n    label = \"Email\"\n)\n```\n\n**❌ BAD - Duplicates existing components**:\n```kotlin\n// ❌ Recreating BitwardenButton\nButton(\n    onClick = onSubmit,\n    colors = ButtonDefaults.buttonColors(\n        containerColor = BitwardenTheme.colorScheme.primary\n    )\n) {\n    Text(\"Submit\")\n}\n```\n\n**Key Rules**:\n- Check `:ui` module for existing components before creating custom ones\n- Use BitwardenButton, BitwardenTextField, etc. for consistency\n- Place new reusable components in `:ui` module\n\n---\n\n## Theme Usage\n\n**✅ GOOD - Uses theme**:\n```kotlin\nText(\n    text = \"Title\",\n    style = BitwardenTheme.typography.titleLarge,\n    color = BitwardenTheme.colorScheme.primary\n)\n\nSpacer(modifier = Modifier.height(16.dp))  // Standard spacing\n```\n\n**❌ BAD - Hardcoded values**:\n```kotlin\nText(\n    text = \"Title\",\n    style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold),  // Use theme\n    color = Color(0xFF0066FF)  // Use theme color\n)\n\nSpacer(modifier = Modifier.height(17.dp))  // Non-standard spacing\n```\n\n**Key Rules**:\n- Use `BitwardenTheme.colorScheme` for colors\n- Use `BitwardenTheme.typography` for text styles\n- Use standard spacing (4.dp, 8.dp, 16.dp, 24.dp)\n\n---\n\n## Quick Checklist\n\n### UI Patterns\n- [ ] Using existing Bitwarden components from `:ui` module?\n- [ ] Using BitwardenTheme for colors and typography?\n- [ ] Using standard spacing values (4, 8, 16, 24 dp)?\n- [ ] No hardcoded colors or text styles?\n- [ ] UI is stateless (observes state, doesn't modify)?\n\n---\n\nFor comprehensive details, always refer to:\n- `docs/ARCHITECTURE.md` - Full architecture patterns\n- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide\n\n<!-- File: .claude/skills/reviewing-changes/SKILL.md (Tokens: ~1059 | Category: anthropic-skill) -->\n---\nname: reviewing-changes\ndescription: Android-specific code review checklist and MVVM/Compose pattern validation for Bitwarden Android — use this for any review task, even if the user doesn't explicitly ask for a \"checklist\". Detects change type automatically and loads the right review strategy (feature additions, bug fixes, UI refinements, refactoring, dependency updates, infrastructure). Triggered by \"review PR\", \"review changes\", \"review this code\", \"check this code\", \"Android review\", code review requests on Kotlin/ViewModel/Composable/Repository/Gradle files, or any time someone asks to look at a diff, PR, or code changes in bitwarden/android.\n---\n\n# Reviewing Changes - Android Additions\n\nThis skill provides Android-specific workflow additions that complement the base `bitwarden-code-reviewer` agent standards.\n\n## Instructions\n\n**IMPORTANT**: Work systematically through each step before providing feedback. Each checklist file includes structured thinking guidance for its review passes.\n\n### Step 1: Retrieve Additional Details\n\nRetrieve any additional information linked to the pull request using available tools (JIRA MCP, GitHub API).\n\nIf pull request title and message do not provide enough context, request additional details from the reviewer:\n- Link a JIRA ticket\n- Associate a GitHub issue\n- Link to another pull request\n- Add more detail to the PR title or body\n\n**Android metadata checks** — flag as ❓ if any of these are missing:\n- PR includes `*Screen.kt` or Composable changes but has no screenshots\n- PR adds new `ViewModel` or `Repository` but has no test plan or test file changes\n\n### Step 2: Detect Change Type with Android Refinements\n\nUse the base change type detection from the agent, with Android-specific refinements:\n\n**Android-specific patterns:**\n- **Feature Addition**: New `ViewModel`, new `Repository`, new `@Composable` functions, new `*Screen.kt` files\n- **UI Refinement**: Changes only in `*Screen.kt`, `*Composable.kt`, `ui/` package files\n- **Infrastructure**: Changes to `.github/workflows/`, `gradle/`, `build.gradle.kts`, `libs.versions.toml`\n- **Dependency Update**: Changes only to `libs.versions.toml` or `build.gradle.kts` with version bumps\n\n### Step 3: Load Appropriate Checklist\n\nBased on detected type, read the relevant checklist file:\n\n- **Dependency Update** → `checklists/dependency-update.md` (expedited review)\n- **Bug Fix** → `checklists/bug-fix.md` (focused review)\n- **Feature Addition** → `checklists/feature-addition.md` (comprehensive review)\n- **UI Refinement** → `checklists/ui-refinement.md` (design-focused review)\n- **Refactoring** → `checklists/refactoring.md` (pattern-focused review)\n- **Infrastructure** → `checklists/infrastructure.md` (tooling-focused review)\n\nThe checklist provides:\n- Multi-pass review strategy\n- Type-specific focus areas\n- What to check and what to skip\n- Structured thinking guidance\n\n### Step 4: Execute Review Following Checklist\n\nFollow the checklist's multi-pass strategy, thinking through each pass systematically.\n\n### Step 5: Consult Android Reference Materials As Needed\n\nLoad reference files only when needed for specific questions:\n\n- **Re-reviews** → invoke `reviewing-incremental-changes` agent skill; scope to changed lines only, do not flag new issues in unchanged code\n- **Issue prioritization** → `reference/priority-framework.md` (Critical vs Suggested vs Optional)\n- **Phrasing feedback** → `reference/review-psychology.md` (questions vs commands, I-statements)\n- **Architecture questions** → `reference/architectural-patterns.md` (MVVM, Hilt DI, module org, error handling)\n- **Security questions (quick reference)** → `reference/security-patterns.md` (common patterns and anti-patterns)\n- **Security questions (comprehensive)** → `docs/ARCHITECTURE.md#security` (full zero-knowledge architecture)\n- **Testing questions** → `reference/testing-patterns.md` (unit tests, mocking, null safety)\n- **UI questions** → `reference/ui-patterns.md` (Compose patterns, theming)\n- **Style questions (project-specific)** → `reference/style-patterns.md` (Kotlin rules enforced in review)\n- **Style questions (general)** → `docs/STYLE_AND_BEST_PRACTICES.md`\n\n## Core Principles\n\n- **Priority order**: Security → Correctness → Breaking Changes → Performance → Maintainability\n- **Appropriate depth**: Match review rigor to change complexity and risk\n- **Specific references**: Always use `file:line_number` format for precise location\n- **Actionable feedback**: Say what to do and why, not just what's wrong\n- **Efficient reviews**: Use multi-pass strategy, skip what's not relevant\n- **Android patterns**: Validate MVVM, Hilt DI, Compose conventions, Kotlin idioms\n\n<!-- File: .claude/skills/testing-android-code/examples/compose-screen-test-example.md (Tokens: ~2128 | Category: anthropic-skill) -->\n/**\n * Complete Compose Screen Test Example\n *\n * Key patterns demonstrated:\n * - Extending BitwardenComposeTest\n * - Mocking ViewModel with flows\n * - Testing UI interactions\n * - Testing navigation callbacks\n * - Using bufferedMutableSharedFlow for events\n * - Testing dialogs with isDialog() and hasAnyAncestor()\n */\npackage com.bitwarden.example.feature\n\nimport androidx.compose.ui.test.assertIsDisplayed\nimport androidx.compose.ui.test.filterToOne\nimport androidx.compose.ui.test.hasAnyAncestor\nimport androidx.compose.ui.test.isDialog\nimport androidx.compose.ui.test.onAllNodesWithText\nimport androidx.compose.ui.test.onNodeWithContentDescription\nimport androidx.compose.ui.test.onNodeWithText\nimport androidx.compose.ui.test.performClick\nimport com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow\nimport com.bitwarden.ui.util.assertNoDialogExists\nimport com.bitwarden.ui.util.isProgressBar\nimport com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.verify\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport junit.framework.TestCase.assertTrue\nimport org.junit.Before\nimport org.junit.Test\n\nclass ExampleScreenTest : BitwardenComposeTest() {\n\n    // Track navigation callbacks\n    private var haveCalledNavigateBack = false\n    private var haveCalledNavigateToNext = false\n\n    // Use bufferedMutableSharedFlow for events (default replay = 0)\n    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n\n    // Mock ViewModel with relaxed = true\n    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        haveCalledNavigateBack = false\n        haveCalledNavigateToNext = false\n\n        setContent {\n            ExampleScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                onNavigateToNext = { haveCalledNavigateToNext = true },\n                viewModel = viewModel,\n            )\n        }\n    }\n\n    /**\n     * Test: Back button sends action to ViewModel\n     */\n    @Test\n    fun `on back click should send BackClick action`() {\n        composeTestRule\n            .onNodeWithContentDescription(\"Back\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.BackClick) }\n    }\n\n    /**\n     * Test: Submit button sends action to ViewModel\n     */\n    @Test\n    fun `on submit click should send SubmitClick action`() {\n        composeTestRule\n            .onNodeWithText(\"Submit\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.SubmitClick) }\n    }\n\n    /**\n     * Test: Loading state shows progress indicator\n     */\n    @Test\n    fun `loading state should display progress indicator`() {\n        mutableStateFlow.update { it.copy(isLoading = true) }\n\n        composeTestRule\n            .onNode(isProgressBar)\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Data state shows content\n     */\n    @Test\n    fun `data state should display content`() {\n        mutableStateFlow.update { it.copy(data = \"Test Data\") }\n\n        composeTestRule\n            .onNodeWithText(\"Test Data\")\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Error state shows error message\n     */\n    @Test\n    fun `error state should display error message`() {\n        mutableStateFlow.update { it.copy(errorMessage = \"Something went wrong\") }\n\n        composeTestRule\n            .onNodeWithText(\"Something went wrong\")\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: NavigateBack event triggers navigation callback\n     */\n    @Test\n    fun `NavigateBack event should call onNavigateBack`() {\n        mutableEventFlow.tryEmit(ExampleEvent.NavigateBack)\n\n        assertTrue(haveCalledNavigateBack)\n    }\n\n    /**\n     * Test: NavigateToNext event triggers navigation callback\n     */\n    @Test\n    fun `NavigateToNext event should call onNavigateToNext`() {\n        mutableEventFlow.tryEmit(ExampleEvent.NavigateToNext)\n\n        assertTrue(haveCalledNavigateToNext)\n    }\n\n    /**\n     * Test: Item in list can be clicked\n     */\n    @Test\n    fun `on item click should send ItemClick action`() {\n        val itemId = \"item-123\"\n        mutableStateFlow.update {\n            it.copy(items = listOf(ExampleItem(id = itemId, name = \"Test Item\")))\n        }\n\n        composeTestRule\n            .onNodeWithText(\"Test Item\")\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.ItemClick(itemId)) }\n    }\n\n    // ==================== DIALOG TESTS ====================\n\n    /**\n     * Test: No dialog exists when dialogState is null\n     */\n    @Test\n    fun `no dialog should exist when dialogState is null`() {\n        mutableStateFlow.update { it.copy(dialogState = null) }\n\n        composeTestRule.assertNoDialogExists()\n    }\n\n    /**\n     * Test: Loading dialog displays when state updates\n     * PATTERN: Use isDialog() to check dialog exists\n     */\n    @Test\n    fun `loading dialog should display when dialogState is Loading`() {\n        mutableStateFlow.update {\n            it.copy(dialogState = ExampleState.DialogState.Loading(\"Please wait...\"))\n        }\n\n        composeTestRule\n            .onNode(isDialog())\n            .assertIsDisplayed()\n\n        // Verify loading text within dialog using hasAnyAncestor(isDialog())\n        composeTestRule\n            .onAllNodesWithText(\"Please wait...\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Error dialog displays title and message\n     * PATTERN: Use filterToOne(hasAnyAncestor(isDialog())) to find text within dialogs\n     */\n    @Test\n    fun `error dialog should display title and message`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Error(\n                    title = \"An error has occurred\",\n                    message = \"Something went wrong. Please try again.\",\n                ),\n            )\n        }\n\n        // Verify dialog exists\n        composeTestRule\n            .onNode(isDialog())\n            .assertIsDisplayed()\n\n        // Verify title within dialog\n        composeTestRule\n            .onAllNodesWithText(\"An error has occurred\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n\n        // Verify message within dialog\n        composeTestRule\n            .onAllNodesWithText(\"Something went wrong. Please try again.\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .assertIsDisplayed()\n    }\n\n    /**\n     * Test: Dialog button click sends action\n     * PATTERN: Find button with hasAnyAncestor(isDialog()) then performClick()\n     */\n    @Test\n    fun `error dialog dismiss button should send DismissDialog action`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Error(\n                    title = \"Error\",\n                    message = \"An error occurred\",\n                ),\n            )\n        }\n\n        // Click dismiss button within dialog\n        composeTestRule\n            .onAllNodesWithText(\"Ok\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.DismissDialog) }\n    }\n\n    /**\n     * Test: Confirmation dialog with multiple buttons\n     * PATTERN: Test both confirm and cancel actions\n     */\n    @Test\n    fun `confirmation dialog confirm button should send ConfirmAction`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Confirmation(\n                    title = \"Confirm Action\",\n                    message = \"Are you sure you want to proceed?\",\n                ),\n            )\n        }\n\n        // Click confirm button\n        composeTestRule\n            .onAllNodesWithText(\"Confirm\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.ConfirmAction) }\n    }\n\n    @Test\n    fun `confirmation dialog cancel button should send DismissDialog action`() {\n        mutableStateFlow.update {\n            it.copy(\n                dialogState = ExampleState.DialogState.Confirmation(\n                    title = \"Confirm Action\",\n                    message = \"Are you sure?\",\n                ),\n            )\n        }\n\n        // Click cancel button\n        composeTestRule\n            .onAllNodesWithText(\"Cancel\")\n            .filterToOne(hasAnyAncestor(isDialog()))\n            .performClick()\n\n        verify { viewModel.trySendAction(ExampleAction.DismissDialog) }\n    }\n}\n\nprivate val DEFAULT_STATE = ExampleState(\n    isLoading = false,\n    data = null,\n    errorMessage = null,\n    items = emptyList(),\n    dialogState = null,\n)\n\n// Example types (normally in separate files)\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    val errorMessage: String? = null,\n    val items: List<ExampleItem> = emptyList(),\n    val dialogState: DialogState? = null,\n) {\n    /**\n     * PATTERN: Nested sealed class for dialog states.\n     * Common dialog types: Loading, Error, Confirmation\n     */\n    sealed class DialogState {\n        data class Loading(val message: String) : DialogState()\n        data class Error(val title: String, val message: String) : DialogState()\n        data class Confirmation(val title: String, val message: String) : DialogState()\n    }\n}\n\ndata class ExampleItem(val id: String, val name: String)\n\nsealed class ExampleAction {\n    data object BackClick : ExampleAction()\n    data object SubmitClick : ExampleAction()\n    data class ItemClick(val itemId: String) : ExampleAction()\n    data object DismissDialog : ExampleAction()\n    data object ConfirmAction : ExampleAction()\n}\n\nsealed class ExampleEvent {\n    data object NavigateBack : ExampleEvent()\n    data object NavigateToNext : ExampleEvent()\n}\n\n<!-- File: .claude/skills/testing-android-code/examples/repository-test-example.md (Tokens: ~1760 | Category: anthropic-skill) -->\n/**\n * Complete Repository Test Example\n *\n * Key patterns demonstrated:\n * - Fake for disk sources, Mock for network services\n * - Using FakeDispatcherManager for deterministic coroutines\n * - Using fixed Clock for deterministic time\n * - Testing Result types with .asSuccess() / .asFailure()\n * - Asserting actual objects (not isSuccess/isFailure) for better diagnostics\n * - Testing Flow emissions with Turbine\n */\npackage com.bitwarden.example.data.repository\n\nimport app.cash.turbine.test\nimport com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager\nimport com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow\nimport com.bitwarden.core.data.util.asFailure\nimport com.bitwarden.core.data.util.asSuccess\nimport io.mockk.coEvery\nimport io.mockk.coVerify\nimport io.mockk.mockk\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.onSubscription\nimport kotlinx.coroutines.test.runTest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Assertions.assertNull\nimport org.junit.jupiter.api.Assertions.assertTrue\nimport org.junit.jupiter.api.BeforeEach\nimport org.junit.jupiter.api.Test\nimport java.time.Clock\nimport java.time.Instant\nimport java.time.ZoneOffset\n\nclass ExampleRepositoryTest {\n\n    // Fixed clock for deterministic time-based tests\n    private val fixedClock: Clock = Clock.fixed(\n        Instant.parse(\"2023-10-27T12:00:00Z\"),\n        ZoneOffset.UTC,\n    )\n\n    // Use FakeDispatcherManager for deterministic coroutine execution\n    private val dispatcherManager = FakeDispatcherManager()\n\n    // Mock service (network layer is always mocked)\n    private val mockService: ExampleService = mockk()\n\n    /**\n     * PATTERN: Use Fake for disk source in happy path tests.\n     * This is the Bitwarden convention for repository testing.\n     */\n    private val fakeDiskSource = FakeExampleDiskSource()\n\n    private lateinit var repository: ExampleRepositoryImpl\n\n    @BeforeEach\n    fun setup() {\n        repository = ExampleRepositoryImpl(\n            clock = fixedClock,\n            service = mockService,\n            diskSource = fakeDiskSource,\n            dispatcherManager = dispatcherManager,\n        )\n    }\n\n    // ==================== HAPPY PATH TESTS (use Fake) ====================\n\n    /**\n     * Test: Successful fetch returns data and saves to disk\n     */\n    @Test\n    fun `fetchData should return success and save to disk when service succeeds`() = runTest {\n        val expectedData = ExampleData(id = \"1\", name = \"Test\", updatedAt = fixedClock.instant())\n        coEvery { mockService.getData() } returns expectedData.asSuccess()\n\n        val result = repository.fetchData()\n\n        assertEquals(expectedData, result.getOrThrow())\n        // Fake automatically stores the data - verify it's there\n        assertEquals(expectedData, fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Service failure returns failure without saving\n     */\n    @Test\n    fun `fetchData should return failure when service fails`() = runTest {\n        val exception = Exception(\"Network error\")\n        coEvery { mockService.getData() } returns exception.asFailure()\n\n        val result = repository.fetchData()\n\n        assertEquals(exception, result.exceptionOrNull())\n        // Fake was not updated\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Repository flow emits when disk source updates\n     */\n    @Test\n    fun `dataFlow should emit when disk source updates`() = runTest {\n        val data1 = ExampleData(id = \"1\", name = \"First\", updatedAt = fixedClock.instant())\n        val data2 = ExampleData(id = \"2\", name = \"Second\", updatedAt = fixedClock.instant())\n\n        repository.dataFlow.test {\n            // Initial null value from Fake\n            assertNull(awaitItem())\n\n            // Update via Fake property setter (triggers emission)\n            fakeDiskSource.storedData = data1\n            assertEquals(data1, awaitItem())\n\n            // Another update\n            fakeDiskSource.storedData = data2\n            assertEquals(data2, awaitItem())\n        }\n    }\n\n    /**\n     * Test: Refresh fetches and saves new data\n     */\n    @Test\n    fun `refresh should fetch new data and update disk source`() = runTest {\n        val newData = ExampleData(id = \"new\", name = \"Fresh\", updatedAt = fixedClock.instant())\n        coEvery { mockService.getData() } returns newData.asSuccess()\n\n        val result = repository.refresh()\n\n        assertEquals(Unit, result.getOrThrow())\n        coVerify { mockService.getData() }\n        assertEquals(newData, fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Delete clears data from disk\n     */\n    @Test\n    fun `deleteData should clear disk source`() = runTest {\n        // Pre-populate the fake\n        fakeDiskSource.storedData = ExampleData(id = \"1\", name = \"Test\", updatedAt = fixedClock.instant())\n\n        repository.deleteData()\n\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    /**\n     * Test: Cached data returns from disk when available\n     */\n    @Test\n    fun `getCachedData should return disk data without network call`() = runTest {\n        val cachedData = ExampleData(\n            id = \"cached\",\n            name = \"Cached\",\n            updatedAt = fixedClock.instant(),\n        )\n        fakeDiskSource.storedData = cachedData\n\n        val result = repository.getCachedData()\n\n        assertEquals(cachedData, result)\n        coVerify(exactly = 0) { mockService.getData() }\n    }\n\n    // ==================== ERROR PATH TESTS ====================\n\n    /**\n     * PATTERN: For error paths, reconfigure the class-level mock per-test.\n     * Use coEvery to change mock behavior for each specific test case.\n     */\n    @Test\n    fun `fetchData should return failure when service returns error`() = runTest {\n        val exception = Exception(\"Server unavailable\")\n        coEvery { mockService.getData() } returns exception.asFailure()\n\n        val result = repository.fetchData()\n\n        assertEquals(exception, result.exceptionOrNull())\n        // Fake state unchanged on failure\n        assertNull(fakeDiskSource.storedData)\n    }\n\n    @Test\n    fun `refresh should return failure and preserve cached data when service fails`() = runTest {\n        // Pre-populate cache via Fake\n        val cachedData = ExampleData(id = \"cached\", name = \"Old\", updatedAt = fixedClock.instant())\n        fakeDiskSource.storedData = cachedData\n\n        // Reconfigure mock to return failure\n        coEvery { mockService.getData() } returns Exception(\"Network error\").asFailure()\n\n        val result = repository.refresh()\n\n        assertTrue(result.isFailure)\n        // Cached data preserved on failure\n        assertEquals(cachedData, fakeDiskSource.storedData)\n    }\n}\n\n// Example types (normally in separate files)\ndata class ExampleData(\n    val id: String,\n    val name: String,\n    val updatedAt: Instant,\n)\n\ninterface ExampleService {\n    suspend fun getData(): Result<ExampleData>\n}\n\ninterface ExampleDiskSource {\n    val dataFlow: kotlinx.coroutines.flow.Flow<ExampleData?>\n    fun getData(): ExampleData?\n    fun saveData(data: ExampleData)\n    fun clearData()\n}\n\n/**\n * PATTERN: Fake implementation for happy path testing.\n *\n * Key characteristics:\n * - Uses bufferedMutableSharedFlow(replay = 1) for proper replay behavior\n * - Uses .onSubscription { emit(state) } for immediate state emission\n * - Private storage with override property setter that emits to flow\n * - Test assertions done via the override property getter\n */\nclass FakeExampleDiskSource : ExampleDiskSource {\n    private var storedDataValue: ExampleData? = null\n    private val mutableDataFlow = bufferedMutableSharedFlow<ExampleData?>(replay = 1)\n\n    /**\n     * Override property with getter/setter. Setter emits to flow automatically.\n     * Tests can read this property for assertions and write to trigger emissions.\n     */\n    var storedData: ExampleData?\n        get() = storedDataValue\n        set(value) {\n            storedDataValue = value\n            mutableDataFlow.tryEmit(value)\n        }\n\n    override val dataFlow: Flow<ExampleData?>\n        get() = mutableDataFlow.onSubscription { emit(storedData) }\n\n    override fun getData(): ExampleData? = storedData\n\n    override fun saveData(data: ExampleData) {\n        storedData = data\n    }\n\n    override fun clearData() {\n        storedData = null\n    }\n}\n\n<!-- File: .claude/skills/testing-android-code/examples/viewmodel-test-example.md (Tokens: ~1043 | Category: anthropic-skill) -->\n/**\n * Complete ViewModel Test Example\n *\n * Key patterns demonstrated:\n * - Extending BaseViewModelTest\n * - Testing StateFlow with Turbine\n * - Testing EventFlow with Turbine\n * - Using stateEventFlow() for simultaneous testing\n * - MockK mocking patterns\n * - Test factory method design (accepts domain state, not SavedStateHandle)\n * - Complete state assertions (assert entire state objects)\n */\npackage com.bitwarden.example.feature\n\nimport androidx.lifecycle.SavedStateHandle\nimport app.cash.turbine.test\nimport com.bitwarden.ui.platform.base.BaseViewModelTest\nimport io.mockk.coEvery\nimport io.mockk.coVerify\nimport io.mockk.every\nimport io.mockk.mockk\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.test.runTest\nimport org.junit.jupiter.api.Assertions.assertEquals\nimport org.junit.jupiter.api.Test\n\nclass ExampleViewModelTest : BaseViewModelTest() {\n\n    // Mock dependencies\n    private val mockRepository: ExampleRepository = mockk()\n    private val mockAuthDiskSource: AuthDiskSource = mockk {\n        every { userStateFlow } returns MutableStateFlow(null)\n    }\n\n    /**\n     * StateFlow has replay=1, so first awaitItem() returns current state\n     */\n    @Test\n    fun `initial state should be default state`() = runTest {\n        val viewModel = createViewModel()\n\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n        }\n    }\n\n    /**\n     * Test state transitions: initial -> loading -> success\n     */\n    @Test\n    fun `LoadData action should update state from idle to loading to success`() = runTest {\n        val expectedData = \"loaded data\"\n        coEvery { mockRepository.fetchData(any()) } returns Result.success(expectedData)\n\n        val viewModel = createViewModel()\n\n        viewModel.stateFlow.test {\n            assertEquals(DEFAULT_STATE, awaitItem())\n\n            viewModel.trySendAction(ExampleAction.LoadData)\n\n            assertEquals(DEFAULT_STATE.copy(isLoading = true), awaitItem())\n            assertEquals(DEFAULT_STATE.copy(isLoading = false, data = expectedData), awaitItem())\n        }\n\n        coVerify { mockRepository.fetchData(any()) }\n    }\n\n    /**\n     * EventFlow has no replay - MUST call expectNoEvents() first\n     */\n    @Test\n    fun `SubmitClick action should emit NavigateToNext event`() = runTest {\n        coEvery { mockRepository.submitData(any()) } returns Result.success(Unit)\n\n        val viewModel = createViewModel()\n\n        viewModel.eventFlow.test {\n            expectNoEvents() // CRITICAL for EventFlow\n            viewModel.trySendAction(ExampleAction.SubmitClick)\n            assertEquals(ExampleEvent.NavigateToNext, awaitItem())\n        }\n    }\n\n    /**\n     * Use stateEventFlow() helper for simultaneous testing\n     */\n    @Test\n    fun `complex action should update state and emit event`() = runTest {\n        coEvery { mockRepository.complexOperation(any()) } returns Result.success(\"result\")\n\n        val viewModel = createViewModel()\n\n        viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n            assertEquals(DEFAULT_STATE, stateFlow.awaitItem())\n            eventFlow.expectNoEvents()\n\n            viewModel.trySendAction(ExampleAction.ComplexAction)\n\n            assertEquals(DEFAULT_STATE.copy(isLoading = true), stateFlow.awaitItem())\n            assertEquals(DEFAULT_STATE.copy(data = \"result\"), stateFlow.awaitItem())\n            assertEquals(ExampleEvent.ShowToast(\"Success!\"), eventFlow.awaitItem())\n        }\n    }\n\n    /**\n     * Test state restoration from saved state.\n     * Note: Use initialState parameter, NOT SavedStateHandle directly.\n     */\n    @Test\n    fun `initial state from saved state should be preserved`() = runTest {\n        // Build complete expected state - always assert full objects\n        val savedState = ExampleState(\n            isLoading = false,\n            data = \"restored data\",\n            errorMessage = null,\n        )\n\n        val viewModel = createViewModel(initialState = savedState)\n\n        viewModel.stateFlow.test {\n            assertEquals(savedState, awaitItem())\n        }\n    }\n\n    /**\n     * Factory method accepts domain state, NOT SavedStateHandle.\n     * This hides Android framework details from test logic.\n     */\n    private fun createViewModel(\n        initialState: ExampleState? = null,\n    ): ExampleViewModel = ExampleViewModel(\n        savedStateHandle = SavedStateHandle().apply { set(\"state\", initialState) },\n        repository = mockRepository,\n        authDiskSource = mockAuthDiskSource,\n    )\n}\n\nprivate val DEFAULT_STATE = ExampleState(\n    isLoading = false,\n    data = null,\n    errorMessage = null,\n)\n\n// Example types (normally in separate files)\ndata class ExampleState(\n    val isLoading: Boolean = false,\n    val data: String? = null,\n    val errorMessage: String? = null,\n)\n\nsealed class ExampleAction {\n    data object LoadData : ExampleAction()\n    data object SubmitClick : ExampleAction()\n    data object ComplexAction : ExampleAction()\n}\n\nsealed class ExampleEvent {\n    data object NavigateToNext : ExampleEvent()\n    data class ShowToast(val message: String) : ExampleEvent()\n}\n\n<!-- File: .claude/skills/testing-android-code/references/critical-gotchas.md (Tokens: ~3825 | Category: anthropic-skill) -->\n# Critical Gotchas and Anti-Patterns\n\nCommon mistakes and pitfalls when writing tests in the Bitwarden Android codebase.\n\n## ❌ NEVER wrap assertCoroutineThrows in runTest\n\n### The Problem\n\n`runTest` catches exceptions and rethrows them, which breaks the `assertCoroutineThrows` assertion pattern.\n\n### Wrong\n\n```kotlin\n@Test\nfun `test exception`() = runTest {\n    assertCoroutineThrows<Exception> {\n        repository.throwingFunction()\n    }  // Won't work - exception is caught by runTest!\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `test exception`() {\n    assertCoroutineThrows<Exception> {\n        repository.throwingFunction()\n    }  // Works correctly\n}\n```\n\n### Why This Happens\n\n`runTest` provides a coroutine scope and catches exceptions to provide better error messages. However, `assertCoroutineThrows` needs to catch the exception itself to verify it was thrown. When wrapped in `runTest`, the exception is caught twice, breaking the assertion.\n\n## ❌ ALWAYS unmock static functions\n\n### The Problem\n\nMockK's static mocking persists across tests. Forgetting to clean up causes mysterious failures in subsequent tests.\n\n### Wrong\n\n```kotlin\n@Before\nfun setup() {\n    mockkStatic(::isBuildVersionAtLeast)\n    every { isBuildVersionAtLeast(any()) } returns true\n}\n\n// Forgot @After - subsequent tests will fail mysteriously!\n```\n\n### Correct\n\n```kotlin\n@Before\nfun setup() {\n    mockkStatic(::isBuildVersionAtLeast)\n    every { isBuildVersionAtLeast(any()) } returns true\n}\n\n@After\nfun tearDown() {\n    unmockkStatic(::isBuildVersionAtLeast)  // CRITICAL\n}\n```\n\n### Common Static Functions to Watch\n\n```kotlin\n// Platform version checks\nmockkStatic(::isBuildVersionAtLeast)\nunmockkStatic(::isBuildVersionAtLeast)\n\n// URI parsing\nmockkStatic(Uri::class)\nunmockkStatic(Uri::class)\n\n// Static utility functions\nmockkStatic(MyUtilClass::class)\nunmockkStatic(MyUtilClass::class)\n```\n\n### Debugging Tip\n\nIf tests pass individually but fail when run together, suspect static mocking cleanup issues.\n\n## ❌ Don't confuse StateFlow and EventFlow testing\n\n### StateFlow (replay = 1)\n\n```kotlin\n// CORRECT - StateFlow always has current value\nviewModel.stateFlow.test {\n    val initial = awaitItem()  // Gets current state immediately\n    viewModel.trySendAction(action)\n    val updated = awaitItem()  // Gets new state\n}\n```\n\n### EventFlow (no replay)\n\n```kotlin\n// CORRECT - EventFlow has no initial value\nviewModel.eventFlow.test {\n    expectNoEvents()  // MUST do this first\n    viewModel.trySendAction(action)\n    val event = awaitItem()  // Gets emitted event\n}\n```\n\n### Common Mistake\n\n```kotlin\n// WRONG - Forgetting expectNoEvents() on EventFlow\nviewModel.eventFlow.test {\n    viewModel.trySendAction(action)  // May cause flaky tests\n    assertEquals(event, awaitItem())\n}\n```\n\n## ❌ Don't mix real and test dispatchers\n\n### Wrong\n\n```kotlin\nprivate val repository = ExampleRepositoryImpl(\n    dispatcherManager = DispatcherManagerImpl(),  // Real dispatcher!\n)\n\n@Test\nfun `test repository`() = runTest {\n    // Test will have timing issues - real dispatcher != test dispatcher\n}\n```\n\n### Correct\n\n```kotlin\nprivate val repository = ExampleRepositoryImpl(\n    dispatcherManager = FakeDispatcherManager(),  // Test dispatcher\n)\n\n@Test\nfun `test repository`() = runTest {\n    // Test runs deterministically\n}\n```\n\n### Why This Matters\n\nReal dispatchers use actual thread pools and delays. Test dispatchers (UnconfinedTestDispatcher) execute immediately and deterministically. Mixing them causes:\n- Non-deterministic test failures\n- Real delays in tests (slow test suite)\n- Race conditions\n\n### Always Use\n\n- `FakeDispatcherManager()` for repositories\n- `UnconfinedTestDispatcher()` when manually creating dispatchers\n- `runTest` for coroutine tests (provides TestDispatcher automatically)\n\n## ❌ Don't forget to use runTest for coroutine tests\n\n### Wrong\n\n```kotlin\n@Test\nfun `test coroutine`() {\n    viewModel.stateFlow.test { /* ... */ }  // Missing runTest!\n}\n```\n\nThis causes:\n- Test completes before coroutines finish\n- False positives (test passes but assertions never run)\n- Mysterious failures\n\n### Correct\n\n```kotlin\n@Test\nfun `test coroutine`() = runTest {\n    viewModel.stateFlow.test { /* ... */ }\n}\n```\n\n### When runTest is Required\n\n- Testing ViewModels (they use `viewModelScope`)\n- Testing Flows with Turbine `.test {}`\n- Testing repositories with suspend functions\n- Any test calling suspend functions\n\n### Exception: assertCoroutineThrows\n\nAs noted above, `assertCoroutineThrows` should NOT be wrapped in `runTest`.\n\n## ❌ Don't forget relaxed = true for complex mocks\n\n### Without relaxed\n\n```kotlin\nprivate val viewModel = mockk<ExampleViewModel>()  // Must mock every method!\n\n// Error: \"no answer found for: stateFlow\"\n```\n\n### With relaxed\n\n```kotlin\nprivate val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n    // Only mock what you care about\n    every { stateFlow } returns mutableStateFlow\n    every { eventFlow } returns mutableEventFlow\n}\n```\n\n### When to Use relaxed\n\n- Mocking ViewModels in Compose tests\n- Mocking complex objects with many methods\n- When you only care about specific method calls\n\n### When NOT to Use relaxed\n\n- Mocking repository interfaces (be explicit about behavior)\n- When you want to verify NO unexpected calls\n- Testing error paths (want test to fail if unexpected method called)\n\n## ❌ Don't assert individual fields when complete state is available\n\n### The Problem\n\nAsserting individual state fields can miss unintended side effects on other fields.\n\n### Wrong\n\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    viewModel.trySendAction(SomeAction.DoThing)\n\n    val state = viewModel.stateFlow.value\n    assertEquals(null, state.dialog)  // Only checks one field!\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    viewModel.trySendAction(SomeAction.DoThing)\n\n    val expected = SomeState(\n        isLoading = false,\n        data = \"result\",\n        dialog = null,\n    )\n    assertEquals(expected, viewModel.stateFlow.value)  // Checks all fields\n}\n```\n\n### Why This Matters\n\n- Catches unintended mutations to other state fields\n- Makes expected state explicit and readable\n- Prevents silent regressions when state structure changes\n\n---\n\n## ❌ Don't use Kotlin assert() for boolean checks\n\n### The Problem\n\nKotlin's `assert()` doesn't follow JUnit conventions and provides poor failure messages.\n\n### Wrong\n\n```kotlin\n@Test\nfun `event should trigger callback`() {\n    mutableEventFlow.tryEmit(SomeEvent.Navigate)\n\n    assert(onNavigateCalled)  // Kotlin assert - bad failure messages\n}\n```\n\n### Correct\n\n```kotlin\n@Test\nfun `event should trigger callback`() {\n    mutableEventFlow.tryEmit(SomeEvent.Navigate)\n\n    assertTrue(onNavigateCalled)  // JUnit assertTrue - proper assertion\n}\n```\n\n### Always Use JUnit Assertions\n\n- `assertTrue()` / `assertFalse()` for booleans\n- `assertEquals()` for value comparisons\n- `assertNotNull()` / `assertNull()` for nullability\n- `assertThrows<T>()` for exceptions\n\n---\n\n## ❌ Don't pass SavedStateHandle to test factory methods\n\n### The Problem\n\nExposing `SavedStateHandle` in test factory methods leaks Android framework details into test logic.\n\n### Wrong\n\n```kotlin\nprivate fun createViewModel(\n    savedStateHandle: SavedStateHandle = SavedStateHandle(),  // Framework type exposed\n): MyViewModel = MyViewModel(\n    savedStateHandle = savedStateHandle,\n    repository = mockRepository,\n)\n\n@Test\nfun `initial state from saved state`() = runTest {\n    val savedState = MyState(isLoading = true)\n    val savedStateHandle = SavedStateHandle(mapOf(\"state\" to savedState))\n\n    val viewModel = createViewModel(savedStateHandle = savedStateHandle)\n    // ...\n}\n```\n\n### Correct\n\n```kotlin\nprivate fun createViewModel(\n    initialState: MyState? = null,  // Domain type only\n): MyViewModel = MyViewModel(\n    savedStateHandle = SavedStateHandle().apply { set(\"state\", initialState) },\n    repository = mockRepository,\n)\n\n@Test\nfun `initial state from saved state`() = runTest {\n    val savedState = MyState(isLoading = true)\n\n    val viewModel = createViewModel(initialState = savedState)\n    // ...\n}\n```\n\n### Why This Matters\n\n- Cleaner, more intuitive test code\n- Hides SavedStateHandle implementation details\n- Follows Bitwarden conventions\n\n---\n\n## ❌ Don't test SavedStateHandle persistence in unit tests\n\n### The Problem\n\nTesting whether state persists to SavedStateHandle is testing Android framework behavior, not your business logic.\n\n### Wrong\n\n```kotlin\n@Test\nfun `state should persist to SavedStateHandle`() = runTest {\n    val savedStateHandle = SavedStateHandle()\n    val viewModel = createViewModel(savedStateHandle = savedStateHandle)\n\n    viewModel.trySendAction(SomeAction)\n\n    val savedState = savedStateHandle.get<MyState>(\"state\")\n    assertEquals(expectedState, savedState)  // Testing framework, not logic!\n}\n```\n\n### Correct\n\nFocus on testing business logic and state transformations:\n\n```kotlin\n@Test\nfun `action should update state correctly`() = runTest {\n    val viewModel = createViewModel()\n\n    viewModel.trySendAction(SomeAction)\n\n    assertEquals(expectedState, viewModel.stateFlow.value)  // Test observable state\n}\n```\n\n---\n\n## ❌ Don't use static mocking when DI pattern is available\n\n### The Problem\n\nStatic mocking (`mockkStatic`) is harder to maintain and less testable than dependency injection.\n\n### Wrong\n\n```kotlin\nclass ParserTest {\n    @BeforeEach\n    fun setup() {\n        mockkStatic(UUID::class)\n        every { UUID.randomUUID() } returns mockk {\n            every { toString() } returns \"fixed-uuid\"\n        }\n    }\n\n    @AfterEach\n    fun tearDown() {\n        unmockkStatic(UUID::class)\n    }\n}\n```\n\n### Correct\n\nExtract an interface and inject it:\n\n```kotlin\n// Production code\ninterface UuidManager {\n    fun generateUuid(): String\n}\n\nclass UuidManagerImpl : UuidManager {\n    override fun generateUuid(): String = UUID.randomUUID().toString()\n}\n\nclass Parser(private val uuidManager: UuidManager) { ... }\n\n// Test code\nclass ParserTest {\n    private val mockUuidManager = mockk<UuidManager>()\n\n    @BeforeEach\n    fun setup() {\n        every { mockUuidManager.generateUuid() } returns \"fixed-uuid\"\n    }\n\n    // No tearDown needed - no static mocking!\n}\n```\n\n### When to Use This Pattern\n\n- UUID generation\n- Timestamp/Clock operations\n- System property access\n- Any static function that needs deterministic testing\n\n---\n\n## ❌ Don't forget to test null stream returns from Android APIs\n\n### The Problem\n\nAndroid's `ContentResolver.openOutputStream()` and `openInputStream()` can return null, not just throw exceptions.\n\n### Wrong\n\n```kotlin\nclass FileManagerTest {\n    @Test\n    fun `stringToUri with exception should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } throws IOException()\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n\n        assertFalse(result)\n    }\n    // Missing: test for null return!\n}\n```\n\n### Correct\n\n```kotlin\nclass FileManagerTest {\n    @Test\n    fun `stringToUri with exception should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } throws IOException()\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n        assertFalse(result)\n    }\n\n    @Test\n    fun `stringToUri with null stream should return false`() = runTest {\n        every { mockContentResolver.openOutputStream(any()) } returns null\n\n        val result = fileManager.stringToUri(mockUri, \"data\")\n        assertFalse(result)  // CRITICAL: must handle null!\n    }\n}\n```\n\n### Common Android APIs That Return Null\n\n- `ContentResolver.openOutputStream()` / `openInputStream()`\n- `Context.getExternalFilesDir()`\n- `PackageManager.getApplicationInfo()` (can throw)\n\n---\n\n## Bitwarden Mocking Guidelines\n\n**Mock at architectural boundaries:**\n- Repository → ViewModel (mock repository)\n- Service → Repository (mock service)\n- API → Service (use MockWebServer, not mocks)\n- DiskSource → Repository (mock disk source)\n\n**Fake vs Mock Strategy (IMPORTANT):**\n- **Happy paths**: Use Fake implementations (`FakeAuthenticatorDiskSource`, `FakeVaultDiskSource`)\n- **Error paths**: Use MockK with isolated repository instances\n\n```kotlin\n// Happy path - use Fake\nprivate val fakeDiskSource = FakeAuthenticatorDiskSource()\n\n@Test\nfun `createItem should return Success`() = runTest {\n    val result = repository.createItem(mockItem)\n    assertEquals(CreateItemResult.Success, result)\n}\n\n// Error path - use isolated Mock\n@Test\nfun `createItem with exception should return Error`() = runTest {\n    val mockDiskSource = mockk<AuthenticatorDiskSource> {\n        coEvery { saveItem(any()) } throws RuntimeException()\n    }\n    val repository = RepositoryImpl(diskSource = mockDiskSource)\n\n    val result = repository.createItem(mockItem)\n    assertEquals(CreateItemResult.Error, result)\n}\n```\n\n**Use Fakes for:**\n- `FakeDispatcherManager` - deterministic coroutines\n- `FakeConfigDiskSource` - in-memory config storage\n- `FakeSharedPreferences` - memory-backed preferences\n- `FakeAuthenticatorDiskSource` - in-memory authenticator storage\n\n**Create real instances for:**\n- Data classes, value objects (User, Config, CipherView)\n- Test data builders (`createMockCipher(number = 1)`)\n\n## ❌ Don't forget bufferedMutableSharedFlow with onSubscription for Fakes\n\n### The Problem\n\nFake data sources using `MutableSharedFlow` won't emit cached state to new subscribers without explicit handling.\n\n### Wrong\n\n```kotlin\nclass FakeDataSource : DataSource {\n    private val mutableFlow = MutableSharedFlow<List<Item>>()\n    private val storedItems = mutableListOf<Item>()\n\n    override fun getItems(): Flow<List<Item>> = mutableFlow\n\n    override suspend fun saveItem(item: Item) {\n        storedItems.add(item)\n        mutableFlow.emit(storedItems)\n    }\n}\n\n// Test: Initial collection gets nothing!\nrepository.dataFlow.test {\n    // Hangs or fails - no initial emission\n}\n```\n\n### Correct\n\n```kotlin\nclass FakeDataSource : DataSource {\n    private val mutableFlow = bufferedMutableSharedFlow<List<Item>>()\n    private val storedItems = mutableListOf<Item>()\n\n    override fun getItems(): Flow<List<Item>> = mutableFlow\n        .onSubscription { emit(storedItems.toList()) }\n\n    override suspend fun saveItem(item: Item) {\n        storedItems.add(item)\n        mutableFlow.emit(storedItems.toList())\n    }\n}\n\n// Test: Initial collection receives current state\nrepository.dataFlow.test {\n    assertEquals(emptyList(), awaitItem())  // Works!\n}\n```\n\n### Key Points\n\n- Use `bufferedMutableSharedFlow()` from `core/data/repository/util/`\n- Add `.onSubscription { emit(currentState) }` for immediate state emission\n- This ensures new collectors receive the current cached state\n\n---\n\n## ✅ Use Result extension functions for assertions\n\n### The Pattern\n\nUse `asSuccess()` and `asFailure()` extensions from `com.bitwarden.core.data.util` for cleaner Result assertions.\n\n### Success Path\n\n```kotlin\n@Test\nfun `getData should return success`() = runTest {\n    val result = repository.getData()\n    val expected = expectedData.asSuccess()\n\n    assertEquals(expected.getOrNull(), result.getOrNull())\n}\n```\n\n### Failure Path\n\n```kotlin\n@Test\nfun `getData with error should return failure`() = runTest {\n    val exception = IOException(\"Network error\")\n    coEvery { mockService.getData() } returns exception.asFailure()\n\n    val result = repository.getData()\n\n    assertTrue(result.isFailure)\n    assertEquals(exception, result.exceptionOrNull())\n}\n```\n\n### Avoid Redundant Assertions\n\n```kotlin\n// WRONG - redundant success checks\nassertTrue(result.isSuccess)\nassertTrue(expected.isSuccess)\nassertArrayEquals(expected.getOrNull(), result.getOrNull())\n\n// CORRECT - final assertion is sufficient\nassertArrayEquals(expected.getOrNull(), result.getOrNull())\n```\n\n---\n\n## Summary Checklist\n\nBefore submitting tests, verify:\n\n**Core Patterns:**\n- [ ] No `assertCoroutineThrows` inside `runTest`\n- [ ] All static mocks have `unmockk` in `@After`\n- [ ] EventFlow tests start with `expectNoEvents()`\n- [ ] Using FakeDispatcherManager, not real dispatchers\n- [ ] All coroutine tests use `runTest`\n\n**Assertion Patterns:**\n- [ ] Assert complete state objects, not individual fields\n- [ ] Use JUnit `assertTrue()`, not Kotlin `assert()`\n- [ ] Use `asSuccess()` for Result type assertions\n- [ ] Avoid redundant assertion patterns\n\n**Test Design:**\n- [ ] Test factory methods accept domain types, not SavedStateHandle\n- [ ] Use Fakes for happy paths, Mocks for error paths\n- [ ] Prefer DI patterns over static mocking\n- [ ] Test null returns from Android APIs (streams, files)\n- [ ] Fakes use `bufferedMutableSharedFlow()` with `.onSubscription`\n\n**General:**\n- [ ] Tests don't depend on execution order\n- [ ] Complex mocks use `relaxed = true`\n- [ ] Test data is created fresh for each test\n- [ ] Mocking behavior, not value objects\n- [ ] Testing observable behavior, not implementation\n\nWhen tests fail mysteriously, check these gotchas first.\n\n<!-- File: .claude/skills/testing-android-code/references/flow-testing-patterns.md (Tokens: ~1504 | Category: anthropic-skill) -->\n# Flow Testing with Turbine\n\nBitwarden Android uses Turbine for testing Kotlin Flows, including the critical distinction between StateFlow and EventFlow patterns.\n\n## StateFlow vs EventFlow\n\n### StateFlow (Replayed)\n\n**Characteristics:**\n- `replay = 1` - Always emits current value to new collectors\n- First `awaitItem()` returns the current/initial state\n- Survives configuration changes\n- Used for UI state that needs to be immediately available\n\n**Test Pattern:**\n```kotlin\n@Test\nfun `action should update state`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateFlow.test {\n        // First awaitItem() gets CURRENT state\n        assertEquals(INITIAL_STATE, awaitItem())\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.LoadData)\n\n        // Next awaitItem() gets UPDATED state\n        assertEquals(LOADING_STATE, awaitItem())\n        assertEquals(SUCCESS_STATE, awaitItem())\n    }\n}\n```\n\n### EventFlow (No Replay)\n\n**Characteristics:**\n- `replay = 0` - Only emits new events after subscription\n- No initial value emission\n- One-time events (navigation, toasts, dialogs)\n- Does not survive configuration changes\n\n**Test Pattern:**\n```kotlin\n@Test\nfun `action should emit event`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.eventFlow.test {\n        // MUST call expectNoEvents() first - nothing emitted yet\n        expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.Submit)\n\n        // Now expect the event\n        assertEquals(MyEvent.NavigateToNext, awaitItem())\n    }\n}\n```\n\n**Critical:** Always call `expectNoEvents()` before triggering actions on EventFlow. Forgetting this causes flaky tests.\n\n## Testing State and Events Simultaneously\n\nUse the `stateEventFlow()` helper from `BaseViewModelTest`:\n\n```kotlin\n@Test\nfun `complex action should update state and emit event`() = runTest {\n    val viewModel = MyViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        // Initial state\n        assertEquals(INITIAL_STATE, stateFlow.awaitItem())\n\n        // No events yet\n        eventFlow.expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(MyAction.ComplexAction)\n\n        // Verify state progression\n        assertEquals(LOADING_STATE, stateFlow.awaitItem())\n        assertEquals(SUCCESS_STATE, stateFlow.awaitItem())\n\n        // Verify event emission\n        assertEquals(MyEvent.ShowToast, eventFlow.awaitItem())\n    }\n}\n```\n\n## Repository Flow Testing\n\n### Testing Database Flows\n\n```kotlin\n@Test\nfun `dataFlow should emit when database updates`() = runTest {\n    val dataFlow = MutableStateFlow(initialData)\n    every { mockDiskSource.dataFlow } returns dataFlow\n\n    repository.dataFlow.test {\n        // Initial value\n        assertEquals(initialData, awaitItem())\n\n        // Update disk source\n        dataFlow.value = updatedData\n\n        // Verify emission\n        assertEquals(updatedData, awaitItem())\n    }\n}\n```\n\n### Testing Transformed Flows\n\n```kotlin\n@Test\nfun `flow transformation should map correctly`() = runTest {\n    val sourceFlow = MutableStateFlow(UserEntity(id = \"1\", name = \"John\"))\n    every { mockDao.observeUser() } returns sourceFlow\n\n    // Repository transforms entity to domain model\n    repository.userFlow.test {\n        val expectedUser = User(id = \"1\", name = \"John\")\n        assertEquals(expectedUser, awaitItem())\n    }\n}\n```\n\n## Common Patterns\n\n### Pattern 1: Testing Initial State + Action\n\n```kotlin\n@Test\nfun `load data should update from idle to loading to success`() = runTest {\n    coEvery { repository.getData() } returns \"data\".asSuccess()\n\n    viewModel.stateFlow.test {\n        assertEquals(DEFAULT_STATE, awaitItem())\n\n        viewModel.loadData()\n\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Success), awaitItem())\n    }\n}\n```\n\n### Pattern 2: Testing Error States\n\n```kotlin\n@Test\nfun `load data with error should emit failure state`() = runTest {\n    val error = Exception(\"Network error\")\n    coEvery { repository.getData() } returns error.asFailure()\n\n    viewModel.stateFlow.test {\n        assertEquals(DEFAULT_STATE, awaitItem())\n\n        viewModel.loadData()\n\n        assertEquals(DEFAULT_STATE.copy(loadingState = LoadingState.Loading), awaitItem())\n        assertEquals(\n            DEFAULT_STATE.copy(loadingState = LoadingState.Error(\"Network error\")),\n            awaitItem(),\n        )\n    }\n}\n```\n\n### Pattern 3: Testing Event Sequences\n\n```kotlin\n@Test\nfun `submit should emit validation then navigation events`() = runTest {\n    viewModel.eventFlow.test {\n        expectNoEvents()\n\n        viewModel.trySendAction(MyAction.Submit)\n\n        assertEquals(MyEvent.ShowValidation, awaitItem())\n        assertEquals(MyEvent.NavigateToNext, awaitItem())\n    }\n}\n```\n\n### Pattern 4: Testing Cancellation\n\n```kotlin\n@Test\nfun `cancelling collection should stop emissions`() = runTest {\n    val flow = flow {\n        repeat(100) {\n            emit(it)\n            delay(100)\n        }\n    }\n\n    flow.test {\n        assertEquals(0, awaitItem())\n        assertEquals(1, awaitItem())\n\n        // Cancel after 2 items\n        cancel()\n\n        // No more items received\n    }\n}\n```\n\n## Anti-Patterns\n\n### ❌ Forgetting expectNoEvents() on EventFlow\n\n```kotlin\n// WRONG\nviewModel.eventFlow.test {\n    viewModel.trySendAction(action)  // May fail - no initial expectNoEvents\n    assertEquals(event, awaitItem())\n}\n\n// CORRECT\nviewModel.eventFlow.test {\n    expectNoEvents()  // ALWAYS do this first\n    viewModel.trySendAction(action)\n    assertEquals(event, awaitItem())\n}\n```\n\n### ❌ Not Using runTest\n\n```kotlin\n// WRONG - Missing runTest\n@Test\nfun `test flow`() {\n    flow.test { /* ... */ }\n}\n\n// CORRECT\n@Test\nfun `test flow`() = runTest {\n    flow.test { /* ... */ }\n}\n```\n\n### ❌ Mixing StateFlow and EventFlow Patterns\n\n```kotlin\n// WRONG - Treating StateFlow like EventFlow\nstateFlow.test {\n    expectNoEvents()  // Unnecessary - StateFlow always has value\n    /* ... */\n}\n\n// WRONG - Treating EventFlow like StateFlow\neventFlow.test {\n    val item = awaitItem()  // Will hang - no initial value!\n    /* ... */\n}\n```\n\n## Reference Implementations\n\n**ViewModel with StateFlow and EventFlow:**\n`app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt`\n\n**Repository Flow Testing:**\n`data/src/test/kotlin/com/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt`\n\n**Complex Flow Transformations:**\n`data/src/test/kotlin/com/bitwarden/data/vault/repository/VaultRepositoryTest.kt`\n\n<!-- File: .claude/skills/testing-android-code/references/test-base-classes.md (Tokens: ~1709 | Category: anthropic-skill) -->\n# Test Base Classes Reference\n\nBitwarden Android provides specialized base classes that configure test environments and provide helper utilities.\n\n## BaseViewModelTest\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`\n\n### Purpose\nProvides essential setup for testing ViewModels with proper coroutine dispatcher configuration and Flow testing helpers.\n\n### Automatic Configuration\n- Registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`\n- Ensures deterministic coroutine execution in tests\n- All coroutines complete immediately without real delays\n\n### Key Feature: stateEventFlow() Helper\n\n**Use Case:** When you need to test both StateFlow and EventFlow simultaneously.\n\n```kotlin\n@Test\nfun `complex action should update state and emit event`() = runTest {\n    val viewModel = ExampleViewModel(savedStateHandle, mockRepository)\n\n    viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->\n        // Verify initial state\n        assertEquals(INITIAL_STATE, stateFlow.awaitItem())\n\n        // No events yet\n        eventFlow.expectNoEvents()\n\n        // Trigger action\n        viewModel.trySendAction(ExampleAction.ComplexAction)\n\n        // Verify state updated\n        assertEquals(LOADING_STATE, stateFlow.awaitItem())\n\n        // Verify event emitted\n        assertEquals(ExampleEvent.ShowToast, eventFlow.awaitItem())\n    }\n}\n```\n\n### Usage Pattern\n\n```kotlin\nclass MyViewModelTest : BaseViewModelTest() {\n    private val mockRepository: MyRepository = mockk()\n    private val savedStateHandle = SavedStateHandle(\n        mapOf(KEY_STATE to INITIAL_STATE)\n    )\n\n    @Test\n    fun `test action`() = runTest {\n        val viewModel = MyViewModel(\n            savedStateHandle = savedStateHandle,\n            repository = mockRepository\n        )\n\n        // Test with automatic dispatcher setup\n        viewModel.stateFlow.test {\n            assertEquals(INITIAL_STATE, awaitItem())\n        }\n    }\n}\n```\n\n## BitwardenComposeTest\n\n**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`\n\n### Purpose\nPre-configured test class for Compose UI tests with all Bitwarden managers and theme setup.\n\n### Automatic Configuration\n- All Bitwarden managers pre-configured (FeatureFlags, AuthTab, Biometrics, etc.)\n- Wraps content in `BitwardenTheme` and `LocalManagerProvider`\n- Provides fixed `Clock` for deterministic time-based tests\n- Extends `BaseComposeTest` for Robolectric and dispatcher setup\n\n### Key Features\n\n**Pre-configured Managers:**\n- `FeatureFlagManager` - Controls feature flag behavior\n- `AuthTabManager` - Manages auth tab state\n- `BiometricsManager` - Handles biometric authentication\n- `ClipboardManager` - Clipboard operations\n- `NotificationManager` - Notification display\n\n**Fixed Clock:**\nAll tests use a fixed clock for deterministic time-based testing:\n```kotlin\n// Tests use consistent time: 2023-10-27T12:00:00Z\nval fixedClock: Clock\n```\n\n### Usage Pattern\n\n```kotlin\nclass MyScreenTest : BitwardenComposeTest() {\n    private var haveCalledNavigateBack = false\n    private val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n    private val viewModel = mockk<MyViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        setContent {\n            MyScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                viewModel = viewModel\n            )\n        }\n    }\n\n    @Test\n    fun `on back click should send action`() {\n        composeTestRule.onNodeWithContentDescription(\"Back\").performClick()\n        verify { viewModel.trySendAction(MyAction.BackClick) }\n    }\n\n    @Test\n    fun `loading state should show progress`() {\n        mutableStateFlow.value = DEFAULT_STATE.copy(isLoading = true)\n        composeTestRule.onNode(isProgressBar).assertIsDisplayed()\n    }\n}\n```\n\n### Important: bufferedMutableSharedFlow for Events\n\nIn Compose tests, use `bufferedMutableSharedFlow` instead of regular `MutableSharedFlow` (default replay is 0):\n\n```kotlin\n// Correct for Compose tests\nprivate val mutableEventFlow = bufferedMutableSharedFlow<MyEvent>()\n\n// This allows triggering events and having the UI react\nmutableEventFlow.tryEmit(MyEvent.NavigateBack)\n```\n\n## BaseServiceTest\n\n**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt`\n\n### Purpose\nProvides MockWebServer setup for testing API service implementations.\n\n### Automatic Configuration\n- `server: MockWebServer` - Auto-started before each test, stopped after\n- `retrofit: Retrofit` - Pre-configured with:\n  - JSON converter (kotlinx.serialization)\n  - NetworkResultCallAdapter for Result<T> responses\n  - Base URL pointing to MockWebServer\n- `json: Json` - kotlinx.serialization JSON instance\n\n### Usage Pattern\n\n```kotlin\nclass MyServiceTest : BaseServiceTest() {\n    private val api: MyApi = retrofit.create()\n    private val service = MyServiceImpl(api)\n\n    @Test\n    fun `getConfig should return success when API succeeds`() = runTest {\n        // Enqueue mock response\n        server.enqueue(MockResponse().setBody(EXPECTED_JSON))\n\n        // Call service\n        val result = service.getConfig()\n\n        // Verify result\n        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)\n    }\n\n    @Test\n    fun `getConfig should return failure when API fails`() = runTest {\n        // Enqueue error response\n        server.enqueue(MockResponse().setResponseCode(500))\n\n        // Call service\n        val result = service.getConfig()\n\n        // Verify failure\n        assertTrue(result.isFailure)\n    }\n}\n```\n\n### MockWebServer Patterns\n\n**Enqueue successful response:**\n```kotlin\nserver.enqueue(MockResponse().setBody(\"\"\"{\"key\": \"value\"}\"\"\"))\n```\n\n**Enqueue error response:**\n```kotlin\nserver.enqueue(MockResponse().setResponseCode(404))\nserver.enqueue(MockResponse().setResponseCode(500))\n```\n\n**Enqueue delayed response:**\n```kotlin\nserver.enqueue(\n    MockResponse()\n        .setBody(\"\"\"{\"key\": \"value\"}\"\"\")\n        .setBodyDelay(1000, TimeUnit.MILLISECONDS)\n)\n```\n\n**Verify request details:**\n```kotlin\nval request = server.takeRequest()\nassertEquals(\"/api/config\", request.path)\nassertEquals(\"GET\", request.method)\nassertEquals(\"Bearer token\", request.getHeader(\"Authorization\"))\n```\n\n## BaseComposeTest\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseComposeTest.kt`\n\n### Purpose\nBase class for Compose tests that extends `BaseRobolectricTest` and provides `setTestContent()` helper.\n\n### Features\n- Robolectric configuration for Compose\n- Proper dispatcher setup\n- `composeTestRule` for UI testing\n- `setTestContent()` helper wraps content in theme\n\n### Usage\nTypically you'll extend `BitwardenComposeTest` which extends this class. Use `BaseComposeTest` directly only for tests that don't need Bitwarden-specific manager configuration.\n\n## When to Use Each Base Class\n\n| Test Type | Base Class | Use When |\n|-----------|------------|----------|\n| ViewModel tests | `BaseViewModelTest` | Testing ViewModel state and events |\n| Compose screen tests | `BitwardenComposeTest` | Testing Compose UI with Bitwarden components |\n| API service tests | `BaseServiceTest` | Testing network layer with MockWebServer |\n| Repository tests | None (manual setup) | Testing repository logic with mocked dependencies |\n| Utility/helper tests | None (manual setup) | Testing pure functions or utilities |\n\n## Complete Examples\n\n**ViewModel Test:**\n`../examples/viewmodel-test-example.md`\n\n**Compose Screen Test:**\n`../examples/compose-screen-test-example.md`\n\n**Repository Test:**\n`../examples/repository-test-example.md`\n\n<!-- File: .claude/skills/testing-android-code/SKILL.md (Tokens: ~2736 | Category: anthropic-skill) -->\n---\nname: testing-android-code\ndescription: This skill should be used when writing or reviewing tests for Android code in Bitwarden. Triggered by \"BaseViewModelTest\", \"BitwardenComposeTest\", \"BaseServiceTest\", \"stateEventFlow\", \"bufferedMutableSharedFlow\", \"FakeDispatcherManager\", \"expectNoEvents\", \"assertCoroutineThrows\", \"createMockCipher\", \"createMockSend\", \"asSuccess\", \"Why is my Bitwarden test failing?\", or testing questions about ViewModels, repositories, Compose screens, or data sources in Bitwarden.\nversion: 1.0.0\n---\n\n# Testing Android Code - Bitwarden Testing Patterns\n\n**This skill provides tactical testing guidance for Bitwarden-specific patterns.** For comprehensive architecture and testing philosophy, consult `docs/ARCHITECTURE.md`.\n\n## Test Framework Configuration\n\n**Required Dependencies:**\n- **JUnit 5** (jupiter), **MockK**, **Turbine** (app.cash.turbine)\n- **kotlinx.coroutines.test**, **Robolectric**, **Compose Test**\n\n**Critical Note:** Tests run with en-US locale for consistency. Don't assume other locales.\n\n---\n\n## A. ViewModel Testing Patterns\n\n### Base Class: BaseViewModelTest\n\n**Always extend `BaseViewModelTest` for ViewModel tests.**\n\n**Location:** `ui/src/testFixtures/kotlin/com/bitwarden/ui/platform/base/BaseViewModelTest.kt`\n\n**Benefits:**\n- Automatically registers `MainDispatcherExtension` for `UnconfinedTestDispatcher`\n- Provides `stateEventFlow()` helper for simultaneous StateFlow/EventFlow testing\n\n**Pattern:**\n```kotlin\nclass ExampleViewModelTest : BaseViewModelTest() {\n    private val mockRepository: ExampleRepository = mockk()\n    private val savedStateHandle = SavedStateHandle(mapOf(KEY_STATE to INITIAL_STATE))\n\n    @Test\n    fun `ButtonClick should fetch data and update state`() = runTest {\n        coEvery { mockRepository.fetchData(any()) } returns Result.success(\"data\")\n\n        val viewModel = ExampleViewModel(savedStateHandle, mockRepository)\n\n        viewModel.stateFlow.test {\n            assertEquals(INITIAL_STATE, awaitItem())\n            viewModel.trySendAction(ExampleAction.ButtonClick)\n            assertEquals(INITIAL_STATE.copy(data = \"data\"), awaitItem())\n        }\n\n        coVerify { mockRepository.fetchData(any()) }\n    }\n}\n```\n\n**For complete examples:** See `references/test-base-classes.md`\n\n### StateFlow vs EventFlow (Critical Distinction)\n\n| Flow Type | Replay | First Action | Pattern |\n|-----------|--------|--------------|---------|\n| StateFlow | Yes (1) | `awaitItem()` gets current state | Expect initial → trigger → expect new |\n| EventFlow | No | `expectNoEvents()` first | expectNoEvents → trigger → expect event |\n\n**For detailed patterns:** See `references/flow-testing-patterns.md`\n\n---\n\n## B. Compose UI Testing Patterns\n\n### Base Class: BitwardenComposeTest\n\n**Always extend `BitwardenComposeTest` for Compose screen tests.**\n\n**Location:** `app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/base/BitwardenComposeTest.kt`\n\n**Benefits:**\n- Pre-configures all Bitwarden managers (FeatureFlags, AuthTab, Biometrics, etc.)\n- Wraps content in `BitwardenTheme` and `LocalManagerProvider`\n- Provides fixed Clock for deterministic time-based tests\n\n**Pattern:**\n```kotlin\nclass ExampleScreenTest : BitwardenComposeTest() {\n    private var haveCalledNavigateBack = false\n    private val mutableEventFlow = bufferedMutableSharedFlow<ExampleEvent>()\n    private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)\n    private val viewModel = mockk<ExampleViewModel>(relaxed = true) {\n        every { eventFlow } returns mutableEventFlow\n        every { stateFlow } returns mutableStateFlow\n    }\n\n    @Before\n    fun setup() {\n        setContent {\n            ExampleScreen(\n                onNavigateBack = { haveCalledNavigateBack = true },\n                viewModel = viewModel,\n            )\n        }\n    }\n\n    @Test\n    fun `on back click should send BackClick action`() {\n        composeTestRule.onNodeWithContentDescription(\"Back\").performClick()\n        verify { viewModel.trySendAction(ExampleAction.BackClick) }\n    }\n}\n```\n\n**Note:** Use `bufferedMutableSharedFlow` for event testing in Compose tests. Default replay is 0; pass `replay = 1` if needed.\n\n**For complete base class details:** See `references/test-base-classes.md`\n\n---\n\n## C. Repository and Service Testing\n\n### Service Testing with MockWebServer\n\n**Base Class:** `BaseServiceTest` (`network/src/testFixtures/`)\n\n```kotlin\nclass ExampleServiceTest : BaseServiceTest() {\n    private val api: ExampleApi = retrofit.create()\n    private val service = ExampleServiceImpl(api)\n\n    @Test\n    fun `getConfig should return success when API succeeds`() = runTest {\n        server.enqueue(MockResponse().setBody(EXPECTED_JSON))\n        val result = service.getConfig()\n        assertEquals(EXPECTED_RESPONSE.asSuccess(), result)\n    }\n}\n```\n\n### Repository Testing Pattern\n\n```kotlin\nclass ExampleRepositoryTest {\n    private val fixedClock: Clock = Clock.fixed(\n        Instant.parse(\"2023-10-27T12:00:00Z\"),\n        ZoneOffset.UTC,\n    )\n    private val dispatcherManager = FakeDispatcherManager()\n    private val mockDiskSource: ExampleDiskSource = mockk()\n    private val mockService: ExampleService = mockk()\n\n    private val repository = ExampleRepositoryImpl(\n        clock = fixedClock,\n        exampleDiskSource = mockDiskSource,\n        exampleService = mockService,\n        dispatcherManager = dispatcherManager,\n    )\n\n    @Test\n    fun `fetchData should return success when service succeeds`() = runTest {\n        coEvery { mockService.getData(any()) } returns expectedData.asSuccess()\n        val result = repository.fetchData(userId)\n        assertTrue(result.isSuccess)\n    }\n}\n```\n\n**Key patterns:** Use `FakeDispatcherManager`, fixed Clock, and `.asSuccess()` helpers.\n\n---\n\n## D. Test Data Builders\n\n### Builder Pattern with Number Parameter\n\n**Location:** `network/src/testFixtures/kotlin/com/bitwarden/network/model/`\n\n```kotlin\nfun createMockCipher(\n    number: Int,\n    id: String = \"mockId-$number\",\n    name: String? = \"mockName-$number\",\n    // ... more parameters with defaults\n): SyncResponseJson.Cipher\n\n// Usage:\nval cipher1 = createMockCipher(number = 1)  // mockId-1, mockName-1\nval cipher2 = createMockCipher(number = 2)  // mockId-2, mockName-2\nval custom = createMockCipher(number = 3, name = \"Custom\")\n```\n\n**Available Builders (35+):**\n- **Cipher:** `createMockCipher()`, `createMockLogin()`, `createMockCard()`, `createMockIdentity()`, `createMockSecureNote()`, `createMockSshKey()`, `createMockField()`, `createMockUri()`, `createMockFido2Credential()`, `createMockPasswordHistory()`, `createMockCipherPermissions()`\n- **Sync:** `createMockSyncResponse()`, `createMockFolder()`, `createMockCollection()`, `createMockPolicy()`, `createMockDomains()`\n- **Send:** `createMockSend()`, `createMockFile()`, `createMockText()`, `createMockSendJsonRequest()`\n- **Profile:** `createMockProfile()`, `createMockOrganization()`, `createMockProvider()`, `createMockPermissions()`\n- **Attachments:** `createMockAttachment()`, `createMockAttachmentJsonRequest()`, `createMockAttachmentResponse()`\n\nSee `network/src/testFixtures/kotlin/com/bitwarden/network/model/` for full list.\n\n---\n\n## E. Result Type Testing\n\n**Locations:**\n- `.asSuccess()`, `.asFailure()`: `core/src/main/kotlin/com/bitwarden/core/data/util/ResultExtensions.kt`\n- `assertCoroutineThrows`: `core/src/testFixtures/kotlin/com/bitwarden/core/data/util/TestHelpers.kt`\n\n```kotlin\n// Create results\n\"data\".asSuccess()              // Result.success(\"data\")\nthrowable.asFailure()           // Result.failure<T>(throwable)\n\n// Assertions\nassertTrue(result.isSuccess)\nassertEquals(expectedValue, result.getOrNull())\n```\n\n---\n\n## F. Test Utilities and Helpers\n\n### Fake Implementations\n\n| Fake | Location | Purpose |\n|------|----------|---------|\n| `FakeDispatcherManager` | `core/src/testFixtures/` | Deterministic coroutine execution |\n| `FakeConfigDiskSource` | `data/src/testFixtures/` | In-memory config storage |\n| `FakeSharedPreferences` | `data/src/testFixtures/` | Memory-backed SharedPreferences |\n\n### Exception Testing (CRITICAL)\n\n```kotlin\n// CORRECT - Call directly, NOT inside runTest\n@Test\nfun `test exception`() {\n    assertCoroutineThrows<IllegalStateException> {\n        repository.throwingFunction()\n    }\n}\n```\n\n**Why:** `runTest` catches exceptions and rethrows them, breaking the assertion pattern.\n\n---\n\n## G. Critical Gotchas\n\nCommon testing mistakes in Bitwarden. **For complete details and examples:** See `references/critical-gotchas.md`\n\n**Core Patterns:**\n- **assertCoroutineThrows + runTest** - Never wrap in `runTest`; call directly\n- **Static mock cleanup** - Always `unmockkStatic()` in `@After`\n- **StateFlow vs EventFlow** - StateFlow: `awaitItem()` first; EventFlow: `expectNoEvents()` first\n- **FakeDispatcherManager** - Always use instead of real `DispatcherManagerImpl`\n- **Coroutine test wrapper** - Use `runTest { }` for all Flow/coroutine tests\n\n**Assertion Patterns:**\n- **Complete state assertions** - Assert entire state objects, not individual fields\n- **JUnit over Kotlin** - Use `assertTrue()`, not Kotlin's `assert()`\n- **Use Result extensions** - Use `asSuccess()` and `asFailure()` for Result type assertions\n\n**Test Design:**\n- **Fake vs Mock strategy** - Use Fakes for happy paths, Mocks for error paths\n- **DI over static mocking** - Extract interfaces (like UuidManager) instead of mockkStatic\n- **Null stream testing** - Test null returns from ContentResolver operations\n- **bufferedMutableSharedFlow** - Use with `.onSubscription { emit(state) }` in Fakes\n- **Test factory methods** - Accept domain state types, not SavedStateHandle\n\n---\n\n## H. Test File Organization\n\n### Directory Structure\n\n```\nmodule/src/test/kotlin/com/bitwarden/.../\n├── ui/*ScreenTest.kt, *ViewModelTest.kt\n├── data/repository/*RepositoryTest.kt\n└── network/service/*ServiceTest.kt\n\nmodule/src/testFixtures/kotlin/com/bitwarden/.../\n├── util/TestHelpers.kt\n├── base/Base*Test.kt\n└── model/*Util.kt\n```\n\n### Test Constants Placement\n\nDeclare test constants as top-level `private const val` at the **bottom** of the file, after the class closing brace. Do NOT use `companion object` for test constants.\n\n### Test Naming\n\n- Classes: `*Test.kt`, `*ScreenTest.kt`, `*ViewModelTest.kt`\n- Functions: `` `given state when action should result` ``\n\n---\n\n## Summary\n\nKey Bitwarden-specific testing patterns:\n\n1. **BaseViewModelTest** - Automatic dispatcher setup with `stateEventFlow()` helper\n2. **BitwardenComposeTest** - Pre-configured with all managers and theme\n3. **BaseServiceTest** - MockWebServer setup for network testing\n4. **Turbine Flow Testing** - StateFlow (replay) vs EventFlow (no replay)\n5. **Test Data Builders** - Consistent `number: Int` parameter pattern\n6. **Fake Implementations** - FakeDispatcherManager, FakeConfigDiskSource\n7. **Result Type Testing** - `.asSuccess()`, `.asFailure()`\n\n**Always consult:** `docs/ARCHITECTURE.md` and existing test files for reference implementations.\n\n---\n\n## Reference Documentation\n\nFor detailed information, see:\n\n- `references/test-base-classes.md` - Detailed base class documentation and usage patterns\n- `references/flow-testing-patterns.md` - Complete Turbine patterns for StateFlow/EventFlow\n- `references/critical-gotchas.md` - Full anti-pattern reference and debugging tips\n\n**Complete Examples:**\n- `examples/viewmodel-test-example.md` - Full ViewModel test with StateFlow/EventFlow\n- `examples/compose-screen-test-example.md` - Full Compose screen test\n- `examples/repository-test-example.md` - Full repository test with mocks and fakes\n\n<!-- ... +5 more modular skills available via /api/skills/bitwarden/android?path={path} -->\n</yakaai_skills>"}