mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 10:38:43 -05:00
Compare commits
121 Commits
release/20
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34888f8bc3 | ||
|
|
0975144342 | ||
|
|
07415844ee | ||
|
|
913d877737 | ||
|
|
c16da5090e | ||
|
|
b79aca7338 | ||
|
|
7834d5bf27 | ||
|
|
7c929c3713 | ||
|
|
7f032a8732 | ||
|
|
ef6714fa17 | ||
|
|
d09945d80b | ||
|
|
30ce512091 | ||
|
|
bdbcd5bdc2 | ||
|
|
b4414073c7 | ||
|
|
1594de39c1 | ||
|
|
f0c5c8f421 | ||
|
|
2a343555bf | ||
|
|
dff6a13cd7 | ||
|
|
e415145c53 | ||
|
|
54ea921b25 | ||
|
|
e87ffa3902 | ||
|
|
00cded3a02 | ||
|
|
1503e3f769 | ||
|
|
6840a6c207 | ||
|
|
d32e767c62 | ||
|
|
4a874668f2 | ||
|
|
cd27fe339d | ||
|
|
2eb8ad4221 | ||
|
|
28db795790 | ||
|
|
8c6782dcb1 | ||
|
|
127809b8df | ||
|
|
ca13e615ec | ||
|
|
5e3e8a04aa | ||
|
|
8077895eb8 | ||
|
|
33e9313c6c | ||
|
|
593bfbf8cf | ||
|
|
4905358adb | ||
|
|
02733f785b | ||
|
|
8baa4bf041 | ||
|
|
4d20453d0f | ||
|
|
4b951a1df2 | ||
|
|
9349b235bc | ||
|
|
e9ab5f2def | ||
|
|
3bef282426 | ||
|
|
e1bb3a4b5d | ||
|
|
1904c4ffb9 | ||
|
|
26e7178300 | ||
|
|
2c01abda46 | ||
|
|
b86cbfcd87 | ||
|
|
3f303d3f39 | ||
|
|
ca7a65fc95 | ||
|
|
f02b374e98 | ||
|
|
1a90860080 | ||
|
|
adf83cd315 | ||
|
|
489c0ea8d6 | ||
|
|
9831358a8b | ||
|
|
8bdbccd8de | ||
|
|
a75d904317 | ||
|
|
a395f28eba | ||
|
|
53e358d7b3 | ||
|
|
663eb3641f | ||
|
|
ab305b2631 | ||
|
|
946b0784e0 | ||
|
|
167a46a073 | ||
|
|
7b491d3c3c | ||
|
|
7918abdccf | ||
|
|
5ec0a1986d | ||
|
|
839e9e8a1a | ||
|
|
979237b751 | ||
|
|
621f97d161 | ||
|
|
d81b0005ee | ||
|
|
794b27a750 | ||
|
|
169b21cfdb | ||
|
|
4623a4f079 | ||
|
|
21afa81322 | ||
|
|
55c7ab4cee | ||
|
|
7a40bfe522 | ||
|
|
0c234ee0aa | ||
|
|
7b0b93a204 | ||
|
|
473416c1b4 | ||
|
|
f3646790e3 | ||
|
|
35b87f4390 | ||
|
|
7120eefc94 | ||
|
|
5eb56cafaa | ||
|
|
b2d94fae40 | ||
|
|
ad748eef7f | ||
|
|
8010e8d6c3 | ||
|
|
7a6a493f24 | ||
|
|
4032d2bb5c | ||
|
|
e2c9aae9c1 | ||
|
|
56566b958c | ||
|
|
06bf603ec8 | ||
|
|
79d6da0a61 | ||
|
|
2d2ea9acee | ||
|
|
05d6fe1ba1 | ||
|
|
1024f77ddf | ||
|
|
50e50eb08c | ||
|
|
bd98df6eb9 | ||
|
|
9baec6e6a5 | ||
|
|
efd15b027c | ||
|
|
b715a51188 | ||
|
|
94ed32790f | ||
|
|
dca97e0c8e | ||
|
|
aceb96d18f | ||
|
|
510072b34f | ||
|
|
7324be04f4 | ||
|
|
bfbe47f48f | ||
|
|
2bb06063c7 | ||
|
|
ed47ff4d18 | ||
|
|
448ba97ae2 | ||
|
|
14e833247d | ||
|
|
4b7fcdb6ea | ||
|
|
0959284e6f | ||
|
|
1f24ca7de1 | ||
|
|
dc79176274 | ||
|
|
a631d6822a | ||
|
|
845a5dec22 | ||
|
|
b1195b5f46 | ||
|
|
8de3a07715 | ||
|
|
9c4bd2ee14 | ||
|
|
6dad8c4def |
@@ -1,20 +1,3 @@
|
||||
Use the `reviewing-changes` skill to review this pull request.
|
||||
|
||||
The PR branch is already checked out in the current working directory.
|
||||
|
||||
Provide a comprehensive review including:
|
||||
|
||||
- Summary of changes since last review
|
||||
- Critical issues found (be thorough)
|
||||
- Suggested improvements (be thorough)
|
||||
- Good practices observed (be concise - list only the most notable items without elaboration)
|
||||
- Action items for the author
|
||||
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code snippets
|
||||
|
||||
When reviewing subsequent commits:
|
||||
|
||||
- Track status of previously identified issues (fixed/unfixed/reopened)
|
||||
- Identify NEW problems introduced since last review
|
||||
- Note if fixes introduced new issues
|
||||
|
||||
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively.
|
||||
|
||||
@@ -1,110 +1,98 @@
|
||||
---
|
||||
name: reviewing-changes
|
||||
description: Performs comprehensive code reviews for Bitwarden Android projects, verifying architecture compliance, style guidelines, compilation safety, test coverage, and security requirements. Use when reviewing pull requests, checking commits, analyzing code changes, verifying Bitwarden coding standards, evaluating MVVM patterns, checking Hilt DI usage, reviewing security implementations, or assessing test coverage. Automatically invoked by CI pipeline or manually for interactive code reviews.
|
||||
version: 3.0.0
|
||||
description: Android-specific code review workflow additions for Bitwarden Android. Provides change type refinements, checklist loading, and reference material organization. Complements bitwarden-code-reviewer agent's base review standards.
|
||||
---
|
||||
|
||||
# Reviewing Changes
|
||||
# Reviewing Changes - Android Additions
|
||||
|
||||
This skill provides Android-specific workflow additions that complement the base `bitwarden-code-reviewer` agent standards.
|
||||
|
||||
## Instructions
|
||||
|
||||
Follow this process to review code changes for Bitwarden Android:
|
||||
**IMPORTANT**: Use structured thinking throughout your review process. Plan your analysis in `<thinking>` tags before providing final feedback.
|
||||
|
||||
### Step 1: Understand Context
|
||||
### Step 1: Retrieve Additional Details
|
||||
|
||||
Start with high-level assessment of the change's purpose and approach. Read PR/commit descriptions and understand what problem is being solved.
|
||||
<thinking>
|
||||
Determine if more context is available for the changes:
|
||||
1. Are there JIRA tickets or GitHub Issues mentioned in the PR title or body?
|
||||
2. Are there other GitHub pull requests mentioned in the PR title or body?
|
||||
</thinking>
|
||||
|
||||
### Step 2: Verify Compliance
|
||||
Retrieve any additional information linked to the pull request using available tools (JIRA MCP, GitHub API).
|
||||
|
||||
Systematically check each area against Bitwarden standards documented in `CLAUDE.md`:
|
||||
If pull request title and message do not provide enough context, request additional details from the reviewer:
|
||||
- Link a JIRA ticket
|
||||
- Associate a GitHub issue
|
||||
- Link to another pull request
|
||||
- Add more detail to the PR title or body
|
||||
|
||||
1. **Architecture**: Follow patterns in `docs/ARCHITECTURE.md`
|
||||
- MVVM + UDF (ViewModels with `StateFlow`, Compose UI)
|
||||
- Hilt DI (interface injection, `@HiltViewModel`)
|
||||
- Repository pattern and proper data flow
|
||||
### Step 2: Detect Change Type with Android Refinements
|
||||
|
||||
2. **Style**: Adhere to `docs/STYLE_AND_BEST_PRACTICES.md`
|
||||
- Naming conventions, code organization, formatting
|
||||
- Kotlin idioms (immutability, null safety, coroutines)
|
||||
<thinking>
|
||||
Analyze the changeset systematically:
|
||||
1. What files were modified? (code vs config vs docs)
|
||||
2. What is the PR/commit title indicating?
|
||||
3. Is there new functionality or just modifications?
|
||||
4. What's the risk level of these changes?
|
||||
</thinking>
|
||||
|
||||
3. **Compilation**: Analyze for potential build issues
|
||||
- Import statements and dependencies
|
||||
- Type safety and null safety
|
||||
- API compatibility and deprecation warnings
|
||||
- Resource references and manifest requirements
|
||||
Use the base change type detection from the agent, with Android-specific refinements:
|
||||
|
||||
4. **Testing**: Verify appropriate test coverage
|
||||
- Unit tests for business logic and utility functions
|
||||
- Integration tests for complex workflows
|
||||
- UI tests for user-facing features when applicable
|
||||
- Test coverage for edge cases and error scenarios
|
||||
**Android-specific patterns:**
|
||||
- **Feature Addition**: New `ViewModel`, new `Repository`, new `@Composable` functions, new `*Screen.kt` files
|
||||
- **UI Refinement**: Changes only in `*Screen.kt`, `*Composable.kt`, `ui/` package files
|
||||
- **Infrastructure**: Changes to `.github/workflows/`, `gradle/`, `build.gradle.kts`, `libs.versions.toml`
|
||||
- **Dependency Update**: Changes only to `libs.versions.toml` or `build.gradle.kts` with version bumps
|
||||
|
||||
5. **Security**: Given Bitwarden's security-focused nature
|
||||
- Proper handling of sensitive data
|
||||
- Secure storage practices (Android Keystore)
|
||||
- Authentication and authorization patterns
|
||||
- Data encryption and decryption flows
|
||||
- Zero-knowledge architecture preservation
|
||||
### Step 3: Load Appropriate Checklist
|
||||
|
||||
### Step 3: Document Findings
|
||||
Based on detected type, read the relevant checklist file:
|
||||
|
||||
Identify specific violations with `file:line_number` references. Be precise about locations.
|
||||
- **Dependency Update** → `checklists/dependency-update.md` (expedited review)
|
||||
- **Bug Fix** → `checklists/bug-fix.md` (focused review)
|
||||
- **Feature Addition** → `checklists/feature-addition.md` (comprehensive review)
|
||||
- **UI Refinement** → `checklists/ui-refinement.md` (design-focused review)
|
||||
- **Refactoring** → `checklists/refactoring.md` (pattern-focused review)
|
||||
- **Infrastructure** → `checklists/infrastructure.md` (tooling-focused review)
|
||||
|
||||
### Step 4: Provide Recommendations
|
||||
The checklist provides:
|
||||
- Multi-pass review strategy
|
||||
- Type-specific focus areas
|
||||
- What to check and what to skip
|
||||
- Structured thinking guidance
|
||||
|
||||
Give actionable recommendations for improvements. Explain why changes are needed and suggest specific solutions.
|
||||
### Step 4: Execute Review Following Checklist
|
||||
|
||||
### Step 5: Flag Critical Issues
|
||||
<thinking>
|
||||
Before diving into details:
|
||||
1. What are the highest-risk areas of this change?
|
||||
2. Which architectural patterns need verification?
|
||||
3. What security implications exist?
|
||||
4. How should I prioritize my findings?
|
||||
5. What tone is appropriate for this feedback?
|
||||
</thinking>
|
||||
|
||||
Highlight issues that must be addressed before merge. Distinguish between blockers and suggestions.
|
||||
Follow the checklist's multi-pass strategy, thinking through each pass systematically.
|
||||
|
||||
### Step 6: Acknowledge Quality
|
||||
### Step 5: Consult Android Reference Materials As Needed
|
||||
|
||||
Note well-implemented patterns (briefly, without elaboration). Keep positive feedback concise.
|
||||
Load reference files only when needed for specific questions:
|
||||
|
||||
## Review Anti-Patterns (DO NOT)
|
||||
- **Issue prioritization** → `reference/priority-framework.md` (Critical vs Suggested vs Optional)
|
||||
- **Phrasing feedback** → `reference/review-psychology.md` (questions vs commands, I-statements)
|
||||
- **Architecture questions** → `reference/architectural-patterns.md` (MVVM, Hilt DI, module org, error handling)
|
||||
- **Security questions (quick reference)** → `reference/security-patterns.md` (common patterns and anti-patterns)
|
||||
- **Security questions (comprehensive)** → `docs/ARCHITECTURE.md#security` (full zero-knowledge architecture)
|
||||
- **Testing questions** → `reference/testing-patterns.md` (unit tests, mocking, null safety)
|
||||
- **UI questions** → `reference/ui-patterns.md` (Compose patterns, theming)
|
||||
- **Style questions** → `docs/STYLE_AND_BEST_PRACTICES.md`
|
||||
|
||||
- Be nitpicky about linter-catchable style issues
|
||||
- Review without understanding context - ask for clarification first
|
||||
- Focus only on new code - check surrounding context for issues
|
||||
- Request changes outside the scope of this changeset
|
||||
## Core Principles
|
||||
|
||||
## Examples
|
||||
|
||||
### Good Review Format
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
This PR adds biometric authentication to the login flow, implementing MVVM pattern with proper state management.
|
||||
|
||||
## Critical Issues
|
||||
- `app/login/LoginViewModel.kt:45` - Mutable state exposed; use `StateFlow` instead of `MutableStateFlow`
|
||||
- `data/auth/BiometricRepository.kt:120` - Missing null safety check on `biometricPrompt` result
|
||||
|
||||
## Suggested Improvements
|
||||
- Consider extracting biometric prompt logic to separate use case class
|
||||
- Add integration tests for biometric failure scenarios
|
||||
- `app/login/LoginScreen.kt:89` - Consider using existing `BitwardenButton` component
|
||||
|
||||
## Good Practices
|
||||
- Proper Hilt DI usage throughout
|
||||
- Comprehensive unit test coverage
|
||||
- Clear separation of concerns
|
||||
|
||||
## Action Items
|
||||
1. Fix mutable state exposure in `LoginViewModel`
|
||||
2. Add null safety check in `BiometricRepository`
|
||||
3. Consider adding integration tests for error flows
|
||||
```
|
||||
|
||||
### What to Focus On
|
||||
|
||||
**DO focus on:**
|
||||
- Architecture violations (incorrect patterns)
|
||||
- Security issues (data handling, encryption)
|
||||
- Missing tests for critical paths
|
||||
- Compilation risks (type safety, null safety)
|
||||
|
||||
**DON'T focus on:**
|
||||
- Minor formatting (handled by linters)
|
||||
- Personal preferences without architectural basis
|
||||
- Issues outside the changeset scope
|
||||
- **Appropriate depth**: Match review rigor to change complexity and risk
|
||||
- **Specific references**: Always use `file:line_number` format for precise location
|
||||
- **Actionable feedback**: Say what to do and why, not just what's wrong
|
||||
- **Efficient reviews**: Use multi-pass strategy, skip what's not relevant
|
||||
- **Android patterns**: Validate MVVM, Hilt DI, Compose conventions, Kotlin idioms
|
||||
|
||||
164
.claude/skills/reviewing-changes/checklists/bug-fix.md
Normal file
164
.claude/skills/reviewing-changes/checklists/bug-fix.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# Bug Fix Review Checklist
|
||||
|
||||
## Multi-Pass Strategy
|
||||
|
||||
### First Pass: Understand the Bug
|
||||
|
||||
<thinking>
|
||||
Before evaluating the fix:
|
||||
1. What was the original bug/broken behavior?
|
||||
2. What is the expected correct behavior?
|
||||
3. What was the root cause?
|
||||
4. How was the bug discovered? (user report, test, production)
|
||||
5. What's the severity? (crash, data loss, UI glitch, minor annoyance)
|
||||
</thinking>
|
||||
|
||||
**1. Understand root cause:**
|
||||
- What was the broken behavior?
|
||||
- What caused it?
|
||||
- How does this fix address the root cause?
|
||||
|
||||
**2. Assess scope:**
|
||||
- How many files changed?
|
||||
- Is this a targeted fix or broader refactoring?
|
||||
- Does this affect multiple features?
|
||||
|
||||
**3. Check for side effects:**
|
||||
- Could this break other features?
|
||||
- Are there edge cases not considered?
|
||||
|
||||
### Second Pass: Verify the Fix
|
||||
|
||||
<thinking>
|
||||
Evaluate the fix systematically:
|
||||
1. Does this fix address the root cause or just symptoms?
|
||||
2. Are there edge cases not covered?
|
||||
3. Could this break other functionality?
|
||||
4. Is the fix localized or does it ripple through the codebase?
|
||||
5. How do we prevent this bug from returning?
|
||||
</thinking>
|
||||
|
||||
**4. Code changes:**
|
||||
- Does the fix make sense?
|
||||
- Is it the simplest solution?
|
||||
- Any unnecessary changes included?
|
||||
|
||||
**5. Testing:**
|
||||
- Is there a regression test?
|
||||
- Does test verify the bug is fixed?
|
||||
- Are edge cases covered?
|
||||
|
||||
**6. Related code:**
|
||||
- Same pattern in other places that might have same bug?
|
||||
- Should other similar code be fixed too?
|
||||
|
||||
## What to CHECK
|
||||
|
||||
✅ **Root Cause Analysis**
|
||||
- Does the fix address the root cause or just symptoms?
|
||||
- Is the explanation in PR/commit clear?
|
||||
|
||||
✅ **Regression Testing**
|
||||
- Is there a new test that would fail without this fix?
|
||||
- Does test cover the reported bug scenario?
|
||||
- Are related edge cases tested?
|
||||
|
||||
✅ **Side Effects**
|
||||
- Could this break existing functionality?
|
||||
- Are there similar code paths that need checking?
|
||||
- Does this change behavior in unexpected ways?
|
||||
|
||||
✅ **Fix Scope**
|
||||
- Is the fix appropriately scoped (not too broad, not too narrow)?
|
||||
- Are all instances of the bug fixed?
|
||||
- Any related bugs discovered during investigation?
|
||||
|
||||
## What to SKIP
|
||||
|
||||
❌ **Full Architecture Review** - Unless fix reveals architectural problems
|
||||
❌ **Comprehensive Testing Review** - Focus on regression tests, not entire test suite
|
||||
❌ **Major Refactoring Suggestions** - Unless directly related to preventing similar bugs
|
||||
|
||||
## Red Flags
|
||||
|
||||
🚩 **No test for the bug** - How will we prevent regression?
|
||||
🚩 **Fix doesn't match root cause** - Is this fixing symptoms?
|
||||
🚩 **Broad changes beyond the bug** - Should this be split into separate PRs?
|
||||
🚩 **Similar patterns elsewhere** - Should those be fixed too?
|
||||
|
||||
## Key Questions to Ask
|
||||
|
||||
Use `reference/review-psychology.md` for phrasing:
|
||||
|
||||
- "Can we add a test that would fail without this fix?"
|
||||
- "I see this pattern in [other file] - does it have the same issue?"
|
||||
- "Is this fixing the root cause or masking the symptom?"
|
||||
- "Could this change affect [related feature]?"
|
||||
|
||||
## Prioritizing Findings
|
||||
|
||||
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
|
||||
|
||||
## Output Format
|
||||
|
||||
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE / REQUEST CHANGES
|
||||
|
||||
**Critical Issues** (if any):
|
||||
- [One-line summary of each critical blocking issue with file:line reference]
|
||||
|
||||
See inline comments for all issue details.
|
||||
```
|
||||
|
||||
## Example Review
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE
|
||||
|
||||
See inline comments for suggested improvements.
|
||||
```
|
||||
|
||||
**Inline comment examples:**
|
||||
|
||||
```
|
||||
**data/auth/BiometricRepository.kt:120** - SUGGESTED: Extract null handling
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
Root cause analysis: BiometricPrompt result was nullable but code assumed non-null, causing crash on cancellation (PM-12345).
|
||||
|
||||
Consider extracting null handling pattern:
|
||||
|
||||
\```kotlin
|
||||
private fun handleBiometricResult(result: BiometricPrompt.AuthenticationResult?): AuthResult {
|
||||
return result?.let { AuthResult.Success(it) } ?: AuthResult.Cancelled
|
||||
}
|
||||
\```
|
||||
|
||||
This pattern could be reused if we add other biometric auth points.
|
||||
</details>
|
||||
```
|
||||
|
||||
```
|
||||
**app/auth/BiometricViewModel.kt:89** - SUGGESTED: Add regression test
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
Add test for cancellation scenario to prevent regression:
|
||||
|
||||
\```kotlin
|
||||
@Test
|
||||
fun `when biometric cancelled then returns cancelled state`() = runTest {
|
||||
coEvery { repository.authenticate() } returns Result.failure(CancelledException())
|
||||
viewModel.onBiometricAuth()
|
||||
assertEquals(AuthState.Cancelled, viewModel.state.value)
|
||||
}
|
||||
\```
|
||||
|
||||
This prevents regression of the bug just fixed.
|
||||
</details>
|
||||
```
|
||||
166
.claude/skills/reviewing-changes/checklists/dependency-update.md
Normal file
166
.claude/skills/reviewing-changes/checklists/dependency-update.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Dependency Update Review Checklist
|
||||
|
||||
## Multi-Pass Strategy
|
||||
|
||||
### First Pass: Identify and Assess
|
||||
|
||||
<thinking>
|
||||
Before diving into details:
|
||||
1. Which dependencies were updated?
|
||||
2. What are the version changes? (patch, minor, major)
|
||||
3. Are any security-sensitive libraries involved? (crypto, auth, networking)
|
||||
4. Any pre-release versions (alpha, beta, RC)?
|
||||
5. What's the blast radius if something breaks?
|
||||
</thinking>
|
||||
|
||||
**1. Identify the change:**
|
||||
- Which library? Old version → New version?
|
||||
- Major (X.0.0), Minor (0.X.0), or Patch (0.0.X) version change?
|
||||
- Single dependency or multiple?
|
||||
|
||||
**2. Check compilation safety:**
|
||||
- Any imports in codebase that might break?
|
||||
- Any deprecated APIs we're currently using?
|
||||
- Check if this is a breaking change version
|
||||
|
||||
### Second Pass: Deep Analysis
|
||||
|
||||
<thinking>
|
||||
For each dependency update:
|
||||
1. What changes are in this release?
|
||||
2. Are there breaking changes?
|
||||
3. Are there security fixes?
|
||||
4. Do we use the affected APIs?
|
||||
5. How does this affect our codebase?
|
||||
</thinking>
|
||||
|
||||
**3. Review release notes** (if available):
|
||||
- Breaking changes mentioned?
|
||||
- Security fixes included?
|
||||
- New features we should know about?
|
||||
- Deprecations that affect our usage?
|
||||
|
||||
**4. Verify consistency:**
|
||||
- If updating androidx library, are related libraries updated consistently?
|
||||
- BOM (Bill of Materials) consistency if applicable?
|
||||
- Test dependencies updated alongside main dependencies?
|
||||
|
||||
## What to CHECK
|
||||
|
||||
✅ **Compilation Safety**
|
||||
- Look for API deprecations in our codebase
|
||||
- Check if import statements still valid
|
||||
- Major version bumps require extra scrutiny
|
||||
- Beta/alpha versions need stability assessment
|
||||
|
||||
✅ **Security Implications** (if applicable)
|
||||
- Security-related libraries (crypto, auth, networking)?
|
||||
- Check for CVEs addressed in release notes
|
||||
- Review security advisories for this library
|
||||
|
||||
✅ **Testing Implications**
|
||||
- Does this affect test utilities?
|
||||
- Are there breaking changes in test APIs?
|
||||
- Do existing tests still cover the same scenarios?
|
||||
|
||||
✅ **Changelog Review**
|
||||
- Read release notes for breaking changes
|
||||
- Note any behavioral changes
|
||||
- Check migration guides if major version
|
||||
|
||||
## What to SKIP
|
||||
|
||||
❌ **Full Architecture Review** - No code changed, patterns unchanged
|
||||
❌ **Code Style Review** - No code to review
|
||||
❌ **New Test Requirements** - Unless API changed significantly
|
||||
❌ **Security Deep-Dive** - Unless crypto/auth/networking library
|
||||
❌ **Performance Analysis** - Unless release notes mention performance changes
|
||||
|
||||
## Red Flags (Escalate to Full Review)
|
||||
|
||||
🚩 **Major version bump** (e.g., 1.x → 2.0) - Read `checklists/feature-addition.md`
|
||||
🚩 **Security/crypto library** - Read `reference/architectural-patterns.md` and `docs/ARCHITECTURE.md#security`
|
||||
🚩 **Breaking changes in release notes** - Read relevant code sections carefully
|
||||
🚩 **Multiple dependency updates at once** - Check for interaction risks
|
||||
🚩 **Beta/Alpha versions** - Assess stability concerns and rollback plan
|
||||
|
||||
If any red flags present, escalate to more comprehensive review using appropriate checklist.
|
||||
|
||||
## Prioritizing Findings
|
||||
|
||||
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
|
||||
|
||||
## Output Format
|
||||
|
||||
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE / REQUEST CHANGES
|
||||
|
||||
**Critical Issues** (if any):
|
||||
- [One-line summary of each critical blocking issue with file:line reference]
|
||||
|
||||
See inline comments for all issue details.
|
||||
```
|
||||
|
||||
## Example Reviews
|
||||
|
||||
### Example 1: Simple Patch Version (No Critical Issues)
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE
|
||||
|
||||
See inline comments for all issue details.
|
||||
```
|
||||
|
||||
**Inline comment example:**
|
||||
```
|
||||
**libs.versions.toml:45** - SUGGESTED: Beta version in production
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
androidx.credentials updated from 1.5.0 to 1.6.0-beta03
|
||||
|
||||
Monitor for stability issues - beta releases may have unexpected behavior in production.
|
||||
|
||||
Changelog: Adds support for additional credential types, internal bug fixes.
|
||||
</details>
|
||||
```
|
||||
|
||||
### Example 2: Major Version with Breaking Changes (With Critical Issues)
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** REQUEST CHANGES
|
||||
|
||||
**Critical Issues:**
|
||||
- Breaking API changes in Retrofit 3.0.0 (network/api/BitwardenApiService.kt)
|
||||
- Breaking API changes in Retrofit 3.0.0 (network/api/VaultApiService.kt)
|
||||
|
||||
See inline comments for migration details.
|
||||
```
|
||||
|
||||
**Inline comment example:**
|
||||
```
|
||||
**network/api/BitwardenApiService.kt:15** - CRITICAL: Breaking API changes
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
Retrofit 3.0.0 removes `Call<T>` return type. Migration required:
|
||||
|
||||
\```kotlin
|
||||
// Before
|
||||
fun getUser(): Call<UserResponse>
|
||||
|
||||
// After
|
||||
suspend fun getUser(): Response<UserResponse>
|
||||
\```
|
||||
|
||||
Update all API service interfaces to use suspend functions, update call sites to use coroutines instead of enqueue/execute, and update tests accordingly.
|
||||
|
||||
Consider creating a separate PR for this migration due to scope.
|
||||
|
||||
Reference: https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-300
|
||||
</details>
|
||||
```
|
||||
380
.claude/skills/reviewing-changes/checklists/feature-addition.md
Normal file
380
.claude/skills/reviewing-changes/checklists/feature-addition.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# Feature Addition Review Checklist
|
||||
|
||||
## Multi-Pass Strategy
|
||||
|
||||
### First Pass: High-Level Assessment
|
||||
|
||||
<thinking>
|
||||
Before diving into details:
|
||||
1. What is this feature supposed to do?
|
||||
2. How does it fit into the existing architecture?
|
||||
3. What are the security implications?
|
||||
4. What's the scope? (files touched, modules affected)
|
||||
5. What are the highest-risk areas?
|
||||
</thinking>
|
||||
|
||||
**1. Understand the feature:**
|
||||
- Read PR description - what problem does this solve?
|
||||
- Identify user-facing changes vs internal changes
|
||||
- Note any security implications (auth, encryption, data handling)
|
||||
|
||||
**2. Scan file structure:**
|
||||
- Which modules affected? (app, data, network, ui, core?)
|
||||
- Are files organized correctly per module structure?
|
||||
- Any new public APIs introduced?
|
||||
|
||||
**3. Initial risk assessment:**
|
||||
- Does this touch sensitive data or security-critical paths?
|
||||
- Does this affect existing features or only add new ones?
|
||||
- Are there obvious compilation or null safety issues?
|
||||
|
||||
### Second Pass: Architecture Deep-Dive
|
||||
|
||||
<thinking>
|
||||
Verify architectural integrity:
|
||||
1. Does this follow MVVM + UDF pattern?
|
||||
2. Is Hilt DI used correctly?
|
||||
3. Is state management proper (StateFlow, immutability)?
|
||||
4. Are modules organized correctly?
|
||||
5. Is error handling robust (Result types)?
|
||||
</thinking>
|
||||
|
||||
**4. MVVM + UDF Pattern Compliance:**
|
||||
- ViewModels properly structured?
|
||||
- State management using StateFlow?
|
||||
- Business logic in correct layer?
|
||||
|
||||
**5. Dependency Injection:**
|
||||
- Hilt DI used correctly?
|
||||
- Dependencies injected, not manually instantiated?
|
||||
- Proper scoping applied?
|
||||
|
||||
**6. Module Organization:**
|
||||
- Code placed in correct modules?
|
||||
- No circular dependencies introduced?
|
||||
- Proper separation of concerns?
|
||||
|
||||
**7. Error Handling:**
|
||||
- Using Result types, not exception-based handling?
|
||||
- Errors propagated correctly through layers?
|
||||
|
||||
### Third Pass: Details and Quality
|
||||
|
||||
<thinking>
|
||||
Check quality and completeness:
|
||||
1. Is code quality high? (null safety, documentation, naming)
|
||||
2. Are tests comprehensive? (unit + integration)
|
||||
3. Are there edge cases not covered?
|
||||
4. Is documentation clear?
|
||||
5. Are there any code smells or anti-patterns?
|
||||
</thinking>
|
||||
|
||||
**8. Testing:**
|
||||
- Unit tests for ViewModels and repositories?
|
||||
- Test coverage for edge cases and error scenarios?
|
||||
- Tests verify behavior, not implementation?
|
||||
|
||||
**9. Code Quality:**
|
||||
- Null safety handled properly?
|
||||
- Public APIs have KDoc documentation?
|
||||
- Naming follows project conventions?
|
||||
|
||||
**10. Security:**
|
||||
- Sensitive data encrypted properly?
|
||||
- Authentication/authorization handled correctly?
|
||||
- Zero-knowledge architecture preserved?
|
||||
|
||||
## Architecture Review
|
||||
|
||||
### MVVM Pattern Compliance
|
||||
|
||||
Read `reference/architectural-patterns.md` for detailed patterns.
|
||||
|
||||
**ViewModels must:**
|
||||
- Use `@HiltViewModel` annotation
|
||||
- Use `@Inject constructor`
|
||||
- Expose `StateFlow<T>`, NOT `MutableStateFlow<T>` publicly
|
||||
- Delegate business logic to Repository/Manager
|
||||
- Avoid direct Android framework dependencies (except ViewModel, SavedStateHandle)
|
||||
|
||||
**Common Violations:**
|
||||
```kotlin
|
||||
// ❌ BAD - Exposes mutable state
|
||||
class FeatureViewModel @Inject constructor() : ViewModel() {
|
||||
val state: MutableStateFlow<State> = MutableStateFlow(State.Initial)
|
||||
}
|
||||
|
||||
// ✅ GOOD - Exposes immutable state
|
||||
class FeatureViewModel @Inject constructor() : ViewModel() {
|
||||
private val _state = MutableStateFlow<State>(State.Initial)
|
||||
val state: StateFlow<State> = _state.asStateFlow()
|
||||
}
|
||||
|
||||
// ❌ BAD - Business logic in ViewModel
|
||||
fun onSubmit() {
|
||||
val encrypted = encryptionManager.encrypt(password) // Should be in Repository
|
||||
_state.value = State.Success
|
||||
}
|
||||
|
||||
// ✅ GOOD - Business logic in Repository, state updated via internal event
|
||||
fun onSubmit() {
|
||||
viewModelScope.launch {
|
||||
// The result of the async operation is captured
|
||||
val result = repository.submitData(password)
|
||||
// A single event is sent with the result, not updating state directly
|
||||
sendAction(FeatureAction.Internal.SubmissionComplete(result))
|
||||
}
|
||||
}
|
||||
|
||||
// The ViewModel has a handler that processes the internal event
|
||||
private fun handleInternalAction(action: FeatureAction.Internal) {
|
||||
when (action) {
|
||||
is FeatureAction.Internal.SubmissionComplete -> {
|
||||
// The event handler evaluates the result and updates state
|
||||
action.result.fold(
|
||||
onSuccess = { _state.value = State.Success },
|
||||
onFailure = { _state.value = State.Error(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**UI Layer must:**
|
||||
- Only observe state, never modify
|
||||
- Pass user actions as events to ViewModel
|
||||
- Contain no business logic
|
||||
- Use existing UI components from `:ui` module where possible
|
||||
|
||||
### Hilt Dependency Injection
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#dependency-injection`
|
||||
|
||||
**Required Patterns:**
|
||||
- ViewModels: `@HiltViewModel` + `@Inject constructor`
|
||||
- Repositories: `@Inject constructor` on implementation
|
||||
- Inject interfaces, not concrete implementations
|
||||
- Modules must provide proper scoping (`@Singleton`, `@ViewModelScoped`)
|
||||
|
||||
**Common Violations:**
|
||||
```kotlin
|
||||
// ❌ BAD - Manual instantiation
|
||||
class FeatureViewModel : ViewModel() {
|
||||
private val repository = FeatureRepositoryImpl()
|
||||
}
|
||||
|
||||
// ✅ GOOD - Injected interface
|
||||
@HiltViewModel
|
||||
class FeatureViewModel @Inject constructor(
|
||||
private val repository: FeatureRepository // Interface, not implementation
|
||||
) : ViewModel()
|
||||
|
||||
// ❌ BAD - Injecting implementation
|
||||
class FeatureViewModel @Inject constructor(
|
||||
private val repository: FeatureRepositoryImpl // Should inject interface
|
||||
)
|
||||
|
||||
// ✅ GOOD - Interface injection
|
||||
class FeatureViewModel @Inject constructor(
|
||||
private val repository: FeatureRepository // Interface
|
||||
)
|
||||
```
|
||||
|
||||
### Module Organization
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#module-structure`
|
||||
|
||||
**Correct Placement:**
|
||||
- `:core` - Shared utilities (cryptography, analytics, logging)
|
||||
- `:data` - Repositories, database, domain models
|
||||
- `:network` - API clients, network utilities
|
||||
- `:ui` - Reusable Compose components, theme
|
||||
- `:app` - Feature screens, ViewModels, navigation
|
||||
- `:authenticator` - Authenticator app (separate from password manager)
|
||||
|
||||
**Check:**
|
||||
- UI code in `:ui` or `:app` modules
|
||||
- Data models in `:data`
|
||||
- Network clients in `:network`
|
||||
- No circular dependencies between modules
|
||||
|
||||
### Error Handling
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#error-handling`
|
||||
|
||||
**Required Pattern - Use Result types:**
|
||||
```kotlin
|
||||
// ✅ GOOD - Result type
|
||||
suspend fun fetchData(): Result<Data> = runCatching {
|
||||
apiService.getData()
|
||||
}
|
||||
|
||||
// ViewModel handles Result
|
||||
repository.fetchData().fold(
|
||||
onSuccess = { data -> _state.value = State.Success(data) },
|
||||
onFailure = { error -> _state.value = State.Error(error) }
|
||||
)
|
||||
|
||||
// ❌ BAD - Exception-based in business logic
|
||||
suspend fun fetchData(): Data {
|
||||
try {
|
||||
return apiService.getData()
|
||||
} catch (e: Exception) {
|
||||
throw FeatureException(e) // Don't throw in business logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Review
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#security`
|
||||
|
||||
**Critical Security Checks:**
|
||||
|
||||
- **Sensitive data encrypted**: Passwords, keys, tokens use Android Keystore or EncryptedSharedPreferences
|
||||
- **No plaintext secrets**: No passwords/keys in logs, memory dumps, or SharedPreferences
|
||||
- **Input validation**: All user-provided data validated and sanitized
|
||||
- **Authentication tokens**: Securely stored and transmitted
|
||||
- **Zero-knowledge architecture**: Encryption happens client-side, server never sees plaintext
|
||||
|
||||
**Red Flags:**
|
||||
```kotlin
|
||||
// ❌ CRITICAL - Plaintext storage
|
||||
sharedPreferences.edit {
|
||||
putString("pin", userPin) // Must use EncryptedSharedPreferences
|
||||
}
|
||||
|
||||
// ❌ CRITICAL - Logging sensitive data
|
||||
Log.d("Auth", "Password: $password") // Never log sensitive data
|
||||
|
||||
// ❌ CRITICAL - Weak encryption
|
||||
val cipher = Cipher.getInstance("DES") // Use AES-256-GCM
|
||||
|
||||
// ✅ GOOD - Keystore encryption
|
||||
val encryptedData = keystoreManager.encrypt(sensitiveData)
|
||||
secureStorage.store(encryptedData)
|
||||
```
|
||||
|
||||
**If security concerns found, classify as CRITICAL using `reference/priority-framework.md`**
|
||||
|
||||
## Testing Review
|
||||
|
||||
Reference: `reference/testing-patterns.md`
|
||||
|
||||
**Required Test Coverage:**
|
||||
|
||||
- **ViewModels**: Unit tests for state transitions, actions, error scenarios
|
||||
- **Repositories**: Unit tests for data transformations, error handling
|
||||
- **Business logic**: Unit tests for complex algorithms, calculations
|
||||
- **Edge cases**: Null inputs, empty states, network failures, concurrent operations
|
||||
|
||||
**Test Quality:**
|
||||
```kotlin
|
||||
// ✅ GOOD - Tests behavior
|
||||
@Test
|
||||
fun `when login succeeds then state updates to success`() = runTest {
|
||||
val viewModel = LoginViewModel(mockRepository)
|
||||
|
||||
coEvery { mockRepository.login(any(), any()) } returns Result.success(User())
|
||||
|
||||
viewModel.onLoginClicked("user", "pass")
|
||||
|
||||
viewModel.state.test {
|
||||
assertEquals(LoginState.Success, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ BAD - Tests implementation
|
||||
@Test
|
||||
fun `repository is called with correct parameters`() {
|
||||
// This is testing internal implementation, not behavior
|
||||
}
|
||||
```
|
||||
|
||||
**Testing Frameworks:**
|
||||
- JUnit 5 for test structure
|
||||
- MockK for mocking
|
||||
- Turbine for Flow testing
|
||||
- Kotlinx-coroutines-test for coroutine testing
|
||||
|
||||
## Code Quality
|
||||
|
||||
### Null Safety
|
||||
|
||||
- No `!!` (non-null assertion) without clear safety guarantee
|
||||
- Platform types (from Java) handled with explicit nullability
|
||||
- Nullable types have proper null checks or use safe operators (`?.`, `?:`)
|
||||
|
||||
```kotlin
|
||||
// ❌ BAD - Unsafe assertion
|
||||
val result = apiService.getData()!! // Could crash
|
||||
|
||||
// ✅ GOOD - Safe handling
|
||||
val result = apiService.getData() ?: return State.Error("No data")
|
||||
|
||||
// ❌ BAD - Platform type unchecked
|
||||
val intent: Intent = getIntent() // Could be null from Java
|
||||
intent.getStringExtra("key") // Potential NPE
|
||||
|
||||
// ✅ GOOD - Explicit nullability
|
||||
val intent: Intent? = getIntent()
|
||||
intent?.getStringExtra("key")
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- **Public APIs**: Have KDoc comments explaining purpose, parameters, return values
|
||||
- **Complex algorithms**: Explained in comments
|
||||
- **Non-obvious behavior**: Documented with rationale
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Documented public API
|
||||
/**
|
||||
* Encrypts the given data using AES-256-GCM with a key from Android Keystore.
|
||||
*
|
||||
* @param plaintext The data to encrypt
|
||||
* @return Result containing encrypted data or encryption error
|
||||
*/
|
||||
suspend fun encrypt(plaintext: ByteArray): Result<EncryptedData>
|
||||
```
|
||||
|
||||
### Style Compliance
|
||||
|
||||
Reference: `docs/STYLE_AND_BEST_PRACTICES.md`
|
||||
|
||||
Only flag style issues if:
|
||||
- Not caught by linters (Detekt, ktlint)
|
||||
- Have architectural implications
|
||||
- Significantly impact readability
|
||||
|
||||
Skip minor formatting (spaces, line breaks, etc.) - linters handle this.
|
||||
|
||||
## Prioritizing Findings
|
||||
|
||||
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
|
||||
|
||||
## Providing Feedback
|
||||
|
||||
Use `reference/review-psychology.md` for phrasing guidance.
|
||||
|
||||
**Key principles:**
|
||||
- **Ask questions** for design decisions: "Can we use the existing BitwardenTextField component here?"
|
||||
- **Be prescriptive** for clear violations: "Change MutableStateFlow to StateFlow (MVVM pattern requirement)"
|
||||
- **Explain rationale**: "This exposes mutable state, violating unidirectional data flow"
|
||||
- **Use I-statements**: "It's hard for me to understand this logic without comments"
|
||||
- **Avoid condescension**: Don't use "just", "simply", "obviously"
|
||||
|
||||
## Output Format
|
||||
|
||||
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
|
||||
|
||||
See `examples/review-outputs.md` for comprehensive feature review example.
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE / REQUEST CHANGES
|
||||
|
||||
**Critical Issues** (if any):
|
||||
- [One-line summary of each critical blocking issue with file:line reference]
|
||||
|
||||
See inline comments for all issue details.
|
||||
```
|
||||
250
.claude/skills/reviewing-changes/checklists/infrastructure.md
Normal file
250
.claude/skills/reviewing-changes/checklists/infrastructure.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Infrastructure Review Checklist
|
||||
|
||||
## Multi-Pass Strategy
|
||||
|
||||
### First Pass: Understand the Change
|
||||
|
||||
<thinking>
|
||||
Assess infrastructure change:
|
||||
1. What problem does this solve?
|
||||
2. Does this affect production builds, CI/CD, or dev workflow?
|
||||
3. What's the risk if this breaks?
|
||||
4. Can this be tested before merge?
|
||||
5. What's the rollback plan?
|
||||
</thinking>
|
||||
|
||||
**1. Identify the goal:**
|
||||
- What problem does this solve?
|
||||
- Is this optimization, fix, or new capability?
|
||||
- What's the expected impact?
|
||||
|
||||
**2. Assess risk:**
|
||||
- Does this affect production builds?
|
||||
- Could this break CI/CD pipelines?
|
||||
- Impact on developer workflow?
|
||||
|
||||
**3. Performance implications:**
|
||||
- Will builds be faster or slower?
|
||||
- CI time impact?
|
||||
- Resource usage changes?
|
||||
|
||||
### Second Pass: Verify Implementation
|
||||
|
||||
<thinking>
|
||||
Verify configuration and impact:
|
||||
1. Is the configuration syntax valid?
|
||||
2. Are secrets/credentials handled securely?
|
||||
3. What's the impact on build times and CI performance?
|
||||
4. How will this affect the team's workflow?
|
||||
5. Is there adequate testing/validation?
|
||||
</thinking>
|
||||
|
||||
**4. Configuration correctness:**
|
||||
- Syntax valid?
|
||||
- References correct?
|
||||
- Secrets/credentials handled securely?
|
||||
|
||||
**5. Impact analysis:**
|
||||
- What workflows/builds are affected?
|
||||
- Rollback plan if this breaks?
|
||||
- Documentation for team?
|
||||
|
||||
**6. Testing strategy:**
|
||||
- How can this be tested before merge?
|
||||
- Canary/gradual rollout possible?
|
||||
- Monitoring for issues post-merge?
|
||||
|
||||
## What to CHECK
|
||||
|
||||
✅ **Configuration Correctness**
|
||||
- YAML/Groovy syntax valid
|
||||
- File references correct
|
||||
- Version numbers/tags valid
|
||||
- Conditional logic sound
|
||||
|
||||
✅ **Security**
|
||||
- No hardcoded secrets or credentials
|
||||
- GitHub secrets used properly
|
||||
- Permissions appropriately scoped
|
||||
- No sensitive data in logs
|
||||
|
||||
✅ **Performance Impact**
|
||||
- Build time implications understood
|
||||
- CI queue time impact assessed
|
||||
- Resource usage reasonable
|
||||
|
||||
✅ **Rollback Plan**
|
||||
- Can this be reverted easily?
|
||||
- Dependencies on other changes?
|
||||
- Gradual rollout possible?
|
||||
|
||||
✅ **Documentation**
|
||||
- Changes documented for team?
|
||||
- README or CONTRIBUTING updated?
|
||||
- Breaking changes clearly noted?
|
||||
|
||||
## What to SKIP
|
||||
|
||||
❌ **Bikeshedding Configuration** - Unless clear performance/maintenance benefit
|
||||
❌ **Over-Optimization** - Unless current system has proven problems
|
||||
❌ **Suggesting Major Rewrites** - Unless current approach is fundamentally broken
|
||||
|
||||
## Red Flags
|
||||
|
||||
🚩 **Hardcoded secrets** - Use GitHub secrets or secure storage
|
||||
🚩 **No rollback plan** - Critical infrastructure should be revertible
|
||||
🚩 **Untested changes** - CI changes should be validated
|
||||
🚩 **Breaking changes without notice** - Team needs advance warning
|
||||
🚩 **Performance regression** - Builds shouldn't get significantly slower
|
||||
|
||||
## Key Questions to Ask
|
||||
|
||||
Use `reference/review-psychology.md` for phrasing:
|
||||
|
||||
- "What's the rollback plan if this breaks CI?"
|
||||
- "Can we test this on a feature branch before main?"
|
||||
- "Will this impact build times? By how much?"
|
||||
- "Should this be documented in CONTRIBUTING.md?"
|
||||
|
||||
## Common Infrastructure Patterns
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
# ✅ GOOD - Secure, clear, tested
|
||||
name: Build and Test
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # Prevent runaway builds
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run tests
|
||||
env:
|
||||
API_KEY: ${{ secrets.API_KEY }} # Secure secret usage
|
||||
run: ./gradlew test
|
||||
|
||||
# ❌ BAD - Insecure, unclear
|
||||
name: Build
|
||||
on: push # Too broad, runs on all branches
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
# No timeout - could run forever
|
||||
steps:
|
||||
- run: |
|
||||
export API_KEY="hardcoded_key_here" # Hardcoded secret!
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
### Gradle Configuration
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Clear, maintainable
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx) // Version catalog
|
||||
implementation(libs.hilt.android)
|
||||
|
||||
testImplementation(libs.junit5)
|
||||
testImplementation(libs.mockk)
|
||||
}
|
||||
|
||||
// ❌ BAD - Hardcoded versions
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.12.0") // Hardcoded version
|
||||
implementation("com.google.dagger:hilt-android:2.48")
|
||||
}
|
||||
```
|
||||
|
||||
### Build Optimization
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Parallel, cached
|
||||
tasks.register("checkAll") {
|
||||
dependsOn("detekt", "ktlintCheck", "testStandardDebug")
|
||||
group = "verification"
|
||||
description = "Run all checks in parallel"
|
||||
|
||||
// Enable caching for faster builds
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
// ❌ BAD - Sequential, no caching
|
||||
tasks.register("checkAll") {
|
||||
doLast {
|
||||
exec { commandLine("./gradlew", "detekt") }
|
||||
exec { commandLine("./gradlew", "ktlintCheck") } // Sequential
|
||||
exec { commandLine("./gradlew", "test") }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Prioritizing Findings
|
||||
|
||||
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
|
||||
|
||||
## Output Format
|
||||
|
||||
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE / REQUEST CHANGES
|
||||
|
||||
**Critical Issues** (if any):
|
||||
- [One-line summary of each critical blocking issue with file:line reference]
|
||||
|
||||
See inline comments for all issue details.
|
||||
```
|
||||
|
||||
## Example Review
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Optimizes CI build by parallelizing test execution and caching dependencies
|
||||
|
||||
Impact: Estimated 40% reduction in CI time (12 min → 7 min per build)
|
||||
|
||||
## Critical Issues
|
||||
None
|
||||
|
||||
## Suggested Improvements
|
||||
|
||||
**.github/workflows/build.yml:23** - Add timeout for safety
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # Prevent builds from hanging
|
||||
steps:
|
||||
# ...
|
||||
```
|
||||
This prevents runaway builds if something goes wrong.
|
||||
|
||||
**.github/workflows/build.yml:45** - Consider matrix strategy for module tests
|
||||
Can we run module tests in parallel using a matrix strategy?
|
||||
```yaml
|
||||
strategy:
|
||||
matrix:
|
||||
module: [app, data, network, ui]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: ./gradlew :${{ matrix.module }}:test
|
||||
```
|
||||
This could further reduce CI time.
|
||||
|
||||
**build.gradle.kts:12** - Document caching strategy
|
||||
Can we add a comment explaining the caching configuration?
|
||||
Future maintainers will appreciate understanding why these specific cache keys are used.
|
||||
|
||||
## Rollback Plan
|
||||
If CI breaks:
|
||||
- Revert commit: `git revert [commit-hash]`
|
||||
- Previous workflow available at: `.github/workflows/build.yml@main^`
|
||||
- Monitor CI times at: https://github.com/[org]/[repo]/actions
|
||||
```
|
||||
294
.claude/skills/reviewing-changes/checklists/refactoring.md
Normal file
294
.claude/skills/reviewing-changes/checklists/refactoring.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# Refactoring Review Checklist
|
||||
|
||||
## Multi-Pass Strategy
|
||||
|
||||
### First Pass: Understand the Refactoring
|
||||
|
||||
<thinking>
|
||||
Analyze the refactoring scope:
|
||||
1. What pattern is being improved?
|
||||
2. Why is this refactoring needed?
|
||||
3. Does this change behavior or just structure?
|
||||
4. What's the scope? (files affected, migration completeness)
|
||||
5. What are the risks if something breaks?
|
||||
</thinking>
|
||||
|
||||
**1. Understand the goal:**
|
||||
- What pattern is being improved?
|
||||
- Why is this refactoring needed?
|
||||
- What's the scope of changes?
|
||||
|
||||
**2. Assess completeness:**
|
||||
- Are all instances refactored or just some?
|
||||
- Are there related areas that should also change?
|
||||
- Is the migration complete or partial?
|
||||
|
||||
**3. Risk assessment:**
|
||||
- Does this change behavior?
|
||||
- How many files affected?
|
||||
- Are tests updated to reflect changes?
|
||||
|
||||
### Second Pass: Verify Consistency
|
||||
|
||||
<thinking>
|
||||
Verify refactoring quality:
|
||||
1. Is the new pattern applied consistently throughout?
|
||||
2. Are there missed instances of the old pattern?
|
||||
3. Do tests still pass with same behavior?
|
||||
4. Is the migration complete or partial?
|
||||
5. Does this introduce any new issues?
|
||||
</thinking>
|
||||
|
||||
**4. Pattern consistency:**
|
||||
- Is the new pattern applied consistently throughout?
|
||||
- Are there missed instances of the old pattern?
|
||||
- Does this match established project patterns?
|
||||
|
||||
**5. Migration completeness:**
|
||||
- Old pattern fully removed or deprecated?
|
||||
- All usages updated?
|
||||
- Documentation updated?
|
||||
|
||||
**6. Test coverage:**
|
||||
- Do tests still pass?
|
||||
- Are tests refactored to match?
|
||||
- Does behavior remain unchanged?
|
||||
|
||||
## What to CHECK
|
||||
|
||||
✅ **Pattern Consistency**
|
||||
- New pattern applied consistently across all touched code
|
||||
- Follows established project patterns (MVVM, DI, error handling)
|
||||
- No mix of old and new patterns
|
||||
|
||||
✅ **Migration Completeness**
|
||||
- All instances of old pattern updated?
|
||||
- Deprecated methods removed or marked @Deprecated?
|
||||
- Related code also updated (tests, docs)?
|
||||
|
||||
✅ **Behavior Preservation**
|
||||
- Refactoring doesn't change behavior
|
||||
- Tests still pass
|
||||
- Edge cases still handled
|
||||
|
||||
✅ **Deprecation Strategy** (if applicable)
|
||||
- Old APIs marked @Deprecated with migration guidance
|
||||
- Replacement clearly documented
|
||||
- Timeline for removal specified
|
||||
|
||||
## What to SKIP
|
||||
|
||||
❌ **Suggesting Additional Refactorings** - Unless directly related to current changes
|
||||
❌ **Scope Creep** - Don't request refactoring of untouched code
|
||||
❌ **Perfection** - Better code is better than perfect code
|
||||
|
||||
## Red Flags
|
||||
|
||||
🚩 **Incomplete migration** - Mix of old and new patterns
|
||||
🚩 **Behavior changes** - Refactoring shouldn't change behavior
|
||||
🚩 **Broken tests** - Tests should be updated to match refactoring
|
||||
🚩 **Undocumented pattern** - New pattern should be clear to team
|
||||
|
||||
## Key Questions to Ask
|
||||
|
||||
Use `reference/review-psychology.md` for phrasing:
|
||||
|
||||
- "I see the old pattern still used in [file:line] - should that be updated too?"
|
||||
- "Can we add @Deprecated to the old method with migration guidance?"
|
||||
- "How do we ensure this behavior remains the same?"
|
||||
- "Should this pattern be documented in ARCHITECTURE.md?"
|
||||
|
||||
## Common Refactoring Patterns
|
||||
|
||||
### Extract Interface/Repository
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Complete migration
|
||||
interface FeatureRepository {
|
||||
suspend fun getData(): Result<Data>
|
||||
}
|
||||
|
||||
class FeatureRepositoryImpl @Inject constructor(
|
||||
private val apiService: FeatureApiService
|
||||
) : FeatureRepository {
|
||||
override suspend fun getData(): Result<Data> = runCatching {
|
||||
apiService.fetchData()
|
||||
}
|
||||
}
|
||||
|
||||
// All usages updated to inject interface
|
||||
class FeatureViewModel @Inject constructor(
|
||||
private val repository: FeatureRepository // Interface
|
||||
) : ViewModel()
|
||||
|
||||
// ❌ BAD - Incomplete migration
|
||||
// Some files still inject FeatureRepositoryImpl directly
|
||||
```
|
||||
|
||||
### Modernize Error Handling
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Complete migration
|
||||
// Old exception-based removed
|
||||
suspend fun fetchData(): Result<Data> = runCatching {
|
||||
apiService.getData()
|
||||
}
|
||||
|
||||
// All call sites updated
|
||||
repository.fetchData().fold(
|
||||
onSuccess = { /* handle */ },
|
||||
onFailure = { /* handle */ }
|
||||
)
|
||||
|
||||
// ❌ BAD - Mixed patterns
|
||||
// Some functions use Result, others still throw exceptions
|
||||
```
|
||||
|
||||
### Extract Reusable Component
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Complete extraction
|
||||
// Component moved to :ui module
|
||||
@Composable
|
||||
fun BitwardenButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
)
|
||||
|
||||
// All usages updated to use new component
|
||||
// Old inline button implementations removed
|
||||
|
||||
// ❌ BAD - Incomplete extraction
|
||||
// Some screens use new component, others still have inline implementation
|
||||
```
|
||||
|
||||
## Prioritizing Findings
|
||||
|
||||
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
|
||||
|
||||
## Output Format
|
||||
|
||||
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE / REQUEST CHANGES
|
||||
|
||||
**Critical Issues** (if any):
|
||||
- [One-line summary of each critical blocking issue with file:line reference]
|
||||
|
||||
See inline comments for all issue details.
|
||||
```
|
||||
|
||||
## Example Reviews
|
||||
|
||||
### Example 1: Refactoring with Incomplete Migration
|
||||
|
||||
**Context**: Refactoring authentication to Repository pattern, but one ViewModel still uses old pattern
|
||||
|
||||
**Summary Comment:**
|
||||
```markdown
|
||||
**Overall Assessment:** REQUEST CHANGES
|
||||
|
||||
**Critical Issues:**
|
||||
- Incomplete migration (app/vault/VaultViewModel.kt:89)
|
||||
|
||||
See inline comments for details.
|
||||
```
|
||||
|
||||
**Inline Comment 1** (on `app/vault/VaultViewModel.kt:89`):
|
||||
```markdown
|
||||
**IMPORTANT**: Incomplete migration
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
This ViewModel still injects AuthManager directly. Should it use AuthRepository like the other 11 ViewModels?
|
||||
|
||||
\```kotlin
|
||||
// Current (old pattern)
|
||||
class VaultViewModel @Inject constructor(
|
||||
private val authManager: AuthManager
|
||||
)
|
||||
|
||||
// Should be (new pattern)
|
||||
class VaultViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository
|
||||
)
|
||||
\```
|
||||
|
||||
This is the only ViewModel still using the old pattern.
|
||||
</details>
|
||||
```
|
||||
|
||||
**Inline Comment 2** (on `data/auth/AuthManager.kt:1`):
|
||||
```markdown
|
||||
**SUGGESTED**: Add deprecation notice
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
Can we add @Deprecated to AuthManager to guide future development?
|
||||
|
||||
\```kotlin
|
||||
@Deprecated(
|
||||
message = "Use AuthRepository interface instead",
|
||||
replaceWith = ReplaceWith("AuthRepository"),
|
||||
level = DeprecationLevel.WARNING
|
||||
)
|
||||
class AuthManager @Inject constructor(...)
|
||||
\```
|
||||
|
||||
This helps prevent new code from using the old pattern.
|
||||
</details>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Clean Refactoring (No Issues)
|
||||
|
||||
**Context**: Refactoring with complete migration, all patterns followed correctly, tests passing
|
||||
|
||||
**Review Comment:**
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE
|
||||
|
||||
Clean refactoring moving ExitManager to :ui module. Follows established patterns, eliminates duplication, tests updated correctly.
|
||||
```
|
||||
|
||||
**Token count:** ~30 tokens (vs ~800 for verbose format)
|
||||
|
||||
**Why this works:**
|
||||
- 3 lines total
|
||||
- Clear approval decision
|
||||
- Briefly notes what was done
|
||||
- No elaborate sections, checkmarks, or excessive praise
|
||||
- Author gets immediate green light to merge
|
||||
|
||||
**What NOT to do for clean refactorings:**
|
||||
```markdown
|
||||
❌ DO NOT create these sections:
|
||||
|
||||
## Summary
|
||||
This PR successfully refactors ExitManager into shared code...
|
||||
|
||||
## Key Strengths
|
||||
- ✅ Follows established module organization patterns
|
||||
- ✅ Removes code duplication between apps
|
||||
- ✅ Improves test coverage
|
||||
- ✅ Maintains consistent behavior
|
||||
[...20 more checkmarks...]
|
||||
|
||||
## Code Quality & Architecture
|
||||
**Architectural Compliance:** ✅
|
||||
- Correctly places manager in :ui module
|
||||
- Follows established pattern for UI-layer managers
|
||||
[...detailed analysis...]
|
||||
|
||||
## Changes
|
||||
- ✅ Moved ExitManager interface from app → ui module
|
||||
- ✅ Moved ExitManagerImpl from app → ui module
|
||||
[...listing every file...]
|
||||
```
|
||||
|
||||
This is excessive. **For clean PRs: 2-3 lines maximum.**
|
||||
233
.claude/skills/reviewing-changes/checklists/ui-refinement.md
Normal file
233
.claude/skills/reviewing-changes/checklists/ui-refinement.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# UI Refinement Review Checklist
|
||||
|
||||
## Multi-Pass Strategy
|
||||
|
||||
### First Pass: Visual Changes
|
||||
|
||||
<thinking>
|
||||
Analyze the UI changes:
|
||||
1. What visual/UX problem is being solved?
|
||||
2. Are there designs or screenshots to reference?
|
||||
3. Is this affecting existing screens or new ones?
|
||||
4. What's the scope of visual changes?
|
||||
5. Are design tokens (colors, spacing, typography) being used correctly?
|
||||
</thinking>
|
||||
|
||||
**1. Understand the changes:**
|
||||
- What visual/UX problem is being solved?
|
||||
- Are there designs or screenshots to reference?
|
||||
- Is this a bug fix or enhancement?
|
||||
|
||||
**2. Component usage:**
|
||||
- Using existing components from `:ui` module?
|
||||
- Any new custom components created?
|
||||
- Could existing components be reused?
|
||||
|
||||
### Second Pass: Implementation Review
|
||||
|
||||
<thinking>
|
||||
Check implementation quality:
|
||||
1. Are Compose best practices followed?
|
||||
2. Is state hoisting applied correctly?
|
||||
3. Are existing components reused where possible?
|
||||
4. Is accessibility properly handled?
|
||||
5. Does this follow design system patterns?
|
||||
</thinking>
|
||||
|
||||
**3. Compose best practices:**
|
||||
- Composables properly structured?
|
||||
- State hoisted correctly?
|
||||
- Preview composables included?
|
||||
|
||||
**4. Accessibility:**
|
||||
- Content descriptions for images/icons?
|
||||
- Semantic properties for screen readers?
|
||||
- Touch targets meet minimum size (48dp)?
|
||||
|
||||
**5. Design consistency:**
|
||||
- Using theme colors, spacing, typography?
|
||||
- Consistent with other screens?
|
||||
- Responsive to different screen sizes?
|
||||
|
||||
## What to CHECK
|
||||
|
||||
✅ **Compose Best Practices**
|
||||
- Composables are stateless where possible
|
||||
- State hoisting follows patterns
|
||||
- Side effects (LaunchedEffect, DisposableEffect) used correctly
|
||||
- Preview composables provided for development
|
||||
|
||||
✅ **Component Reuse**
|
||||
- Using existing BitwardenButton, BitwardenTextField, etc.?
|
||||
- Could custom UI be replaced with existing components?
|
||||
- New reusable components placed in `:ui` module?
|
||||
|
||||
✅ **Accessibility**
|
||||
- `contentDescription` for icons and images
|
||||
- `semantics` for custom interactions
|
||||
- Sufficient contrast ratios
|
||||
- Touch targets ≥ 48dp minimum
|
||||
|
||||
✅ **Design Consistency**
|
||||
- Using `BitwardenTheme` colors (not hardcoded)
|
||||
- Using `BitwardenTheme` spacing (16.dp, 8.dp, etc.)
|
||||
- Using `BitwardenTheme` typography styles
|
||||
- Consistent with existing screen patterns
|
||||
|
||||
✅ **Responsive Design**
|
||||
- Handles different screen sizes?
|
||||
- Scrollable content where appropriate?
|
||||
- Landscape orientation considered?
|
||||
|
||||
## What to SKIP
|
||||
|
||||
❌ **Deep Architecture Review** - Unless ViewModel changes are substantial
|
||||
❌ **Business Logic Review** - Focus is on presentation, not logic
|
||||
❌ **Security Review** - Unless UI exposes sensitive data improperly
|
||||
|
||||
## Red Flags
|
||||
|
||||
🚩 **Duplicating existing components** - Should reuse from `:ui` module
|
||||
🚩 **Hardcoded colors/dimensions** - Should use theme
|
||||
🚩 **Missing accessibility properties** - Critical for screen readers
|
||||
🚩 **State management in UI** - Should be hoisted to ViewModel
|
||||
|
||||
## Key Questions to Ask
|
||||
|
||||
Use `reference/review-psychology.md` for phrasing:
|
||||
|
||||
- "Can we use BitwardenButton here instead of this custom button?"
|
||||
- "Should this color come from BitwardenTheme instead of being hardcoded?"
|
||||
- "How will this look on a small screen?"
|
||||
- "Is there a contentDescription for this icon?"
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Composable Structure
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Stateless, hoisted state
|
||||
@Composable
|
||||
fun FeatureScreen(
|
||||
state: FeatureState,
|
||||
onActionClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// UI rendering only
|
||||
}
|
||||
|
||||
// ❌ BAD - Business state in composable
|
||||
@Composable
|
||||
fun FeatureScreen() {
|
||||
var userData by remember { mutableStateOf<User?>(null) } // Business state should be in ViewModel
|
||||
var isLoading by remember { mutableStateOf(false) } // App state should be in ViewModel
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ OK - UI-local state in composable
|
||||
@Composable
|
||||
fun LoginForm(onSubmit: (String, String) -> Unit) {
|
||||
var username by remember { mutableStateOf("") } // UI-local input state is fine
|
||||
var password by remember { mutableStateOf("") }
|
||||
// Hoist only as high as needed
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Usage
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Using theme
|
||||
Text(
|
||||
text = "Title",
|
||||
style = BitwardenTheme.typography.titleLarge,
|
||||
color = BitwardenTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// ❌ BAD - Hardcoded
|
||||
Text(
|
||||
text = "Title",
|
||||
style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold), // Should use theme
|
||||
color = Color(0xFF0000FF) // Should use theme color
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(17.dp)) // Non-standard spacing
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
```kotlin
|
||||
// ✅ GOOD - Interactive element with description
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_password),
|
||||
contentDescription = "Password visibility toggle",
|
||||
modifier = Modifier.clickable { onToggle() }
|
||||
)
|
||||
|
||||
// ✅ GOOD - Decorative icon with explicit null
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check),
|
||||
contentDescription = null, // Decorative icon next to descriptive text
|
||||
tint = BitwardenTheme.colorScheme.success
|
||||
)
|
||||
|
||||
// ❌ BAD - Interactive element missing description
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_delete),
|
||||
contentDescription = null, // Interactive elements need descriptions
|
||||
modifier = Modifier.clickable { onDelete() }
|
||||
)
|
||||
```
|
||||
|
||||
## Prioritizing Findings
|
||||
|
||||
Use `reference/priority-framework.md` to classify findings as Critical/Important/Suggested/Optional.
|
||||
|
||||
## Output Format
|
||||
|
||||
Follow the format guidance from `SKILL.md` Step 5 (concise summary with critical issues only, detailed inline comments with `<details>` tags).
|
||||
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE / REQUEST CHANGES
|
||||
|
||||
**Critical Issues** (if any):
|
||||
- [One-line summary of each critical blocking issue with file:line reference]
|
||||
|
||||
See inline comments for all issue details.
|
||||
```
|
||||
|
||||
## Example Review
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
Updates login screen layout for improved visual hierarchy and touch targets
|
||||
|
||||
## Critical Issues
|
||||
None
|
||||
|
||||
## Suggested Improvements
|
||||
|
||||
**app/auth/LoginScreen.kt:67** - Can we use BitwardenTextField?
|
||||
This custom text field looks very similar to `ui/components/BitwardenTextField.kt:89`.
|
||||
Would using the existing component maintain consistency?
|
||||
|
||||
**app/auth/LoginScreen.kt:123** - Add contentDescription
|
||||
```kotlin
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_visibility),
|
||||
contentDescription = "Show password", // Add for accessibility
|
||||
modifier = Modifier.clickable { onToggleVisibility() }
|
||||
)
|
||||
```
|
||||
|
||||
**app/auth/LoginScreen.kt:145** - Use design system spacing
|
||||
```kotlin
|
||||
// Current
|
||||
Spacer(modifier = Modifier.height(17.dp))
|
||||
|
||||
// Design system uses 4.dp increments (4, 8, 12, 16, 24, 32, etc.)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
```
|
||||
```
|
||||
446
.claude/skills/reviewing-changes/examples/review-outputs.md
Normal file
446
.claude/skills/reviewing-changes/examples/review-outputs.md
Normal file
@@ -0,0 +1,446 @@
|
||||
# Review Output Examples
|
||||
|
||||
Well-structured code reviews demonstrating appropriate depth, tone, and formatting for different change types.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
**Format Reference:**
|
||||
- [Quick Format Reference](#quick-format-reference)
|
||||
- [Inline Comment Format](#inline-comment-format-required)
|
||||
- [Summary Comment Format](#summary-comment-format)
|
||||
|
||||
**Examples:**
|
||||
- [Example 1: Clean PR (No Issues)](#example-1-clean-pr-no-issues)
|
||||
- [Example 2: Dependency Update with Breaking Changes](#example-2-dependency-update-with-breaking-changes)
|
||||
- [Example 3: Feature Addition with Critical Issues](#example-3-feature-addition-with-critical-issues)
|
||||
|
||||
**Anti-Patterns:**
|
||||
- [❌ Anti-Patterns to Avoid](#-anti-patterns-to-avoid)
|
||||
- [Problem: Verbose Summary with Multiple Sections](#problem-verbose-summary-with-multiple-sections)
|
||||
- [Problem: Praise-Only Inline Comments](#problem-praise-only-inline-comments)
|
||||
- [Problem: Missing `<details>` Tags](#problem-missing-details-tags)
|
||||
|
||||
**Summary:**
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## Quick Format Reference
|
||||
|
||||
### Inline Comment Format (REQUIRED)
|
||||
|
||||
**MUST use `<details>` tags.** Only severity + description visible; all other content collapsed.
|
||||
|
||||
```
|
||||
[emoji] **[SEVERITY]**: [One-line issue description]
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
[Code example or specific fix]
|
||||
|
||||
[Rationale explaining why]
|
||||
|
||||
Reference: [docs link if applicable]
|
||||
</details>
|
||||
```
|
||||
|
||||
**Severity Levels:**
|
||||
- ❌ **CRITICAL** - Blocking, must fix (security, crashes, architecture violations)
|
||||
- ⚠️ **IMPORTANT** - Should fix (missing tests, quality issues)
|
||||
- ♻️ **DEBT** - Technical debt (duplication, convention violations, future rework needed)
|
||||
- 🎨 **SUGGESTED** - Nice to have (refactoring, improvements)
|
||||
- 💭 **QUESTION** - Seeking clarification (requirements, design decisions)
|
||||
|
||||
### Summary Comment Format
|
||||
|
||||
**Required format for ALL PRs:**
|
||||
```
|
||||
**Overall Assessment:** APPROVE / REQUEST CHANGES
|
||||
|
||||
**Critical Issues** (if any):
|
||||
- [issue with file:line]
|
||||
|
||||
See inline comments for details.
|
||||
```
|
||||
|
||||
All PRs use the same minimal format - no exceptions for size or complexity. Summary must be 5-10 lines maximum.
|
||||
|
||||
---
|
||||
|
||||
## Example 1: Clean PR (No Issues)
|
||||
|
||||
**Context**: Moving shared code to common module, complete migration, all patterns followed
|
||||
|
||||
**Review Comment:**
|
||||
```markdown
|
||||
**Overall Assessment:** APPROVE
|
||||
|
||||
Clean refactoring that moves ExitManager to :ui module, eliminating duplication between apps.
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- Immediate approval visible (2-3 lines)
|
||||
- One sentence acknowledging the work
|
||||
- No unnecessary sections or elaborate praise
|
||||
- Author gets quick feedback and can proceed
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Dependency Update with Breaking Changes
|
||||
|
||||
**Context**: Major version update requiring code migration
|
||||
|
||||
**Summary Comment:**
|
||||
```markdown
|
||||
**Overall Assessment:** REQUEST CHANGES
|
||||
|
||||
**Critical Issues:**
|
||||
- API migration required for Retrofit 3.0 breaking changes (network/api/BitwardenApiService.kt:34)
|
||||
|
||||
See inline comments for migration details.
|
||||
```
|
||||
|
||||
**Inline Comment 1** (on `network/api/BitwardenApiService.kt:34`):
|
||||
```markdown
|
||||
❌ **CRITICAL**: API migration required for Retrofit 3.0
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
Retrofit 3.0 removes the `Call<T>` return type. All 12 API methods in this file need migration:
|
||||
|
||||
```kotlin
|
||||
// Current (deprecated in Retrofit 3.0)
|
||||
@GET("api/accounts/profile")
|
||||
fun getProfile(): Call<ProfileResponse>
|
||||
|
||||
// Must migrate to
|
||||
@GET("api/accounts/profile")
|
||||
suspend fun getProfile(): Response<ProfileResponse>
|
||||
```
|
||||
|
||||
Breaking API change affects:
|
||||
- 12 methods in BitwardenApiService
|
||||
- 8 methods in VaultApiService
|
||||
- All call sites using enqueue/execute
|
||||
- Test utilities
|
||||
|
||||
Consider creating separate PR for this migration given the scope.
|
||||
|
||||
Reference: [Retrofit 3.0 migration guide](https://square.github.io/retrofit/changelogs/changelog-3.x/)
|
||||
</details>
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Minimal summary (2-3 lines)
|
||||
- Full details in collapsed inline comment
|
||||
- Specific file:line references
|
||||
- Code examples in <details>
|
||||
- Migration guidance and scope assessment
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Feature Addition with Critical Issues
|
||||
|
||||
**Context**: Implements PIN unlock for vault access
|
||||
|
||||
**Summary Comment:**
|
||||
```markdown
|
||||
**Overall Assessment:** REQUEST CHANGES
|
||||
|
||||
**Critical Issues:**
|
||||
- Exposes mutable state violating MVVM (UnlockViewModel.kt:78)
|
||||
- PIN stored without encryption - SECURITY ISSUE (UnlockRepository.kt:145)
|
||||
|
||||
See inline comments for all issues and suggestions.
|
||||
```
|
||||
|
||||
**Inline Comment 1** (on `app/vault/unlock/UnlockViewModel.kt:78`):
|
||||
```markdown
|
||||
❌ **CRITICAL**: Exposes mutable state
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
Change `MutableStateFlow<State>` to `StateFlow<State>`:
|
||||
|
||||
```kotlin
|
||||
// Current (problematic)
|
||||
val unlockState: MutableStateFlow<UnlockState>
|
||||
|
||||
// Should be
|
||||
private val _unlockState = MutableStateFlow<UnlockState>()
|
||||
val unlockState: StateFlow<UnlockState> = _unlockState.asStateFlow()
|
||||
```
|
||||
|
||||
Exposing MutableStateFlow allows external mutation, violating MVVM unidirectional data flow.
|
||||
|
||||
Reference: docs/ARCHITECTURE.md#mvvm-pattern
|
||||
</details>
|
||||
```
|
||||
|
||||
**Inline Comment 2** (on `data/vault/UnlockRepository.kt:145`):
|
||||
```markdown
|
||||
❌ **CRITICAL**: PIN stored without encryption - SECURITY ISSUE
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
Storing PIN in plaintext SharedPreferences exposes it to backup systems and rooted devices.
|
||||
|
||||
```kotlin
|
||||
// Current (CRITICAL SECURITY ISSUE)
|
||||
sharedPreferences.edit {
|
||||
putString(KEY_PIN, pin)
|
||||
}
|
||||
|
||||
// Must use Android Keystore encryption
|
||||
suspend fun storePin(pin: String): Result<Unit> = runCatching {
|
||||
val encrypted = keystoreManager.encrypt(pin.toByteArray())
|
||||
encryptedPrefs.putBytes(KEY_PIN, encrypted)
|
||||
}
|
||||
```
|
||||
|
||||
Use Android Keystore encryption or EncryptedSharedPreferences per security architecture.
|
||||
|
||||
Reference: docs/ARCHITECTURE.md#security
|
||||
</details>
|
||||
```
|
||||
|
||||
**Inline Comment 3** (on `app/vault/unlock/UnlockViewModel.kt:92`):
|
||||
```markdown
|
||||
⚠️ **IMPORTANT**: Missing error handling test
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
Add test to prevent regression if error handling changes:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `when incorrect PIN entered then returns error state`() = runTest {
|
||||
val viewModel = UnlockViewModel(mockRepository)
|
||||
coEvery { mockRepository.validatePin("1234") }
|
||||
returns Result.failure(InvalidPinException())
|
||||
|
||||
viewModel.onPinEntered("1234")
|
||||
|
||||
assertEquals(UnlockState.Error("Invalid PIN"), viewModel.state.value)
|
||||
}
|
||||
```
|
||||
|
||||
Ensures error flow remains robust across refactorings.
|
||||
</details>
|
||||
```
|
||||
|
||||
**Inline Comment 4** (on `app/vault/unlock/UnlockViewModel.kt:105`):
|
||||
```markdown
|
||||
🎨 **SUGGESTED**: Consider rate limiting for PIN attempts
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
Currently allows unlimited attempts, which could enable brute force attacks.
|
||||
|
||||
```kotlin
|
||||
private var attemptCount = 0
|
||||
private var lockoutUntil: Instant? = null
|
||||
|
||||
fun onPinEntered(pin: String) {
|
||||
if (isLockedOut()) {
|
||||
_state.value = UnlockState.LockedOut(lockoutUntil!!)
|
||||
return
|
||||
}
|
||||
// ... validate PIN ...
|
||||
if (invalid) {
|
||||
attemptCount++
|
||||
if (attemptCount >= MAX_ATTEMPTS) {
|
||||
lockoutUntil = clock.millis() + 15.minutes
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Would add security layer against brute force. Consider discussing threat model with security team.
|
||||
</details>
|
||||
```
|
||||
|
||||
**Inline Comment 5** (on `app/vault/unlock/UnlockScreen.kt:134`):
|
||||
```markdown
|
||||
💭 **QUESTION**: Can we use BitwardenTextField?
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
This custom PIN input field looks similar to `ui/components/BitwardenTextField.kt:67`.
|
||||
|
||||
Would using the existing component maintain consistency and reduce custom UI code?
|
||||
</details>
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Minimal summary (3-4 lines) with critical issues only
|
||||
- Each issue gets separate inline comment with `<details>` tag
|
||||
- Multiple severity levels demonstrated (CRITICAL, IMPORTANT, SUGGESTED, QUESTION)
|
||||
- Mix of prescriptive fixes and collaborative questions
|
||||
- Code examples collapsed in <details>
|
||||
- No "Good Practices" or "Action Items" sections
|
||||
|
||||
---
|
||||
|
||||
## ❌ Anti-Patterns to Avoid
|
||||
|
||||
### Problem: Verbose Summary with Multiple Sections
|
||||
|
||||
**What NOT to do:**
|
||||
```markdown
|
||||
### Review Complete ✅
|
||||
|
||||
## Summary
|
||||
[Lengthy description of what the PR does]
|
||||
|
||||
### Strengths 👍
|
||||
1. **Excellent documentation** - KDoc comments are comprehensive
|
||||
2. **Proper fail-closed design** - Security defaults to rejection
|
||||
3. **Defense in depth** - Multiple validation layers
|
||||
[7 total items with elaboration]
|
||||
|
||||
### Critical Issues ⚠️
|
||||
- Missing test coverage for security-critical code (with full details)
|
||||
- [More issues with full explanations]
|
||||
|
||||
### Recommendations 🎨
|
||||
- [Multiple recommendations]
|
||||
|
||||
### Test Coverage Status 📊
|
||||
- [Analysis]
|
||||
|
||||
### Architecture Compliance ✅
|
||||
- [Analysis]
|
||||
|
||||
## Recommendation
|
||||
**Conditional approval** with follow-up...
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- 800+ tokens for a summary comment
|
||||
- Multiple sections (Strengths, Recommendations, Test Coverage, Architecture)
|
||||
- Elaborates on positive aspects ("Excellent documentation...")
|
||||
- Duplicates critical issues (summary has details + inline comments have same details)
|
||||
- Creates visual clutter in PR conversation
|
||||
|
||||
**Correct approach:**
|
||||
```markdown
|
||||
**Overall Assessment:** REQUEST CHANGES
|
||||
|
||||
**Critical Issues:**
|
||||
- Missing test coverage for security-critical code (PasswordManagerSignatureVerifierImpl.kt:47)
|
||||
|
||||
See inline comments for details.
|
||||
```
|
||||
|
||||
**Key differences:**
|
||||
- 3-5 lines vs 800+ tokens
|
||||
- Verdict + critical issues only
|
||||
- All details belong in inline comments
|
||||
- No positive commentary sections
|
||||
- Scales with PR complexity, not analysis thoroughness
|
||||
|
||||
### Problem: Praise-Only Inline Comments
|
||||
|
||||
**What NOT to do:**
|
||||
|
||||
Creating inline comment on `AuthenticatorBridgeManagerImpl.kt:73`:
|
||||
```markdown
|
||||
👍 **Excellent integration of signature verification**
|
||||
|
||||
The signature verification is properly integrated into the connection flow:
|
||||
- Checked during initialization (line 73)
|
||||
- Checked before binding (line 134)
|
||||
- Ensures only validated apps can connect
|
||||
|
||||
This is exactly the right approach for fail-safe security.
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- Entire comment is positive feedback with no actionable issue
|
||||
- Takes up space in PR conversation
|
||||
- Distracts from actual issues
|
||||
- Violates "focus on actionable feedback" principle
|
||||
|
||||
**Correct approach:**
|
||||
- Do not create this comment at all
|
||||
- Reserve inline comments exclusively for issues requiring attention
|
||||
|
||||
### Problem: Missing `<details>` Tags
|
||||
|
||||
**What NOT to do:**
|
||||
|
||||
```markdown
|
||||
❌ **CRITICAL**: Missing test coverage for security-critical code
|
||||
|
||||
The `@OmitFromCoverage` annotation excludes this entire class from test coverage.
|
||||
|
||||
**Problems:**
|
||||
1. No validation that certificate hashes match actual Bitwarden certificates
|
||||
2. No verification of fail-closed behavior on edge cases
|
||||
3. No tests for multiple signer rejection logic
|
||||
4. Certificate hash typos would go undetected until production
|
||||
|
||||
**Recommendation:**
|
||||
Replace `@OmitFromCoverage` with proper unit tests.
|
||||
|
||||
Example test structure:
|
||||
[long code block]
|
||||
|
||||
Security-critical code should have the highest test coverage, not be omitted.
|
||||
```
|
||||
|
||||
**Why this is wrong:**
|
||||
- All content visible immediately (code examples, problems list, rationale)
|
||||
- Creates visual clutter in PR conversation
|
||||
- Makes it hard to scan multiple issues quickly
|
||||
|
||||
**Correct approach:**
|
||||
```markdown
|
||||
❌ **CRITICAL**: Missing test coverage for security-critical code
|
||||
|
||||
<details>
|
||||
<summary>Details and fix</summary>
|
||||
|
||||
The `@OmitFromCoverage` annotation excludes this entire class from test coverage.
|
||||
|
||||
**Problems:**
|
||||
1. No validation that certificate hashes match actual Bitwarden certificates
|
||||
2. No verification of fail-closed behavior on edge cases
|
||||
3. No tests for multiple signer rejection logic
|
||||
4. Certificate hash typos would go undetected until production
|
||||
|
||||
**Recommendation:**
|
||||
Replace `@OmitFromCoverage` with proper unit tests.
|
||||
|
||||
Example test structure:
|
||||
[code block]
|
||||
|
||||
Security-critical code should have the highest test coverage, not be omitted.
|
||||
</details>
|
||||
```
|
||||
|
||||
**Key difference:** Only severity + one-line description visible. All details collapsed.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Always use:**
|
||||
- Minimal summary (verdict + critical issues)
|
||||
- Separate inline comments with `<details>` tags
|
||||
- Hybrid emoji + text severity prefixes
|
||||
- Focus exclusively on actionable feedback
|
||||
|
||||
**Never use:**
|
||||
- Multiple summary sections (Strengths, Recommendations, etc.)
|
||||
- Praise-only inline comments
|
||||
- Duplication between summary and inline comments
|
||||
- Verbose analysis in summary (belongs in inline comments)
|
||||
@@ -0,0 +1,312 @@
|
||||
# Architectural Patterns Quick Reference
|
||||
|
||||
Quick reference for Bitwarden Android architectural patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
**Core Patterns:**
|
||||
- [MVVM + UDF Pattern](#mvvm--udf-pattern)
|
||||
- [ViewModel Structure](#viewmodel-structure)
|
||||
- [UI Layer (Compose)](#ui-layer-compose)
|
||||
- [Hilt Dependency Injection](#hilt-dependency-injection)
|
||||
- [ViewModels](#viewmodels)
|
||||
- [Repositories and Managers](#repositories-and-managers)
|
||||
- [Module Organization](#module-organization)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Use Result Types, Not Exceptions](#use-result-types-not-exceptions)
|
||||
- [Quick Checklist](#quick-checklist)
|
||||
|
||||
---
|
||||
|
||||
## MVVM + UDF Pattern
|
||||
|
||||
### ViewModel Structure
|
||||
|
||||
**✅ GOOD - Proper state encapsulation**:
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class FeatureViewModel @Inject constructor(
|
||||
private val repository: FeatureRepository
|
||||
) : ViewModel() {
|
||||
// Private mutable state
|
||||
private val _state = MutableStateFlow<FeatureState>(FeatureState.Initial)
|
||||
|
||||
// Public immutable state
|
||||
val state: StateFlow<FeatureState> = _state.asStateFlow()
|
||||
|
||||
// Actions as functions, state updated via internal action
|
||||
fun onActionClicked() {
|
||||
viewModelScope.launch {
|
||||
val result = repository.performAction()
|
||||
sendAction(FeatureAction.Internal.ActionComplete(result))
|
||||
}
|
||||
}
|
||||
|
||||
// The ViewModel has a handler that processes the internal action
|
||||
private fun handleInternalAction(action: FeatureAction.Internal) {
|
||||
when (action) {
|
||||
is FeatureAction.Internal.ActionComplete -> {
|
||||
// The action handler evaluates the result and updates state
|
||||
action.result.fold(
|
||||
onSuccess = { _state.value = State.Success },
|
||||
onFailure = { _state.value = State.Error(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**❌ BAD - Common violations**:
|
||||
```kotlin
|
||||
class FeatureViewModel : ViewModel() {
|
||||
// ❌ Exposes mutable state
|
||||
val state: MutableStateFlow<FeatureState>
|
||||
|
||||
// ❌ Business logic in ViewModel
|
||||
fun onSubmit() {
|
||||
val encrypted = encryptionManager.encrypt(data) // Should be in Repository
|
||||
_state.value = FeatureState.Success
|
||||
}
|
||||
|
||||
// ❌ Direct Android framework dependency
|
||||
fun onCreate(context: Context) { // ViewModels shouldn't depend on Context
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Expose `StateFlow<T>`, never `MutableStateFlow<T>`
|
||||
- Delegate business logic to Repository/Manager
|
||||
- No direct Android framework dependencies (except ViewModel, SavedStateHandle)
|
||||
- Use `viewModelScope` for coroutines
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#mvvm-pattern`
|
||||
|
||||
---
|
||||
|
||||
### UI Layer (Compose)
|
||||
|
||||
**✅ GOOD - Stateless, observes only**:
|
||||
```kotlin
|
||||
@Composable
|
||||
fun FeatureScreen(
|
||||
state: FeatureState,
|
||||
onActionClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
when (state) {
|
||||
is FeatureState.Loading -> LoadingIndicator()
|
||||
is FeatureState.Success -> SuccessContent(state.data)
|
||||
is FeatureState.Error -> ErrorMessage(state.error)
|
||||
}
|
||||
|
||||
BitwardenButton(
|
||||
text = "Action",
|
||||
onClick = onActionClick // Sends event to ViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**❌ BAD - Stateful, modifies state**:
|
||||
```kotlin
|
||||
@Composable
|
||||
fun FeatureScreen(viewModel: FeatureViewModel) {
|
||||
var localState by remember { mutableStateOf(...) } // ❌ State in UI
|
||||
|
||||
Button(onClick = {
|
||||
viewModel._state.value = FeatureState.Loading // ❌ Directly modifying ViewModel state
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Compose screens observe state, never modify
|
||||
- User actions passed as events/callbacks to ViewModel
|
||||
- No business logic in UI layer
|
||||
- Use existing components from `:ui` module
|
||||
|
||||
---
|
||||
|
||||
## Hilt Dependency Injection
|
||||
|
||||
### ViewModels
|
||||
|
||||
**✅ GOOD - Interface injection**:
|
||||
```kotlin
|
||||
@HiltViewModel
|
||||
class FeatureViewModel @Inject constructor(
|
||||
private val repository: FeatureRepository, // Interface, not implementation
|
||||
private val authManager: AuthManager,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : ViewModel()
|
||||
```
|
||||
|
||||
**❌ BAD - Common violations**:
|
||||
```kotlin
|
||||
// ❌ No @HiltViewModel annotation
|
||||
class FeatureViewModel @Inject constructor(...)
|
||||
|
||||
// ❌ Injecting implementation instead of interface
|
||||
class FeatureViewModel @Inject constructor(
|
||||
private val repository: FeatureRepositoryImpl // Should inject interface
|
||||
)
|
||||
|
||||
// ❌ Manual instantiation
|
||||
class FeatureViewModel : ViewModel() {
|
||||
private val repository = FeatureRepositoryImpl() // Should use @Inject
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Annotate with `@HiltViewModel`
|
||||
- Use `@Inject constructor`
|
||||
- Inject interfaces, not implementations
|
||||
- Use `SavedStateHandle` for process death survival
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#dependency-injection`
|
||||
|
||||
---
|
||||
|
||||
### Repositories and Managers
|
||||
|
||||
**✅ GOOD - Implementation with @Inject**:
|
||||
```kotlin
|
||||
interface FeatureRepository {
|
||||
suspend fun fetchData(): Result<Data>
|
||||
}
|
||||
|
||||
class FeatureRepositoryImpl @Inject constructor(
|
||||
private val apiService: FeatureApiService,
|
||||
private val database: FeatureDao
|
||||
) : FeatureRepository {
|
||||
override suspend fun fetchData(): Result<Data> = runCatching {
|
||||
apiService.getData()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Module provides interface**:
|
||||
```kotlin
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class DataModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindFeatureRepository(
|
||||
impl: FeatureRepositoryImpl
|
||||
): FeatureRepository
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Define interface for abstraction
|
||||
- Implementation uses `@Inject constructor`
|
||||
- Module binds implementation to interface
|
||||
- Appropriate scoping (`@Singleton`, `@ViewModelScoped`)
|
||||
|
||||
---
|
||||
|
||||
## Module Organization
|
||||
|
||||
```
|
||||
android/
|
||||
├── core/ # Shared utilities (cryptography, analytics, logging)
|
||||
├── data/ # Repositories, database, domain models
|
||||
├── network/ # API clients, network utilities
|
||||
├── ui/ # Reusable Compose components, theme
|
||||
├── app/ # Application, feature screens, ViewModels
|
||||
└── authenticator/ # Authenticator app (separate from password manager)
|
||||
```
|
||||
|
||||
**Correct Placement**:
|
||||
- UI screens and ViewModels → `:app`
|
||||
- Reusable Compose components → `:ui`
|
||||
- Data models and Repositories → `:data`
|
||||
- API services → `:network`
|
||||
- Cryptography, logging → `:core`
|
||||
|
||||
**Check for**:
|
||||
- No circular dependencies
|
||||
- Correct module placement
|
||||
- Proper visibility (internal vs public)
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#module-structure`
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Use Result Types, Not Exceptions
|
||||
|
||||
**✅ GOOD - Result-based**:
|
||||
```kotlin
|
||||
// Repository
|
||||
suspend fun fetchData(): Result<Data> = runCatching {
|
||||
apiService.getData()
|
||||
}
|
||||
|
||||
// ViewModel
|
||||
fun onFetch() {
|
||||
viewModelScope.launch {
|
||||
val result = repository.fetchData()
|
||||
sendAction(FeatureAction.Internal.FetchComplete(result))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**❌ BAD - Exception-based in business logic**:
|
||||
```kotlin
|
||||
// ❌ Don't throw in business logic
|
||||
suspend fun fetchData(): Data {
|
||||
try {
|
||||
return apiService.getData()
|
||||
} catch (e: Exception) {
|
||||
throw FeatureException(e) // Don't throw in repositories
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Try-catch in ViewModel
|
||||
fun onFetch() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val data = repository.fetchData()
|
||||
sendAction(FeatureAction.Internal.FetchComplete(data))
|
||||
} catch (e: Exception) {
|
||||
sendAction(FeatureAction.Internal.FetchComplete(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Use `Result<T>` return types in repositories
|
||||
- Use `runCatching { }` to wrap API calls
|
||||
- Handle results with `.fold()` in ViewModels
|
||||
- Don't throw exceptions in business logic
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#error-handling`
|
||||
|
||||
---
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
### Architecture
|
||||
- [ ] ViewModels expose StateFlow, not MutableStateFlow?
|
||||
- [ ] Business logic in Repository, not ViewModel?
|
||||
- [ ] Using Hilt DI (@HiltViewModel, @Inject constructor)?
|
||||
- [ ] Injecting interfaces, not implementations?
|
||||
- [ ] Correct module placement?
|
||||
|
||||
### Error Handling
|
||||
- [ ] Using Result types, not exceptions in business logic?
|
||||
- [ ] Errors handled with .fold() in ViewModels?
|
||||
|
||||
---
|
||||
|
||||
For comprehensive details, always refer to:
|
||||
- `docs/ARCHITECTURE.md` - Full architecture patterns
|
||||
- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide
|
||||
431
.claude/skills/reviewing-changes/reference/priority-framework.md
Normal file
431
.claude/skills/reviewing-changes/reference/priority-framework.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# Finding Priority Framework
|
||||
|
||||
Use this framework to classify findings during code review. Clear prioritization helps authors triage and address issues effectively.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
**Severity Categories:**
|
||||
- [❌ CRITICAL (Blocker - Must Fix Before Merge)](#critical-blocker---must-fix-before-merge)
|
||||
- [⚠️ IMPORTANT (Should Fix)](#important-should-fix)
|
||||
- [♻️ DEBT (Technical Debt)](#debt-technical-debt)
|
||||
- [🎨 SUGGESTED (Nice to Have)](#suggested-nice-to-have)
|
||||
- [💭 QUESTION (Seeking Clarification)](#question-seeking-clarification)
|
||||
- [Optional (Acknowledge But Don't Require)](#optional-acknowledge-but-dont-require)
|
||||
|
||||
**Guidelines:**
|
||||
- [Classification Guidelines](#classification-guidelines)
|
||||
- [When Something is Between Categories](#when-something-is-between-categories)
|
||||
- [Context Matters](#context-matters)
|
||||
- [Examples by Change Type](#examples-by-change-type)
|
||||
- [Special Cases](#special-cases)
|
||||
- [Summary](#summary)
|
||||
|
||||
---
|
||||
|
||||
## ❌ **CRITICAL** (Blocker - Must Fix Before Merge)
|
||||
|
||||
These issues **must** be addressed before the PR can be merged. They pose immediate risks to security, stability, or architecture integrity.
|
||||
|
||||
### Security
|
||||
- Data leaks or plaintext sensitive data (passwords, keys, tokens)
|
||||
- Weak encryption or insecure key storage
|
||||
- Missing authentication or authorization checks
|
||||
- Input injection vulnerabilities (SQL, XSS, command injection)
|
||||
- Sensitive data in logs or error messages
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption
|
||||
PIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.
|
||||
Reference: docs/ARCHITECTURE.md#security
|
||||
```
|
||||
|
||||
### Stability
|
||||
- Compilation errors or warnings
|
||||
- Null pointer exceptions in production paths
|
||||
- Resource leaks (file handles, network connections, memory)
|
||||
- Crashes or unhandled exceptions in critical paths
|
||||
- Thread safety violations
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/auth/BiometricRepository.kt:120** - CRITICAL: Missing null safety check
|
||||
biometricPrompt result can be null. Add explicit null check to prevent crash.
|
||||
```
|
||||
|
||||
### Architecture
|
||||
- Mutable state exposure in ViewModels (violates MVVM)
|
||||
- Exception-based error handling in business logic (should use Result)
|
||||
- Circular dependencies between modules
|
||||
- Violation of zero-knowledge principles
|
||||
- Direct dependency instantiation (should use DI)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/login/LoginViewModel.kt:45** - CRITICAL: Exposes mutable state
|
||||
Change MutableStateFlow to StateFlow in public API to prevent external state mutation.
|
||||
This violates MVVM encapsulation pattern.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ **IMPORTANT** (Should Fix)
|
||||
|
||||
These issues should be addressed but don't block merge if there's a compelling reason. They improve code quality, maintainability, or robustness.
|
||||
|
||||
### Testing
|
||||
- Missing tests for critical paths (authentication, encryption, data sync)
|
||||
- Missing tests for new public APIs
|
||||
- Tests that don't verify actual behavior (test implementation, not behavior)
|
||||
- Missing test coverage for error scenarios
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/auth/BiometricRepository.kt** - IMPORTANT: Missing test for cancellation
|
||||
Add test for user cancellation scenario to prevent regression.
|
||||
```
|
||||
|
||||
### Architecture
|
||||
- Inconsistent patterns within PR (mixing error handling approaches)
|
||||
- Poor separation of concerns
|
||||
- Tight coupling between components
|
||||
- Not following established project patterns
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/vault/VaultViewModel.kt:89** - IMPORTANT: Business logic in ViewModel
|
||||
Encryption logic should be in Repository, not ViewModel.
|
||||
Reference: docs/ARCHITECTURE.md#mvvm-pattern
|
||||
```
|
||||
|
||||
### Documentation
|
||||
- Undocumented public APIs (missing KDoc)
|
||||
- Missing documentation for complex algorithms
|
||||
- Unclear naming or confusing interfaces
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**core/crypto/EncryptionManager.kt:34** - IMPORTANT: Missing KDoc
|
||||
Public encryption method should document parameters, return value, and exceptions.
|
||||
```
|
||||
|
||||
### Performance
|
||||
- Inefficient algorithms in hot paths (with evidence from profiling)
|
||||
- Blocking main thread with I/O operations
|
||||
- Memory inefficient data structures (with evidence)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/vault/VaultListViewModel.kt:78** - IMPORTANT: N+1 query pattern
|
||||
Fetching items one-by-one in loop. Consider batch fetch to reduce database queries.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ♻️ **DEBT** (Technical Debt)
|
||||
|
||||
Code that duplicates existing patterns, violates established conventions, or will require rework within 6 months. Introduces technical debt that should be tracked for future cleanup.
|
||||
|
||||
### Duplication
|
||||
- Copy-pasted code blocks across files
|
||||
- Repeated validation or business logic
|
||||
- Multiple implementations of same pattern
|
||||
- Data transformation duplicated in multiple places
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/vault/VaultListViewModel.kt:156** - DEBT: Duplicates encryption logic
|
||||
Same encryption pattern exists in VaultRepository.kt:234 and SyncManager.kt:89.
|
||||
Extract to shared EncryptionUtil to reduce maintenance burden.
|
||||
```
|
||||
|
||||
### Convention Violations
|
||||
- Inconsistent error handling approaches within same module
|
||||
- Mixing architectural patterns (MVVM + MVC)
|
||||
- Not following established DI patterns
|
||||
- Deviating from project code style significantly
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/auth/AuthRepository.kt:78** - DEBT: Exception-based error handling
|
||||
Project standard is Result<T> for error handling. This uses try-catch with throws.
|
||||
Creates inconsistency and makes testing harder.
|
||||
Reference: docs/ARCHITECTURE.md#error-handling
|
||||
```
|
||||
|
||||
### Future Rework Required
|
||||
- Hardcoded values that should be configurable
|
||||
- Temporary workarounds without TODO/FIXME
|
||||
- Code that will need changes when planned features arrive
|
||||
- Tight coupling that prevents future extensibility
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/settings/SettingsViewModel.kt:45** - DEBT: Hardcoded feature flags
|
||||
Feature flags should come from remote config for A/B testing.
|
||||
Will require rework when experimentation framework launches.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 **SUGGESTED** (Nice to Have)
|
||||
|
||||
These are improvement opportunities but not required. Consider the effort vs. benefit before requesting changes.
|
||||
|
||||
### Code Quality
|
||||
- Minor style inconsistencies (if not caught by linter)
|
||||
- Opportunities for DRY improvements
|
||||
- Better variable naming for clarity
|
||||
- Simplification opportunities
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/vault/VaultScreen.kt:145** - SUGGESTED: Consider extracting helper function
|
||||
This 20-line block appears in 3 places. Consider extracting to reduce duplication.
|
||||
```
|
||||
|
||||
### Testing
|
||||
- Additional test coverage for edge cases (beyond critical paths)
|
||||
- More comprehensive integration tests
|
||||
- Performance tests for non-critical paths
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/login/LoginViewModelTest.kt** - SUGGESTED: Add test for concurrent login attempts
|
||||
Not critical, but would increase confidence in edge case handling.
|
||||
```
|
||||
|
||||
### Refactoring
|
||||
- Extracting reusable patterns
|
||||
- Modernizing old patterns (if touching related code)
|
||||
- Improving testability
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/vault/VaultRepository.kt:200** - SUGGESTED: Consider extracting validation logic
|
||||
Could be extracted to separate validator class for reusability and testing.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💭 **QUESTION** (Seeking Clarification)
|
||||
|
||||
Questions about requirements, unclear intent, or potential conflicts that require human knowledge to answer. Open inquiries that cannot be resolved through code inspection alone.
|
||||
|
||||
### Requirements Clarification
|
||||
- Ambiguous acceptance criteria
|
||||
- Multiple valid implementation approaches
|
||||
- Unclear business rules or edge case handling
|
||||
- Conflicting requirements between specs and implementation
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/vault/ItemListViewModel.kt:67** - QUESTION: Expected sort behavior for equal timestamps?
|
||||
When items have identical timestamps, should secondary sort be by:
|
||||
- Name (alphabetical)
|
||||
- Creation order
|
||||
- Item type priority
|
||||
|
||||
Spec doesn't specify tie-breaking logic.
|
||||
```
|
||||
|
||||
### Design Decisions
|
||||
- Architecture choices that could go multiple ways
|
||||
- Trade-offs between approaches without clear winner
|
||||
- Feature flag strategy or rollout approach
|
||||
- API design with multiple valid patterns
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/sync/SyncManager.kt:134** - QUESTION: Should sync failures retry automatically?
|
||||
Current implementation fails immediately. Options:
|
||||
- Exponential backoff (3 retries)
|
||||
- User-triggered retry only
|
||||
- Background retry on network restore
|
||||
|
||||
What's the expected UX?
|
||||
```
|
||||
|
||||
### System Integration
|
||||
- Unclear contracts with external systems
|
||||
- Potential conflicts with other features/modules
|
||||
- Assumptions about third-party API behavior
|
||||
- Cross-team coordination needs
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/auth/BiometricPrompt.kt:89** - QUESTION: Compatibility with pending device credentials PR?
|
||||
PR #1234 is refactoring device credentials. Should this:
|
||||
- Merge first and adapt later
|
||||
- Wait for #1234 to land
|
||||
- Coordinate with that author
|
||||
|
||||
Timing unclear.
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
- Uncertainty about test scope or approach
|
||||
- Questions about mocking external dependencies
|
||||
- Edge cases that need product input
|
||||
- Performance testing requirements
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/vault/EncryptionTest.kt:45** - QUESTION: Should we test against real Keystore?
|
||||
Currently using mocked Keystore. Real Keystore testing would:
|
||||
+ Catch hardware-specific issues
|
||||
- Slow down CI significantly
|
||||
- Require API 23+ emulators
|
||||
|
||||
What's the priority?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optional (Acknowledge But Don't Require)
|
||||
|
||||
Note good practices to reinforce positive patterns. Keep these **brief** - list only, no elaboration.
|
||||
|
||||
### Good Practices
|
||||
|
||||
**Format**: Simple bullet list, no explanation
|
||||
|
||||
```markdown
|
||||
## Good Practices
|
||||
- Proper Hilt DI usage throughout
|
||||
- Comprehensive unit test coverage
|
||||
- Clear separation of concerns
|
||||
- Well-documented public APIs
|
||||
```
|
||||
|
||||
**Don't do this** (too verbose):
|
||||
```markdown
|
||||
## Good Practices
|
||||
- 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! 👍
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Classification Guidelines
|
||||
|
||||
### When Something is Between Categories
|
||||
|
||||
**If unsure between Critical and Important**:
|
||||
- Ask: "Could this cause production incidents, data loss, or security breaches?"
|
||||
- If yes → Critical
|
||||
- If no → Important
|
||||
|
||||
**If unsure between Important and Debt**:
|
||||
- Ask: "Is this a bug/defect or just duplication/inconsistency?"
|
||||
- If bug/defect → Important
|
||||
- If duplication/inconsistency → Debt
|
||||
|
||||
**If unsure between Important and Suggested**:
|
||||
- Ask: "Would I block merge over this?"
|
||||
- If yes → Important
|
||||
- If no → Suggested
|
||||
|
||||
**If unsure between Debt and Suggested**:
|
||||
- Ask: "Will this require rework within 6 months?"
|
||||
- If yes → Debt
|
||||
- If no → Suggested
|
||||
|
||||
**If unsure between Suggested and Question**:
|
||||
- Ask: "Am I requesting a change or asking for clarification?"
|
||||
- If requesting change → Suggested
|
||||
- If seeking clarification → Question
|
||||
|
||||
**If unsure between Suggested and Optional**:
|
||||
- Ask: "Is this actionable feedback or just acknowledgment?"
|
||||
- If actionable → Suggested
|
||||
- If acknowledgment → Optional
|
||||
|
||||
### Context Matters
|
||||
|
||||
**Same issue, different contexts**:
|
||||
|
||||
```
|
||||
// Critical for production code
|
||||
Missing null safety check in auth flow → CRITICAL
|
||||
|
||||
// Suggested for internal test utility
|
||||
Missing null safety check in test helper → SUGGESTED
|
||||
```
|
||||
|
||||
**Same pattern, different risk levels**:
|
||||
|
||||
```
|
||||
// Critical for new feature
|
||||
Missing tests for new auth method → CRITICAL
|
||||
|
||||
// Important for bug fix
|
||||
Missing regression test → IMPORTANT
|
||||
|
||||
// Suggested for refactoring
|
||||
Missing tests for refactored helper → SUGGESTED
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples by Change Type
|
||||
|
||||
### Dependency Update
|
||||
- **Critical**: Known CVEs in old version not addressed
|
||||
- **Important**: Breaking changes that need migration
|
||||
- **Suggested**: Beta/alpha version stability concerns
|
||||
|
||||
### Bug Fix
|
||||
- **Critical**: Fix doesn't address root cause
|
||||
- **Important**: Missing regression test
|
||||
- **Suggested**: Similar bugs in related code
|
||||
|
||||
### Feature Addition
|
||||
- **Critical**: Security vulnerabilities, architecture violations
|
||||
- **Important**: Missing tests for critical paths
|
||||
- **Suggested**: Additional test coverage, minor refactoring
|
||||
|
||||
### UI Refinement
|
||||
- **Critical**: Missing accessibility for key actions
|
||||
- **Important**: Not using theme (hardcoded colors)
|
||||
- **Suggested**: Minor spacing/alignment improvements
|
||||
|
||||
### Refactoring
|
||||
- **Critical**: Changes behavior (should be behavior-preserving)
|
||||
- **Important**: Incomplete migration (mix of old/new patterns)
|
||||
- **Suggested**: Additional instances that could be refactored
|
||||
|
||||
### Infrastructure
|
||||
- **Critical**: Hardcoded secrets, no rollback plan
|
||||
- **Important**: Performance regression in build times
|
||||
- **Suggested**: Further optimization opportunities
|
||||
|
||||
---
|
||||
|
||||
## Special Cases
|
||||
|
||||
### Technical Debt
|
||||
- Acknowledge existing tech debt but don't require fixing in unrelated PR
|
||||
- Exception: If change makes tech debt worse, it's Important to address
|
||||
|
||||
### Scope Creep
|
||||
- Don't request changes outside PR scope
|
||||
- Can note as "Future consideration" but not required for this PR
|
||||
|
||||
### Linter-Catchable Issues
|
||||
- Don't flag issues that automated tools handle
|
||||
- Exception: If linter is misconfigured and missing real issues
|
||||
|
||||
### Personal Preferences
|
||||
- Don't flag unless grounded in project standards or architectural principles
|
||||
- Use "I-statements" if suggesting alternative approaches
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Critical**: Block merge, must fix (security, stability, architecture)
|
||||
**Important**: Should fix before merge (testing, quality, performance)
|
||||
**Debt**: Technical debt introduced, track for future cleanup
|
||||
**Suggested**: Nice to have, consider effort vs benefit
|
||||
**Question**: Seeking clarification on requirements or design
|
||||
**Optional**: Acknowledge good practices, keep brief
|
||||
175
.claude/skills/reviewing-changes/reference/review-psychology.md
Normal file
175
.claude/skills/reviewing-changes/reference/review-psychology.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Review Psychology: Constructive Feedback Phrasing
|
||||
|
||||
Effective code review feedback is clear, actionable, and constructive. This guide provides phrasing patterns for inline comments.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
**Guidelines:**
|
||||
- [Core Directives](#core-directives)
|
||||
- [Phrasing Templates](#phrasing-templates)
|
||||
- [Critical Issues (Prescriptive)](#critical-issues-prescriptive)
|
||||
- [Suggested Improvements (Exploratory)](#suggested-improvements-exploratory)
|
||||
- [Questions (Collaborative)](#questions-collaborative)
|
||||
- [Test Suggestions](#test-suggestions)
|
||||
- [When to Be Prescriptive vs Ask Questions](#when-to-be-prescriptive-vs-ask-questions)
|
||||
- [Special Cases](#special-cases)
|
||||
|
||||
---
|
||||
|
||||
## Core Directives
|
||||
|
||||
- **Keep positive feedback minimal**: For clean PRs with no issues, use 2-3 line approval only. When acknowledging good practices in PRs with issues, use single bullet list with no elaboration. Never create elaborate sections praising correct implementations.
|
||||
- Ask questions for design decisions, be prescriptive for clear violations
|
||||
- Focus on code, not people ("This code..." not "You...")
|
||||
- Use I-statements for subjective feedback ("Hard for me to understand...")
|
||||
- Explain rationale with every recommendation
|
||||
- Avoid: "just", "simply", "obviously", "easy"
|
||||
|
||||
---
|
||||
|
||||
## Phrasing Templates
|
||||
|
||||
### Critical Issues (Prescriptive)
|
||||
|
||||
**Pattern**: State problem + Provide solution + Explain why
|
||||
|
||||
```
|
||||
**[file:line]** - CRITICAL: [Issue description]
|
||||
|
||||
[Specific fix with code example if applicable]
|
||||
|
||||
[Rationale explaining why this is critical]
|
||||
|
||||
Reference: [docs link if applicable]
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/vault/VaultRepository.kt:145** - CRITICAL: PIN stored without encryption
|
||||
|
||||
PIN must be encrypted using Android Keystore, not stored in plaintext SharedPreferences.
|
||||
Plaintext storage exposes the PIN to backup systems and rooted devices.
|
||||
|
||||
Reference: docs/ARCHITECTURE.md#security
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Suggested Improvements (Exploratory)
|
||||
|
||||
**Pattern**: Observe + Suggest + Explain benefit
|
||||
|
||||
```
|
||||
**[file:line]** - Consider [alternative approach]
|
||||
|
||||
[Current observation]
|
||||
Can we [specific suggestion]?
|
||||
|
||||
[Benefit or rationale]
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**app/login/LoginScreen.kt:89** - Consider using existing BitwardenButton
|
||||
|
||||
This custom button implementation looks similar to `ui/components/BitwardenButton.kt:45`.
|
||||
Can we use the existing component to maintain consistency across the app?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Questions (Collaborative)
|
||||
|
||||
**Pattern**: Ask + Provide context (optional)
|
||||
|
||||
```
|
||||
**[file:line]** - [Question about intent or approach]?
|
||||
|
||||
[Optional context or observation]
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/sync/SyncManager.kt:234** - How does this handle concurrent sync attempts?
|
||||
|
||||
It looks like multiple coroutines could call `startSync()` simultaneously.
|
||||
Is there a mechanism to prevent race conditions, or is that handled elsewhere?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test Suggestions
|
||||
|
||||
**Pattern**: Observe gap + Suggest specific test + Provide skeleton
|
||||
|
||||
```
|
||||
**[file:line]** - Consider adding test for [scenario]
|
||||
|
||||
[Rationale]
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `test description`() = runTest {
|
||||
// Test skeleton
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```
|
||||
**data/auth/BiometricRepository.kt** - Consider adding test for cancellation scenario
|
||||
|
||||
This would prevent regression of the bug you just fixed:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `when biometric cancelled then returns cancelled state`() = runTest {
|
||||
coEvery { biometricPrompt.authenticate() } returns null
|
||||
|
||||
val result = repository.authenticate()
|
||||
|
||||
assertEquals(AuthResult.Cancelled, result)
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Be Prescriptive vs Ask Questions
|
||||
|
||||
**Be Prescriptive** (Tell them what to do):
|
||||
- Security issues
|
||||
- Architecture pattern violations
|
||||
- Null safety problems
|
||||
- Compilation errors
|
||||
- Documented project standards
|
||||
|
||||
**Ask Questions** (Seek explanation):
|
||||
- Design decisions with multiple valid approaches
|
||||
- Performance trade-offs without data
|
||||
- Unclear intent or reasoning
|
||||
- Scope decisions (this PR vs future work)
|
||||
- Patterns not documented in project guidelines
|
||||
|
||||
---
|
||||
|
||||
## Special Cases
|
||||
|
||||
**Nitpicks** - For truly minor suggestions, use "Nit:" prefix:
|
||||
```
|
||||
**Nit**: Extra blank line at line 145
|
||||
```
|
||||
|
||||
**Uncertainty** - If unsure, acknowledge it:
|
||||
```
|
||||
I'm not certain, but this might be called frequently.
|
||||
Has this been profiled?
|
||||
```
|
||||
|
||||
**Positive Feedback** - Brief list only, no elaboration:
|
||||
```
|
||||
## Good Practices
|
||||
- Proper Hilt DI usage throughout
|
||||
- Comprehensive unit test coverage
|
||||
- Clear separation of concerns
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Security Patterns Quick Reference
|
||||
|
||||
Quick reference for Bitwarden Android security patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md#security`.
|
||||
|
||||
## Encryption and Key Storage
|
||||
|
||||
**✅ GOOD - Android Keystore**:
|
||||
```kotlin
|
||||
// Sensitive data encrypted with Keystore
|
||||
class SecureStorage @Inject constructor(
|
||||
private val keystoreManager: KeystoreManager
|
||||
) {
|
||||
suspend fun storePin(pin: String): Result<Unit> = runCatching {
|
||||
val encrypted = keystoreManager.encrypt(pin.toByteArray())
|
||||
securePreferences.putBytes(KEY_PIN, encrypted)
|
||||
}
|
||||
}
|
||||
|
||||
// Or use EncryptedSharedPreferences
|
||||
val encryptedPrefs = EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"secure_prefs",
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
```
|
||||
|
||||
**❌ BAD - Plaintext or weak encryption**:
|
||||
```kotlin
|
||||
// ❌ CRITICAL - Plaintext storage
|
||||
sharedPreferences.edit {
|
||||
putString("pin", userPin) // Never store sensitive data in plaintext
|
||||
}
|
||||
|
||||
// ❌ CRITICAL - Weak encryption
|
||||
val cipher = Cipher.getInstance("DES") // Use AES-256-GCM
|
||||
|
||||
// ❌ CRITICAL - Hardcoded keys
|
||||
val key = "my_secret_key_123" // Use Android Keystore
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Use Android Keystore for encryption keys
|
||||
- Use EncryptedSharedPreferences for simple key-value storage
|
||||
- Use AES-256-GCM for encryption
|
||||
- Never store sensitive data in plaintext
|
||||
- Never hardcode encryption keys
|
||||
|
||||
Reference: `docs/ARCHITECTURE.md#security`
|
||||
|
||||
---
|
||||
|
||||
## Logging Sensitive Data
|
||||
|
||||
**✅ GOOD - No sensitive data**:
|
||||
```kotlin
|
||||
Log.d(TAG, "Authentication attempt for user")
|
||||
Log.d(TAG, "Vault sync completed with ${items.size} items")
|
||||
```
|
||||
|
||||
**❌ BAD - Logs sensitive data**:
|
||||
```kotlin
|
||||
// ❌ CRITICAL
|
||||
Log.d(TAG, "Password: $password")
|
||||
Log.d(TAG, "Auth token: $token")
|
||||
Log.d(TAG, "PIN: $pin")
|
||||
Log.d(TAG, "Encryption key: ${key.encoded}")
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Never log passwords, PINs, tokens, keys
|
||||
- Never log encryption keys or sensitive data
|
||||
- Be careful with error messages (don't include sensitive context)
|
||||
|
||||
---
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
### Security
|
||||
- [ ] Sensitive data encrypted with Keystore?
|
||||
- [ ] No plaintext passwords/keys?
|
||||
- [ ] No sensitive data in logs?
|
||||
- [ ] Using AES-256-GCM for encryption?
|
||||
- [ ] No hardcoded encryption keys?
|
||||
|
||||
---
|
||||
|
||||
For comprehensive security details, always refer to:
|
||||
- `docs/ARCHITECTURE.md#security` - Complete security architecture and zero-knowledge principles
|
||||
127
.claude/skills/reviewing-changes/reference/testing-patterns.md
Normal file
127
.claude/skills/reviewing-changes/reference/testing-patterns.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Testing Patterns Quick Reference
|
||||
|
||||
Quick reference for Bitwarden Android testing patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.
|
||||
|
||||
## ViewModel Tests
|
||||
|
||||
**✅ GOOD - Tests behavior**:
|
||||
```kotlin
|
||||
@Test
|
||||
fun `when login succeeds then state updates to success`() = runTest {
|
||||
// Arrange
|
||||
val viewModel = LoginViewModel(mockRepository)
|
||||
coEvery { mockRepository.login(any(), any()) } returns Result.success(User())
|
||||
|
||||
// Act
|
||||
viewModel.onLoginClicked("user@example.com", "password")
|
||||
|
||||
// Assert
|
||||
viewModel.state.test {
|
||||
assertEquals(LoginState.Loading, awaitItem())
|
||||
assertEquals(LoginState.Success, awaitItem())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**❌ BAD - Tests implementation**:
|
||||
```kotlin
|
||||
@Test
|
||||
fun `repository is called with correct parameters`() {
|
||||
// ❌ This tests implementation details, not behavior
|
||||
viewModel.onLoginClicked("user", "pass")
|
||||
coVerify { mockRepository.login("user", "pass") }
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Test behavior, not implementation
|
||||
- Use `runTest` for coroutine tests
|
||||
- Use Turbine for Flow testing
|
||||
- Use MockK for mocking
|
||||
|
||||
---
|
||||
|
||||
## Repository Tests
|
||||
|
||||
**✅ GOOD - Tests data transformations**:
|
||||
```kotlin
|
||||
@Test
|
||||
fun `fetchItems maps API response to domain model`() = runTest {
|
||||
// Arrange
|
||||
val apiResponse = listOf(ApiItem(id = "1", name = "Test"))
|
||||
coEvery { apiService.getItems() } returns apiResponse
|
||||
|
||||
// Act
|
||||
val result = repository.fetchItems()
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals(
|
||||
listOf(DomainItem(id = "1", name = "Test")),
|
||||
result.getOrThrow()
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Test data transformations
|
||||
- Test error handling (network failures, API errors)
|
||||
- Test caching behavior if applicable
|
||||
- Mock API services and databases
|
||||
|
||||
Reference: Project uses JUnit 5, MockK, Turbine, kotlinx-coroutines-test
|
||||
|
||||
---
|
||||
|
||||
## Null Safety
|
||||
|
||||
**✅ GOOD - Safe handling**:
|
||||
```kotlin
|
||||
// Safe call with elvis operator
|
||||
val result = apiService.getData() ?: return State.Error("No data")
|
||||
|
||||
// Let with safe call
|
||||
intent?.getStringExtra("key")?.let { value ->
|
||||
processValue(value)
|
||||
}
|
||||
|
||||
// Require with message
|
||||
val data = requireNotNull(response.data) { "Response data must not be null" }
|
||||
```
|
||||
|
||||
**❌ BAD - Unsafe assertions**:
|
||||
```kotlin
|
||||
// ❌ Unsafe - can crash
|
||||
val result = apiService.getData()!!
|
||||
|
||||
// ❌ Platform type unchecked
|
||||
val intent: Intent = getIntent() // Could be null from Java
|
||||
val value = intent.getStringExtra("key") // Potential NPE
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Avoid `!!` unless safety is guaranteed (rare)
|
||||
- Handle platform types with explicit nullability
|
||||
- Use safe calls (`?.`), elvis operator (`?:`), or explicit checks
|
||||
- Use `requireNotNull` with descriptive message if crash is acceptable
|
||||
|
||||
---
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
### Testing
|
||||
- [ ] ViewModels have unit tests?
|
||||
- [ ] Tests verify behavior, not implementation?
|
||||
- [ ] Edge cases covered?
|
||||
- [ ] Error scenarios tested?
|
||||
|
||||
### Code Quality
|
||||
- [ ] Null safety handled properly (no `!!` without guarantee)?
|
||||
- [ ] Public APIs have KDoc?
|
||||
- [ ] Following naming conventions?
|
||||
|
||||
---
|
||||
|
||||
For comprehensive details, always refer to:
|
||||
- `docs/ARCHITECTURE.md` - Full architecture patterns
|
||||
- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide
|
||||
85
.claude/skills/reviewing-changes/reference/ui-patterns.md
Normal file
85
.claude/skills/reviewing-changes/reference/ui-patterns.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Compose UI Patterns Quick Reference
|
||||
|
||||
Quick reference for Bitwarden Android Compose UI patterns during code reviews. For comprehensive details, read `docs/ARCHITECTURE.md` and `docs/STYLE_AND_BEST_PRACTICES.md`.
|
||||
|
||||
## Component Reuse
|
||||
|
||||
**✅ GOOD - Uses existing components**:
|
||||
```kotlin
|
||||
BitwardenButton(
|
||||
text = "Submit",
|
||||
onClick = onSubmit
|
||||
)
|
||||
|
||||
BitwardenTextField(
|
||||
value = text,
|
||||
onValueChange = onTextChange,
|
||||
label = "Email"
|
||||
)
|
||||
```
|
||||
|
||||
**❌ BAD - Duplicates existing components**:
|
||||
```kotlin
|
||||
// ❌ Recreating BitwardenButton
|
||||
Button(
|
||||
onClick = onSubmit,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = BitwardenTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text("Submit")
|
||||
}
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Check `:ui` module for existing components before creating custom ones
|
||||
- Use BitwardenButton, BitwardenTextField, etc. for consistency
|
||||
- Place new reusable components in `:ui` module
|
||||
|
||||
---
|
||||
|
||||
## Theme Usage
|
||||
|
||||
**✅ GOOD - Uses theme**:
|
||||
```kotlin
|
||||
Text(
|
||||
text = "Title",
|
||||
style = BitwardenTheme.typography.titleLarge,
|
||||
color = BitwardenTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp)) // Standard spacing
|
||||
```
|
||||
|
||||
**❌ BAD - Hardcoded values**:
|
||||
```kotlin
|
||||
Text(
|
||||
text = "Title",
|
||||
style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold), // Use theme
|
||||
color = Color(0xFF0066FF) // Use theme color
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(17.dp)) // Non-standard spacing
|
||||
```
|
||||
|
||||
**Key Rules**:
|
||||
- Use `BitwardenTheme.colorScheme` for colors
|
||||
- Use `BitwardenTheme.typography` for text styles
|
||||
- Use standard spacing (4.dp, 8.dp, 16.dp, 24.dp)
|
||||
|
||||
---
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
### UI Patterns
|
||||
- [ ] Using existing Bitwarden components from `:ui` module?
|
||||
- [ ] Using BitwardenTheme for colors and typography?
|
||||
- [ ] Using standard spacing values (4, 8, 16, 24 dp)?
|
||||
- [ ] No hardcoded colors or text styles?
|
||||
- [ ] UI is stateless (observes state, doesn't modify)?
|
||||
|
||||
---
|
||||
|
||||
For comprehensive details, always refer to:
|
||||
- `docs/ARCHITECTURE.md` - Full architecture patterns
|
||||
- `docs/STYLE_AND_BEST_PRACTICES.md` - Complete style guide
|
||||
16
.github/actions/log-inputs/action.yml
vendored
16
.github/actions/log-inputs/action.yml
vendored
@@ -11,10 +11,14 @@ runs:
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
shell: bash
|
||||
env:
|
||||
INPUTS: ${{ inputs.inputs }}
|
||||
run: |
|
||||
echo "<details><summary>Job Inputs</summary>" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ inputs.inputs }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "</details>" >> $GITHUB_STEP_SUMMARY
|
||||
{
|
||||
echo "<details><summary>Job Inputs</summary>"
|
||||
echo ""
|
||||
echo '```json'
|
||||
echo "$INPUTS"
|
||||
echo '```'
|
||||
echo "</details>"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -44,6 +44,5 @@ runs:
|
||||
- name: Install Fastlane
|
||||
shell: bash
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
50
.github/label-pr.json
vendored
Normal file
50
.github/label-pr.json
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"catch_all_label": "t:misc",
|
||||
"title_patterns": {
|
||||
"t:new-feature": ["feat", "feature"],
|
||||
"t:enhancement": ["enhancement", "enh", "impr"],
|
||||
"t:bug": ["fix", "bug", "bugfix"],
|
||||
"t:tech-debt": ["refactor", "chore", "cleanup", "revert", "debt", "test", "perf"],
|
||||
"t:docs": ["docs"],
|
||||
"t:ci": ["ci", "build", "chore(ci)"],
|
||||
"t:deps": ["deps"],
|
||||
"t:breaking-change": ["breaking", "breaking-change"],
|
||||
"t:misc": ["misc"]
|
||||
},
|
||||
"path_patterns": {
|
||||
"app:shared": [
|
||||
"annotation/",
|
||||
"core/",
|
||||
"data/",
|
||||
"network/",
|
||||
"ui/",
|
||||
"authenticatorbridge/",
|
||||
"gradle/"
|
||||
],
|
||||
"app:password-manager": [
|
||||
"app/",
|
||||
"cxf/",
|
||||
"testharness/"
|
||||
],
|
||||
"app:authenticator": [
|
||||
"authenticator/"
|
||||
],
|
||||
"t:ci": [
|
||||
".github/",
|
||||
"scripts/",
|
||||
"fastlane/",
|
||||
".gradle/",
|
||||
".claude/",
|
||||
"detekt-config.yml"
|
||||
],
|
||||
"t:docs": [
|
||||
"docs/"
|
||||
],
|
||||
"t:deps": [
|
||||
"gradle/"
|
||||
],
|
||||
"t:misc": [
|
||||
"keystore/"
|
||||
]
|
||||
}
|
||||
}
|
||||
3
.github/renovate.json
vendored
3
.github/renovate.json
vendored
@@ -29,7 +29,8 @@
|
||||
"gradle"
|
||||
],
|
||||
"excludePackageNames": [
|
||||
"com.github.bumptech.glide:compose"
|
||||
"com.github.bumptech.glide:compose",
|
||||
"com.bitwarden:sdk-android"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ Single line of release notes text
|
||||
|
||||
```json
|
||||
...
|
||||
"customfield_10335": {
|
||||
"customfield_9999": {
|
||||
"type": "doc",
|
||||
"version": 1,
|
||||
"content": [
|
||||
@@ -62,7 +62,7 @@ Single line of release notes text
|
||||
|
||||
```json
|
||||
...
|
||||
"customfield_10335": {
|
||||
"customfield_9999": {
|
||||
"type": "doc",
|
||||
"version": 1,
|
||||
"content": [
|
||||
|
||||
@@ -5,6 +5,8 @@ import base64
|
||||
import json
|
||||
import requests
|
||||
|
||||
SCRIPT_NAME = "jira_release_notes.py"
|
||||
|
||||
def extract_text_from_content(content):
|
||||
if isinstance(content, list):
|
||||
texts = [extract_text_from_content(item) for item in content]
|
||||
@@ -23,19 +25,42 @@ def extract_text_from_content(content):
|
||||
|
||||
return ''
|
||||
|
||||
def parse_release_notes(response_json):
|
||||
try:
|
||||
fields = response_json.get('fields', {})
|
||||
release_notes_field = fields.get('customfield_10335', {})
|
||||
def log_customfields_with_content(fields):
|
||||
"""Log all customfield_* fields that have a 'content' key to help troubleshoot structure changes."""
|
||||
print(f"[{SCRIPT_NAME}] Available customfield_* fields with 'content':", file=sys.stderr)
|
||||
found = False
|
||||
for key, value in fields.items():
|
||||
if key.startswith('customfield_') and isinstance(value, dict) and 'content' in value:
|
||||
found = True
|
||||
print(f"[{SCRIPT_NAME}] {key}: {json.dumps(value, indent=2)}", file=sys.stderr)
|
||||
if not found:
|
||||
print(f"[{SCRIPT_NAME}] None found", file=sys.stderr)
|
||||
|
||||
if not release_notes_field or not release_notes_field.get('content'):
|
||||
def parse_release_notes(response_json):
|
||||
release_notes_field_name = 'customfield_10309'
|
||||
try:
|
||||
fields = response_json.get('fields')
|
||||
if not fields:
|
||||
print(f"[{SCRIPT_NAME}] 'fields' is empty or missing in response", file=sys.stderr)
|
||||
return ''
|
||||
|
||||
release_notes = extract_text_from_content(release_notes_field.get('content', []))
|
||||
release_notes_field = fields.get(release_notes_field_name)
|
||||
if not release_notes_field:
|
||||
print(f"[{SCRIPT_NAME}] Release notes field is empty or missing. Field name: {release_notes_field_name}", file=sys.stderr)
|
||||
log_customfields_with_content(fields)
|
||||
return ''
|
||||
|
||||
content = release_notes_field.get('content', [])
|
||||
if not content:
|
||||
print(f"[{SCRIPT_NAME}] Release notes field was found but 'content' is empty or missing in {release_notes_field_name}", file=sys.stderr)
|
||||
log_customfields_with_content(fields)
|
||||
return ''
|
||||
|
||||
release_notes = extract_text_from_content(content)
|
||||
return release_notes
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error parsing release notes: {str(e)}", file=sys.stderr)
|
||||
print(f"[{SCRIPT_NAME}] Error parsing release notes: {str(e)}", file=sys.stderr)
|
||||
return ''
|
||||
|
||||
def main():
|
||||
@@ -60,7 +85,7 @@ def main():
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Error fetching Jira issue: {response.status_code}", file=sys.stderr)
|
||||
print(f"[{SCRIPT_NAME}] Error fetching Jira issue ({jira_issue_id}). Status code: {response.status_code}. Msg: {response.text}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
release_notes = parse_release_notes(response.json())
|
||||
|
||||
238
.github/scripts/label-pr.py
vendored
Normal file
238
.github/scripts/label-pr.py
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python3
|
||||
# Requires Python 3.9+
|
||||
"""
|
||||
Label pull requests based on changed file paths and PR title patterns (conventional commit format).
|
||||
|
||||
Usage:
|
||||
python label-pr.py <pr-number> [-a|--add|-r|--replace] [-d|--dry-run] [-c|--config CONFIG]
|
||||
|
||||
Arguments:
|
||||
pr-number: The pull request number
|
||||
-a, --add: Add labels without removing existing ones (default)
|
||||
-r, --replace: Replace all existing labels
|
||||
-d, --dry-run: Run without actually applying labels
|
||||
-c, --config: Path to JSON config file (default: .github/label-pr.json)
|
||||
|
||||
Examples:
|
||||
python label-pr.py 1234
|
||||
python label-pr.py 1234 -a
|
||||
python label-pr.py 1234 --replace
|
||||
python label-pr.py 1234 -r -d
|
||||
python label-pr.py 1234 --config custom-config.json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
DEFAULT_MODE = "add"
|
||||
DEFAULT_CONFIG_PATH = ".github/label-pr.json"
|
||||
|
||||
def load_config_json(config_file: str) -> dict:
|
||||
"""Load configuration from JSON file."""
|
||||
if not os.path.exists(config_file):
|
||||
print(f"❌ Config file not found: {config_file}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
print(f"✅ Loaded config from: {config_file}")
|
||||
|
||||
valid_config = True
|
||||
if not config.get("catch_all_label"):
|
||||
print("❌ Missing 'catch_all_label' in config file")
|
||||
valid_config = False
|
||||
if not config.get("title_patterns"):
|
||||
print("❌ Missing 'title_patterns' in config file")
|
||||
valid_config = False
|
||||
if not config.get("path_patterns"):
|
||||
print("❌ Missing 'path_patterns' in config file")
|
||||
valid_config = False
|
||||
|
||||
if not valid_config:
|
||||
print("::error::Invalid label-pr.json config file, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
return config
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ JSON deserialization error in label-pr.json config: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error loading label-pr.json config: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def gh_get_changed_files(pr_number: str) -> list[str]:
|
||||
"""Get list of changed files in a pull request."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gh", "pr", "diff", pr_number, "--name-only"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
changed_files = result.stdout.strip().split("\n")
|
||||
return list(filter(None, changed_files))
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"::error::Error getting changed files: {e}")
|
||||
return []
|
||||
|
||||
def gh_get_pr_title(pr_number: str) -> str:
|
||||
"""Get the title of a pull request."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["gh", "pr", "view", pr_number, "--json", "title", "--jq", ".title"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"::error::Error getting PR title: {e}")
|
||||
return ""
|
||||
|
||||
def gh_add_labels(pr_number: str, labels: list[str]) -> None:
|
||||
"""Add labels to a pull request (doesn't remove existing labels)."""
|
||||
gh_labels = ','.join(labels)
|
||||
subprocess.run(
|
||||
["gh", "pr", "edit", pr_number, "--add-label", gh_labels],
|
||||
check=True
|
||||
)
|
||||
|
||||
def gh_replace_labels(pr_number: str, labels: list[str]) -> None:
|
||||
"""Replace all labels on a pull request with the specified labels."""
|
||||
payload = json.dumps({"labels": labels})
|
||||
subprocess.run(
|
||||
["gh", "api", "repos/{owner}/{repo}/issues/" + pr_number, "-X", "PATCH", "--silent", "--input", "-"],
|
||||
input=payload,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
def label_filepaths(changed_files: list[str], path_patterns: dict) -> list[str]:
|
||||
"""Check changed files against path patterns and return labels to apply."""
|
||||
if not changed_files:
|
||||
return []
|
||||
|
||||
labels_to_apply = set() # Use set to avoid duplicates
|
||||
|
||||
for label, patterns in path_patterns.items():
|
||||
for file in changed_files:
|
||||
if any(file.startswith(pattern) for pattern in patterns):
|
||||
print(f"👀 File '{file}' matches pattern for label '{label}'")
|
||||
labels_to_apply.add(label)
|
||||
break
|
||||
|
||||
if "app:shared" in labels_to_apply:
|
||||
labels_to_apply.add("app:password-manager")
|
||||
labels_to_apply.add("app:authenticator")
|
||||
labels_to_apply.remove("app:shared")
|
||||
|
||||
if not labels_to_apply:
|
||||
print("::warning::No matching file paths found, no labels applied.")
|
||||
|
||||
return list(labels_to_apply)
|
||||
|
||||
def label_title(pr_title: str, title_patterns: dict) -> list[str]:
|
||||
"""Check PR title against patterns and return labels to apply."""
|
||||
if not pr_title:
|
||||
return []
|
||||
|
||||
labels_to_apply = set()
|
||||
title_lower = pr_title.lower()
|
||||
for label, patterns in title_patterns.items():
|
||||
for pattern in patterns:
|
||||
# Check for pattern with : or ( suffix (conventional commits format)
|
||||
if f"{pattern}:" in title_lower or f"{pattern}(" in title_lower:
|
||||
print(f"📝 Title matches pattern '{pattern}' for label '{label}'")
|
||||
labels_to_apply.add(label)
|
||||
break
|
||||
|
||||
if not labels_to_apply:
|
||||
print("::warning::No matching title patterns found, no labels applied.")
|
||||
|
||||
return list(labels_to_apply)
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Label pull requests based on changed file paths and PR title patterns."
|
||||
)
|
||||
parser.add_argument(
|
||||
"pr_number",
|
||||
help="The pull request number"
|
||||
)
|
||||
|
||||
mode_group = parser.add_mutually_exclusive_group()
|
||||
mode_group.add_argument(
|
||||
"-a", "--add",
|
||||
action="store_true",
|
||||
help="Add labels without removing existing ones (default)"
|
||||
)
|
||||
mode_group.add_argument(
|
||||
"-r", "--replace",
|
||||
action="store_true",
|
||||
help="Replace all existing labels"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-d", "--dry-run",
|
||||
action="store_true",
|
||||
help="Run without actually applying labels"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-c", "--config",
|
||||
default=DEFAULT_CONFIG_PATH,
|
||||
help=f"Path to JSON config file (default: {DEFAULT_CONFIG_PATH})"
|
||||
)
|
||||
args, unknown = parser.parse_known_args() # required to handle --dry-run passed as an empty string ("") by the workflow
|
||||
return args
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
config = load_config_json(args.config)
|
||||
CATCH_ALL_LABEL = config["catch_all_label"]
|
||||
LABEL_TITLE_PATTERNS = config["title_patterns"]
|
||||
LABEL_PATH_PATTERNS = config["path_patterns"]
|
||||
|
||||
pr_number = args.pr_number
|
||||
mode = "replace" if args.replace else "add"
|
||||
|
||||
if args.dry_run:
|
||||
print("🔍 DRY RUN MODE - Labels will not be applied")
|
||||
print(f"📌 Labeling mode: {mode}")
|
||||
print(f"🔍 Checking PR #{pr_number}...")
|
||||
|
||||
pr_title = gh_get_pr_title(pr_number)
|
||||
print(f"📋 PR Title: {pr_title}\n")
|
||||
|
||||
changed_files = gh_get_changed_files(pr_number)
|
||||
print("👀 Changed files:\n" + "\n".join(changed_files) + "\n")
|
||||
|
||||
filepath_labels = label_filepaths(changed_files, LABEL_PATH_PATTERNS)
|
||||
title_labels = label_title(pr_title, LABEL_TITLE_PATTERNS)
|
||||
all_labels = set(filepath_labels + title_labels)
|
||||
|
||||
if not any(label.startswith("t:") for label in all_labels):
|
||||
all_labels.add(CATCH_ALL_LABEL)
|
||||
|
||||
if all_labels:
|
||||
labels_str = ', '.join(sorted(all_labels))
|
||||
if mode == "add":
|
||||
print(f"🏷️ Adding labels: {labels_str}")
|
||||
if not args.dry_run:
|
||||
gh_add_labels(pr_number, list(all_labels))
|
||||
else:
|
||||
print(f"🏷️ Replacing labels with: {labels_str}")
|
||||
if not args.dry_run:
|
||||
gh_replace_labels(pr_number, list(all_labels))
|
||||
else:
|
||||
print("ℹ️ No matching patterns found, no labels applied.")
|
||||
|
||||
print("✅ Done")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
64
.github/workflows/_version.yml
vendored
64
.github/workflows/_version.yml
vendored
@@ -73,25 +73,31 @@ jobs:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Echo distinct ID ${{ github.event.inputs.distinct_id }}
|
||||
run: echo ${{ github.event.inputs.distinct_id }}
|
||||
env:
|
||||
_DISTINCT_ID: ${{ inputs.distinct_id }}
|
||||
run: echo "${_DISTINCT_ID}"
|
||||
|
||||
- name: Check out repository
|
||||
if: ${{ !inputs.skip_checkout || false }}
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Calculate version name
|
||||
id: calc-version-name
|
||||
env:
|
||||
_VERSION_NAME: ${{ inputs.version_name }}
|
||||
_PATCH_VERSION: ${{ inputs.patch_version }}
|
||||
run: |
|
||||
output() {
|
||||
local version_name=$1
|
||||
echo "version_name=$version_name" >> $GITHUB_OUTPUT
|
||||
echo "version_name=$version_name" >> "$GITHUB_OUTPUT"
|
||||
}
|
||||
|
||||
# override version name if provided
|
||||
if [[ ! -z "${{ inputs.version_name }}" ]]; then
|
||||
version_name=${{ inputs.version_name }}
|
||||
if [[ ! -z "${_VERSION_NAME}" ]]; then
|
||||
version_name=${_VERSION_NAME}
|
||||
echo "::warning::Override applied: $version_name"
|
||||
output "$version_name"
|
||||
exit 0
|
||||
@@ -102,7 +108,7 @@ jobs:
|
||||
|
||||
latest_tag_version=$(git tag -l --sort=-creatordate | grep "$APP_CODENAME" | head -n 1)
|
||||
if [[ -z "$latest_tag_version" ]]; then
|
||||
version_name="${current_year}.${current_month}.${{ inputs.patch_version || 0 }}"
|
||||
version_name="${current_year}.${current_month}.${_PATCH_VERSION:-0}"
|
||||
echo "::warning::No tags found, did you checkout? Calculating version from current date: $version_name"
|
||||
output "$version_name"
|
||||
exit 0
|
||||
@@ -111,14 +117,14 @@ jobs:
|
||||
# Git tag was found, calculate version from latest tag
|
||||
latest_version=${latest_tag_version:1} # remove 'v' from tag version
|
||||
|
||||
latest_major_version=$(echo $latest_version | cut -d "." -f 1)
|
||||
latest_minor_version=$(echo $latest_version | cut -d "." -f 2)
|
||||
latest_major_version=$(echo "$latest_version" | cut -d "." -f 1)
|
||||
latest_minor_version=$(echo "$latest_version" | cut -d "." -f 2)
|
||||
patch_version=0
|
||||
if [[ ! -z "${{ inputs.patch_version }}" ]]; then
|
||||
patch_version=${{ inputs.patch_version }}
|
||||
if [[ ! -z "${_PATCH_VERSION}" ]]; then
|
||||
patch_version=${_PATCH_VERSION}
|
||||
echo "::warning::Patch Version Override applied: $patch_version"
|
||||
elif [[ "$current_year" == "$latest_major_version" && "$current_month" == "$latest_minor_version" ]]; then
|
||||
latest_patch_version=$(echo $latest_version | cut -d "." -f 3)
|
||||
latest_patch_version=$(echo "$latest_version" | cut -d "." -f 3)
|
||||
patch_version=$(($latest_patch_version + 1))
|
||||
fi
|
||||
|
||||
@@ -127,33 +133,41 @@ jobs:
|
||||
|
||||
- name: Calculate version number
|
||||
id: calc-version-number
|
||||
env:
|
||||
_VERSION_NUMBER: ${{ inputs.version_number }}
|
||||
run: |
|
||||
# override version number if provided
|
||||
if [[ ! -z "${{ inputs.version_number }}" ]]; then
|
||||
version_number=${{ inputs.version_number }}
|
||||
if [[ ! -z "${_VERSION_NUMBER}" ]]; then
|
||||
version_number=${_VERSION_NUMBER}
|
||||
echo "::warning::Override applied: $version_number"
|
||||
echo "version_number=$version_number" >> $GITHUB_OUTPUT
|
||||
echo "version_number=$version_number" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
version_number=$(($GITHUB_RUN_NUMBER + ${{ env.BASE_VERSION_NUMBER }}))
|
||||
echo "version_number=$version_number" >> $GITHUB_OUTPUT
|
||||
version_number=$(($GITHUB_RUN_NUMBER + ${BASE_VERSION_NUMBER}))
|
||||
echo "version_number=$version_number" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create version info JSON
|
||||
env:
|
||||
_VERSION_NUMBER: ${{ steps.calc-version-number.outputs.version_number }}
|
||||
_VERSION_NAME: ${{ steps.calc-version-name.outputs.version_name }}
|
||||
run: |
|
||||
json='{
|
||||
"version_number": "${{ steps.calc-version-number.outputs.version_number }}",
|
||||
"version_name": "${{ steps.calc-version-name.outputs.version_name }}"
|
||||
}'
|
||||
json=$(cat <<EOF
|
||||
{
|
||||
"version_number": "${_VERSION_NUMBER}",
|
||||
"version_name": "${_VERSION_NAME}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
echo "$json" > version_info.json
|
||||
|
||||
echo "## version-info.json" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$json" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "## version-info.json" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo '```json' >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "$json" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload version info artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: version-info
|
||||
path: version_info.json
|
||||
|
||||
32
.github/workflows/build-authenticator.yml
vendored
32
.github/workflows/build-authenticator.yml
vendored
@@ -36,7 +36,6 @@ env:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
version:
|
||||
@@ -48,7 +47,6 @@ jobs:
|
||||
version_name: ${{ inputs.version-name }}
|
||||
version_number: ${{ inputs.version-code }}
|
||||
patch_version: ${{ inputs.patch_version && '999' || '' }}
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Build Authenticator
|
||||
@@ -56,20 +54,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
env:
|
||||
INPUTS: ${{ toJson(inputs) }}
|
||||
run: |
|
||||
{
|
||||
echo "<details><summary>Job Inputs</summary>"
|
||||
echo ""
|
||||
echo '```json'
|
||||
echo "$INPUTS"
|
||||
echo '```'
|
||||
echo "</details>"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
uses: bitwarden/android/.github/actions/log-inputs@main
|
||||
with:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -108,7 +98,6 @@ jobs:
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
@@ -124,6 +113,8 @@ jobs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -131,7 +122,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -142,7 +133,6 @@ jobs:
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
@@ -293,7 +283,7 @@ jobs:
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.bitwarden.authenticator.aab
|
||||
path: authenticator/build/outputs/bundle/release/com.bitwarden.authenticator.aab
|
||||
@@ -301,7 +291,7 @@ jobs:
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.bitwarden.authenticator.apk
|
||||
path: authenticator/build/outputs/apk/release/com.bitwarden.authenticator.apk
|
||||
@@ -321,7 +311,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: authenticator-android-apk-sha256.txt
|
||||
path: ./authenticator-android-apk-sha256.txt
|
||||
@@ -329,7 +319,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: authenticator-android-aab-sha256.txt
|
||||
path: ./authenticator-android-aab-sha256.txt
|
||||
|
||||
134
.github/workflows/build-testharness.yml
vendored
Normal file
134
.github/workflows/build-testharness.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
name: Build Test Harness
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- testharness/**
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-name:
|
||||
description: "Optional. Version string to use, in X.Y.Z format. Overrides default in the project."
|
||||
required: false
|
||||
type: string
|
||||
version-code:
|
||||
description: "Optional. Build number to use. Overrides default of GitHub run number."
|
||||
required: false
|
||||
type: number
|
||||
patch_version:
|
||||
description: "Order 999 - Overrides Patch version"
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
JAVA_VERSION: 21
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: Calculate Version Name and Number
|
||||
uses: bitwarden/android/.github/workflows/_version.yml@main
|
||||
with:
|
||||
app_codename: "bwpm"
|
||||
base_version_number: 0
|
||||
version_name: ${{ inputs.version-name }}
|
||||
version_number: ${{ inputs.version-code }}
|
||||
patch_version: ${{ inputs.patch_version && '999' || '' }}
|
||||
|
||||
build:
|
||||
name: Build Test Harness
|
||||
runs-on: ubuntu-24.04
|
||||
needs: version
|
||||
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
uses: bitwarden/android/.github/actions/log-inputs@main
|
||||
with:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate Gradle wrapper
|
||||
uses: gradle/actions/wrapper-validation@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Cache Gradle files
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-v2-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-v2-
|
||||
|
||||
- name: Cache build output
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/build-cache
|
||||
key: ${{ runner.os }}-build-cache-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- name: Configure JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Configure Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
with:
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Increment version
|
||||
env:
|
||||
DEFAULT_VERSION_CODE: ${{ github.run_number }}
|
||||
INPUT_VERSION_CODE: "${{ needs.version.outputs.version_number }}"
|
||||
INPUT_VERSION_NAME: ${{ needs.version.outputs.version_name }}
|
||||
run: |
|
||||
VERSION_CODE="${INPUT_VERSION_CODE:-$DEFAULT_VERSION_CODE}"
|
||||
VERSION_NAME_INPUT="${INPUT_VERSION_NAME:-}"
|
||||
bundle exec fastlane setBuildVersionInfo \
|
||||
versionCode:"$VERSION_CODE" \
|
||||
versionName:"$VERSION_NAME_INPUT"
|
||||
|
||||
regex='appVersionName = "(.+)"'
|
||||
if [[ "$(cat gradle/libs.versions.toml)" =~ $regex ]]; then
|
||||
VERSION_NAME="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
echo "Version Name: ${VERSION_NAME}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Version Number: $VERSION_CODE" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Build Test Harness Debug APK
|
||||
run: ./gradlew :testharness:assembleDebug
|
||||
|
||||
- name: Upload Test Harness APK
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: com.bitwarden.testharness.dev-debug.apk
|
||||
path: testharness/build/outputs/apk/debug/com.bitwarden.testharness.dev.apk
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create checksum for Test Harness APK
|
||||
run: |
|
||||
sha256sum "testharness/build/outputs/apk/debug/com.bitwarden.testharness.dev.apk" \
|
||||
> ./com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
|
||||
- name: Upload Test Harness SHA file
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
path: ./com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
@@ -37,7 +37,6 @@ env:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
version:
|
||||
@@ -50,7 +49,6 @@ jobs:
|
||||
version_name: ${{ inputs.version-name }}
|
||||
version_number: ${{ inputs.version-code }}
|
||||
patch_version: ${{ inputs.patch_version && '999' || '' }}
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Build
|
||||
@@ -58,20 +56,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
env:
|
||||
INPUTS: ${{ toJson(inputs) }}
|
||||
run: |
|
||||
{
|
||||
echo "<details><summary>Job Inputs</summary>"
|
||||
echo ""
|
||||
echo '```json'
|
||||
echo "$INPUTS"
|
||||
echo '```'
|
||||
echo "</details>"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
uses: bitwarden/android/.github/actions/log-inputs@main
|
||||
with:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -110,7 +100,6 @@ jobs:
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
@@ -121,7 +110,7 @@ jobs:
|
||||
run: bundle exec fastlane assembleDebugApks
|
||||
|
||||
- name: Upload test reports on failure
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: failure()
|
||||
with:
|
||||
name: test-reports
|
||||
@@ -133,6 +122,8 @@ jobs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -140,7 +131,7 @@ jobs:
|
||||
artifact: ["apk", "aab"]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -151,7 +142,6 @@ jobs:
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
@@ -307,7 +297,7 @@ jobs:
|
||||
|
||||
- name: Upload release Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
|
||||
@@ -315,7 +305,7 @@ jobs:
|
||||
|
||||
- name: Upload beta Play Store .aab artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
|
||||
@@ -323,7 +313,7 @@ jobs:
|
||||
|
||||
- name: Upload release .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
|
||||
@@ -331,7 +321,7 @@ jobs:
|
||||
|
||||
- name: Upload beta .apk artifact
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
|
||||
@@ -340,7 +330,7 @@ jobs:
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload debug .apk artifact
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
|
||||
@@ -378,7 +368,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
@@ -386,7 +376,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
@@ -394,7 +384,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for release
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
@@ -402,7 +392,7 @@ jobs:
|
||||
|
||||
- name: Upload .aab SHA file for beta
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
@@ -410,7 +400,7 @@ jobs:
|
||||
|
||||
- name: Upload .apk SHA file for debug
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
@@ -455,9 +445,11 @@ jobs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -468,7 +460,6 @@ jobs:
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
@@ -585,7 +576,7 @@ jobs:
|
||||
keyPassword:$FDROID_BETA_KEY_PASSWORD
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -597,14 +588,14 @@ jobs:
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid SHA file
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload F-Droid Beta .apk artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
|
||||
@@ -616,7 +607,7 @@ jobs:
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload F-Droid Beta SHA file
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
@@ -2,8 +2,8 @@ name: Cron / Sync Google Privileged Browsers List
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run weekly on Monday at 00:00 UTC
|
||||
- cron: "0 0 * * 1"
|
||||
# Run weekly on Sunday at 00:00 UTC
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: true
|
||||
|
||||
|
||||
12
.github/workflows/crowdin-pull.yml
vendored
12
.github/workflows/crowdin-pull.yml
vendored
@@ -4,19 +4,21 @@ run-name: Crowdin Pull - ${{ github.event_name == 'workflow_dispatch' && 'Manual
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * 5"
|
||||
# Run weekly on Sunday at 00:00 UTC
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
crowdin-sync:
|
||||
name: Crowdin Pull - ${{ github.event_name }}
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -50,6 +52,8 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
permission-contents: write # for creating and pushing a new branch
|
||||
permission-pull-requests: write # for creating pull request
|
||||
|
||||
- name: Download translations
|
||||
uses: crowdin/github-action@0749939f635900a2521aa6aac7a3766642b2dc71 # v2.11.0
|
||||
|
||||
2
.github/workflows/crowdin-push.yml
vendored
2
.github/workflows/crowdin-push.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
16
.github/workflows/github-release.yml
vendored
16
.github/workflows/github-release.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
@@ -183,11 +183,15 @@ jobs:
|
||||
_JIRA_API_EMAIL: ${{ steps.get-kv-secrets.outputs.JIRA-API-EMAIL }}
|
||||
_JIRA_API_TOKEN: ${{ steps.get-kv-secrets.outputs.JIRA-API-TOKEN }}
|
||||
run: |
|
||||
echo "Getting product release notes"
|
||||
product_release_notes=$(python3 .github/scripts/jira-get-release-notes/jira_release_notes.py "$_RELEASE_TICKET_ID" "$_JIRA_API_EMAIL" "$_JIRA_API_TOKEN")
|
||||
echo "Getting product release notes..."
|
||||
# capture output and exit code so this step continues even if we can't retrieve release notes.
|
||||
script_exit_code=0
|
||||
product_release_notes=$(python .github/scripts/jira-get-release-notes/jira_release_notes.py "$_RELEASE_TICKET_ID" "$_JIRA_API_EMAIL" "$_JIRA_API_TOKEN") || script_exit_code=$?
|
||||
echo "--------------------------------"
|
||||
|
||||
if [[ -z "$product_release_notes" || $product_release_notes == "Error checking"* ]]; then
|
||||
echo "::warning::Failed to fetch release notes from Jira. Output: $product_release_notes"
|
||||
if [[ $script_exit_code -ne 0 || -z "$product_release_notes" ]]; then
|
||||
echo "Script Output: $product_release_notes"
|
||||
echo "::warning::Failed to fetch release notes from Jira. Check script logs for more details."
|
||||
product_release_notes="<insert product release notes here>"
|
||||
else
|
||||
echo "✅ Product release notes:"
|
||||
@@ -285,5 +289,5 @@ jobs:
|
||||
echo " * :ocean: Previous tag set in the description \"Full Changelog\" link: \`$_LAST_RELEASE_TAG\`"
|
||||
echo " * :white_check_mark: Description has automated release notes and they match the commits in the release branch"
|
||||
echo "> [!NOTE]"
|
||||
echo "> Commits directly pushed to branches without a Pull Request won't appear in the automated release notes."
|
||||
echo "> Commits directly pushed to branches without a Pull Request won't appear in the automated release notes."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
17
.github/workflows/publish-store.yml
vendored
17
.github/workflows/publish-store.yml
vendored
@@ -68,20 +68,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
env:
|
||||
INPUTS: ${{ toJson(inputs) }}
|
||||
run: |
|
||||
{
|
||||
echo "<details><summary>Job Inputs</summary>"
|
||||
echo ""
|
||||
echo '```json'
|
||||
echo "$INPUTS"
|
||||
echo '```'
|
||||
echo "</details>"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
uses: bitwarden/android/.github/actions/log-inputs@main
|
||||
with:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Configure Ruby
|
||||
@@ -91,7 +83,6 @@ jobs:
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
|
||||
2
.github/workflows/release-branch.yml
vendored
2
.github/workflows/release-branch.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
1
.github/workflows/review-code.yml
vendored
1
.github/workflows/review-code.yml
vendored
@@ -15,6 +15,7 @@ jobs:
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
|
||||
80
.github/workflows/sdlc-label-pr.yml
vendored
Normal file
80
.github/workflows/sdlc-label-pr.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: SDLC / Label PR by Files
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr-number:
|
||||
description: "Pull Request Number"
|
||||
required: true
|
||||
type: number
|
||||
mode:
|
||||
description: "Labeling Mode"
|
||||
type: choice
|
||||
options:
|
||||
- add
|
||||
- replace
|
||||
default: add
|
||||
dry-run:
|
||||
description: "Dry Run - Don't apply labels"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
label-pr:
|
||||
name: Label PR by Changed Files
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write # required to update labels
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Determine label mode for Pull Request
|
||||
id: label-mode
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
_PR_NUMBER: ${{ inputs.pr-number }}
|
||||
_PR_USER: ${{ github.event.pull_request.user.login }}
|
||||
_IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
|
||||
run: |
|
||||
# Support workflow_dispatch testing by retrieving PR data
|
||||
if [ -z "$_PR_USER" ]; then
|
||||
echo "👀 PR User is empty, retrieving PR data for PR #$_PR_NUMBER..."
|
||||
PR_DATA=$(gh pr view "$_PR_NUMBER" --json author,isCrossRepository)
|
||||
_PR_USER=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
_IS_FORK=$(echo "$PR_DATA" | jq -r '.isCrossRepository')
|
||||
fi
|
||||
|
||||
echo "📋 PR User: $_PR_USER"
|
||||
echo "📋 Is Fork: $_IS_FORK"
|
||||
|
||||
# Handle PRs with labels set by other automations by adding instead of replacing
|
||||
if [ "$_IS_FORK" = "true" ]; then
|
||||
echo "➡️ Fork PR ($_PR_USER). Label mode: --add"
|
||||
echo "label_mode=--add" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$_PR_USER" = "renovate[bot]" ] || [ "$_PR_USER" = "bw-ghapp[bot]" ]; then
|
||||
echo "➡️ Bot PR ($_PR_USER). Label mode: --add"
|
||||
echo "label_mode=--add" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "➡️ Normal PR. Label mode: --replace"
|
||||
echo "label_mode=--replace" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Label PR based on changed files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
_PR_NUMBER: ${{ inputs.pr-number || github.event.pull_request.number }}
|
||||
_LABEL_MODE: ${{ inputs.mode && format('--{0}', inputs.mode) || steps.label-mode.outputs.label_mode }}
|
||||
_DRY_RUN: ${{ inputs.dry-run == true && '--dry-run' || '' }}
|
||||
run: |
|
||||
echo "🔍 Labeling PR #$_PR_NUMBER with mode: $_LABEL_MODE and dry-run: $_DRY_RUN"
|
||||
python3 .github/scripts/label-pr.py "$_PR_NUMBER" "$_LABEL_MODE" "$_DRY_RUN"
|
||||
|
||||
4
.github/workflows/sdlc-sdk-update.yml
vendored
4
.github/workflows/sdlc-sdk-update.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
permission-contents: write
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
fetch-depth: 0
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -65,7 +65,6 @@ jobs:
|
||||
|
||||
- name: Install Fastlane
|
||||
run: |
|
||||
gem install bundler:2.2.27
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
@@ -76,7 +75,7 @@ jobs:
|
||||
bundle exec fastlane check
|
||||
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: test-reports
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
npx lint-staged
|
||||
3
Gemfile
3
Gemfile
@@ -14,5 +14,8 @@ gem 'logger'
|
||||
gem 'mutex_m'
|
||||
gem 'csv'
|
||||
|
||||
# Since ruby 3.4.1 these are not included in the standard library
|
||||
gem 'nkf'
|
||||
|
||||
# Starting with Ruby 3.5.0, these are not included in the standard library
|
||||
gem 'ostruct'
|
||||
|
||||
43
Gemfile.lock
43
Gemfile.lock
@@ -1,18 +1,15 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
CFPropertyList (3.0.8)
|
||||
abbrev (0.1.2)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1177.0)
|
||||
aws-sdk-core (3.235.0)
|
||||
aws-partitions (1.1190.0)
|
||||
aws-sdk-core (3.239.2)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -20,10 +17,10 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.115.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.201.0)
|
||||
aws-sdk-s3 (1.206.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -38,7 +35,7 @@ GEM
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
date (3.4.1)
|
||||
date (3.5.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
@@ -58,9 +55,9 @@ GEM
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday-cookie_jar (0.0.8)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
http-cookie (>= 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
@@ -75,8 +72,9 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.228.0)
|
||||
fastlane (2.229.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
@@ -84,6 +82,7 @@ GEM
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
csv (~> 3.3)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
@@ -103,6 +102,7 @@ GEM
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
mutex_m (~> 0.3.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
@@ -169,24 +169,24 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.15.2)
|
||||
json (2.17.1)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.17.0)
|
||||
multi_json (1.18.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
optparse (0.8.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.2)
|
||||
rake (13.3.0)
|
||||
public_suffix (7.0.0)
|
||||
rake (13.3.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
@@ -241,6 +241,7 @@ DEPENDENCIES
|
||||
fastlane-plugin-firebase_app_distribution
|
||||
logger
|
||||
mutex_m
|
||||
nkf
|
||||
ostruct
|
||||
time
|
||||
|
||||
@@ -248,4 +249,4 @@ RUBY VERSION
|
||||
ruby 3.4.2p28
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.9
|
||||
2.6.2
|
||||
|
||||
@@ -220,7 +220,7 @@ dependencies {
|
||||
add("standardImplementation", dependencyNotation)
|
||||
}
|
||||
|
||||
implementation(files("libs/authenticatorbridge-1.0.1-release.aar"))
|
||||
implementation(project(":authenticatorbridge"))
|
||||
|
||||
implementation(project(":annotation"))
|
||||
implementation(project(":core"))
|
||||
@@ -235,6 +235,7 @@ dependencies {
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.androidx.biometrics)
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.animation)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
@@ -281,6 +282,7 @@ dependencies {
|
||||
standardImplementation(libs.google.play.review)
|
||||
|
||||
// Pull in test fixtures from other modules
|
||||
testImplementation(testFixtures(project(":core")))
|
||||
testImplementation(testFixtures(project(":data")))
|
||||
testImplementation(testFixtures(project(":network")))
|
||||
testImplementation(testFixtures(project(":ui")))
|
||||
@@ -302,9 +304,9 @@ tasks {
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "2g"
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors()
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" +
|
||||
// Explicitly setting the user Country and Language because tests assume en-US
|
||||
"-Duser.country=US" +
|
||||
jvmArgs = jvmArgs.orEmpty() + "-XX:+UseParallelGC" +
|
||||
// Explicitly setting the user Country and Language because tests assume en-US
|
||||
"-Duser.country=US" +
|
||||
"-Duser.language=en"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<credential-provider>
|
||||
<capabilities>
|
||||
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
|
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
|
||||
</capabilities>
|
||||
</credential-provider>
|
||||
@@ -0,0 +1,44 @@
|
||||
Recognized as best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
|
||||
|
||||
SECURE YOUR DIGITAL LIFE
|
||||
Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access.
|
||||
|
||||
ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE
|
||||
Easily manage, store, secure, and share unlimited passwords and passkeys across unlimited devices without restrictions.
|
||||
|
||||
USE PASSKEYS WHEREVER YOU LOG IN
|
||||
Create, store, and sync passkeys across the Bitwarden mobile app and browser extensions for a secure, passwordless experience no matter what device you're on.
|
||||
|
||||
EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE
|
||||
Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features.
|
||||
|
||||
EMPOWER YOUR TEAMS WITH BITWARDEN
|
||||
Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more.
|
||||
|
||||
Use Bitwarden to secure your workforce and share sensitive information with colleagues.
|
||||
|
||||
More reasons to choose Bitwarden:
|
||||
|
||||
World-Class Encryption
|
||||
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private.
|
||||
|
||||
3rd-party Audits
|
||||
Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications.
|
||||
|
||||
Advanced 2FA
|
||||
Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey.
|
||||
|
||||
Bitwarden Send
|
||||
Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure.
|
||||
|
||||
Built-in Generator
|
||||
Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy.
|
||||
|
||||
Global Translations
|
||||
Bitwarden translations exist for more than 50 languages.
|
||||
|
||||
Cross-Platform Applications
|
||||
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
|
||||
|
||||
|
||||
Accessibility Services Disclosure: Bitwarden offers the ability to use the Accessibility Service to augment Autofill on older devices or in cases where autofill is not working correctly. When enabled, the Accessibility Service is used to search for login fields in apps and websites. This establishes the appropriate field IDs when a match for the app or site is found and inserts credentials. When the Accessibility Service is active Bitwarden does not store information or control any on-screen elements beyond inserting credentials.
|
||||
@@ -0,0 +1 @@
|
||||
Bitwarden is a login and password manager that helps keep you safe while online.
|
||||
1
app/src/fdroid/fastlane/metadata/android/en-US/title.txt
Normal file
1
app/src/fdroid/fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Bitwarden Password Manager
|
||||
@@ -13,6 +13,7 @@
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="horizons.permission.HEADSET_CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
|
||||
@@ -86,6 +87,7 @@
|
||||
<intent-filter>
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />
|
||||
|
||||
@@ -260,7 +262,7 @@
|
||||
android:name="com.x8bit.bitwarden.AutofillTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:label="@string/autofill_title"
|
||||
android:label="@string/autofill_verb"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
|
||||
@@ -36,7 +36,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.ROOT_ROUTE
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavigationRoute
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.model.AuthTabLaunchers
|
||||
@@ -119,11 +119,11 @@ class MainActivity : AppCompatActivity() {
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = ROOT_ROUTE,
|
||||
startDestination = RootNavigationRoute,
|
||||
) {
|
||||
// Nothing else should end up at this top level, we just want the ability
|
||||
// to have the debug menu appear on top of the rest of the app without
|
||||
// interacting with the state-based navigation used by the RootNavScreen.
|
||||
// Both root navigation and debug menu exist at this top level.
|
||||
// The debug menu can appear on top of the rest of the app without
|
||||
// interacting with the state-based navigation used by RootNavScreen.
|
||||
rootNavDestination { shouldShowSplashScreen = false }
|
||||
debugMenuDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.bitwarden.ui.platform.manager.share.ShareManager
|
||||
import com.bitwarden.ui.platform.model.TotpData
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
|
||||
@@ -46,7 +47,6 @@ import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
|
||||
@@ -211,7 +211,7 @@ interface AuthDiskSource : AppIdProvider {
|
||||
/**
|
||||
* Gets the flow for the biometrics key for the given [userId].
|
||||
*/
|
||||
fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?>
|
||||
fun getUserBiometricUnlockKeyFlow(userId: String): Flow<String?>
|
||||
|
||||
/**
|
||||
* Retrieves a pin-protected user key for the given [userId].
|
||||
|
||||
@@ -145,9 +145,6 @@ class AuthDiskSourceImpl(
|
||||
storeInvalidUnlockAttempts(userId = userId, invalidUnlockAttempts = null)
|
||||
storeUserKey(userId = userId, userKey = null)
|
||||
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
|
||||
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
|
||||
storePinProtectedUserKeyEnvelope(userId = userId, pinProtectedUserKeyEnvelope = null)
|
||||
storeEncryptedPin(userId = userId, encryptedPin = null)
|
||||
storePrivateKey(userId = userId, privateKey = null)
|
||||
storeAccountKeys(userId = userId, accountKeys = null)
|
||||
storeOrganizationKeys(userId = userId, organizationKeys = null)
|
||||
@@ -162,10 +159,14 @@ class AuthDiskSourceImpl(
|
||||
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
|
||||
storeShowImportLogins(userId = userId, showImportLogins = null)
|
||||
storeLastLockTimestamp(userId = userId, lastLockTimestamp = null)
|
||||
storeEncryptedPin(userId = userId, encryptedPin = null)
|
||||
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = null)
|
||||
storePinProtectedUserKeyEnvelope(userId = userId, pinProtectedUserKeyEnvelope = null)
|
||||
|
||||
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
|
||||
// indefinitely unless the TDE flow explicitly removes them.
|
||||
// Do not remove OnboardingStatus we want to keep track of this even after logout.
|
||||
// Certain values are never removed as required by the feature requirements:
|
||||
// * DeviceKey
|
||||
// * PendingAuthRequest
|
||||
// * OnboardingStatus
|
||||
}
|
||||
|
||||
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
|
||||
@@ -330,7 +331,7 @@ class AuthDiskSourceImpl(
|
||||
getMutableBiometricUnlockKeyFlow(userId).tryEmit(biometricsKey)
|
||||
}
|
||||
|
||||
override fun getUserBiometicUnlockKeyFlow(userId: String): Flow<String?> =
|
||||
override fun getUserBiometricUnlockKeyFlow(userId: String): Flow<String?> =
|
||||
getMutableBiometricUnlockKeyFlow(userId)
|
||||
.onSubscription { emit(getUserBiometricUnlockKey(userId = userId)) }
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class AuthSdkSourceImpl(
|
||||
getClient()
|
||||
.auth()
|
||||
.newAuthRequest(
|
||||
email = email,
|
||||
email = email.lowercase(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class AuthSdkSourceImpl(
|
||||
.platform()
|
||||
.fingerprint(
|
||||
req = FingerprintRequest(
|
||||
fingerprintMaterial = email,
|
||||
fingerprintMaterial = email.lowercase(),
|
||||
publicKey = publicKey,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.bitwarden.ui.platform.model.TotpData
|
||||
|
||||
/**
|
||||
* Manager for keeping track of requests from the Bitwarden Authenticator app to add a TOTP
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.bitwarden.ui.platform.model.TotpData
|
||||
|
||||
/**
|
||||
* Default in memory implementation for [AddTotpItemFromAuthenticatorManager].
|
||||
|
||||
@@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
private const val PASSWORDLESS_NOTIFICATION_TIMEOUT_MILLIS: Long = 15L * 60L * 1_000L
|
||||
private const val PASSWORDLESS_NOTIFICATION_RETRY_INTERVAL_MILLIS: Long = 4L * 1_000L
|
||||
@@ -163,7 +162,7 @@ class AuthRequestManagerImpl(
|
||||
emit(result)
|
||||
if (result is AuthRequestUpdatesResult.Error) return@flow
|
||||
var isComplete = false
|
||||
while (coroutineContext.isActive && !isComplete) {
|
||||
while (currentCoroutineContext().isActive && !isComplete) {
|
||||
delay(PASSWORDLESS_APPROVER_INTERVAL_MILLIS)
|
||||
val updateResult = result as AuthRequestUpdatesResult.Update
|
||||
authRequestsService
|
||||
|
||||
@@ -8,8 +8,8 @@ import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.util.toPendingIntentMutabilityFlag
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITER
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import timber.log.Timber
|
||||
import kotlin.collections.get
|
||||
|
||||
/**
|
||||
* Default implementation of [KdfManager].
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
|
||||
@@ -49,14 +49,14 @@ class UserLogoutManagerImpl(
|
||||
override fun logout(userId: String, reason: LogoutReason) {
|
||||
authDiskSource.userState ?: return
|
||||
Timber.d("logout reason=$reason")
|
||||
val isExpired = reason == LogoutReason.SecurityStamp
|
||||
if (isExpired) {
|
||||
val isSecurityStamp = reason == LogoutReason.SecurityStamp
|
||||
if (isSecurityStamp) {
|
||||
showToast(message = BitwardenString.login_expired)
|
||||
}
|
||||
|
||||
val ableToSwitchToNewAccount = switchUserIfAvailable(
|
||||
currentUserId = userId,
|
||||
isExpired = isExpired,
|
||||
isSecurityStamp = isSecurityStamp,
|
||||
removeCurrentUserFromAccounts = true,
|
||||
)
|
||||
|
||||
@@ -73,25 +73,24 @@ class UserLogoutManagerImpl(
|
||||
|
||||
override fun softLogout(userId: String, reason: LogoutReason) {
|
||||
Timber.d("softLogout reason=$reason")
|
||||
val isExpired = reason == LogoutReason.SecurityStamp
|
||||
if (isExpired) {
|
||||
val isSecurityStamp = reason == LogoutReason.SecurityStamp
|
||||
if (isSecurityStamp) {
|
||||
showToast(message = BitwardenString.login_expired)
|
||||
}
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = null,
|
||||
)
|
||||
|
||||
// Save any data that will still need to be retained after otherwise clearing all dat
|
||||
// Save any data that will still need to be retained after otherwise clearing all data
|
||||
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||
val pinProtectedUserKeyEnvelope = authDiskSource
|
||||
.getPinProtectedUserKeyEnvelope(userId = userId)
|
||||
val encryptedPin = authDiskSource.getEncryptedPin(userId = userId)
|
||||
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
|
||||
val pinProtectedUserKeyEnvelope = authDiskSource.getPinProtectedUserKeyEnvelope(
|
||||
userId = userId,
|
||||
)
|
||||
|
||||
switchUserIfAvailable(
|
||||
currentUserId = userId,
|
||||
removeCurrentUserFromAccounts = false,
|
||||
isExpired = isExpired,
|
||||
isSecurityStamp = isSecurityStamp,
|
||||
)
|
||||
|
||||
clearData(userId = userId)
|
||||
@@ -108,10 +107,14 @@ class UserLogoutManagerImpl(
|
||||
vaultTimeoutAction = vaultTimeoutAction,
|
||||
)
|
||||
}
|
||||
authDiskSource.storePinProtectedUserKeyEnvelope(
|
||||
userId = userId,
|
||||
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
|
||||
)
|
||||
authDiskSource.apply {
|
||||
storeEncryptedPin(userId = userId, encryptedPin = encryptedPin)
|
||||
storePinProtectedUserKey(userId = userId, pinProtectedUserKey = pinProtectedUserKey)
|
||||
storePinProtectedUserKeyEnvelope(
|
||||
userId = userId,
|
||||
pinProtectedUserKeyEnvelope = pinProtectedUserKeyEnvelope,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearData(userId: String) {
|
||||
@@ -133,7 +136,7 @@ class UserLogoutManagerImpl(
|
||||
private fun switchUserIfAvailable(
|
||||
currentUserId: String,
|
||||
removeCurrentUserFromAccounts: Boolean,
|
||||
isExpired: Boolean = false,
|
||||
isSecurityStamp: Boolean,
|
||||
): Boolean {
|
||||
val currentUserState = authDiskSource.userState ?: return false
|
||||
|
||||
@@ -145,7 +148,7 @@ class UserLogoutManagerImpl(
|
||||
|
||||
// Check if there is a new active user
|
||||
return if (updatedAccounts.isNotEmpty()) {
|
||||
if (currentUserId == currentUserState.activeUserId && !isExpired) {
|
||||
if (currentUserId == currentUserState.activeUserId && !isSecurityStamp) {
|
||||
showToast(message = BitwardenString.account_switched_automatically)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.network.model.PolicyTypeJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.bitwarden.network.service.AuthRequestsService
|
||||
import com.bitwarden.network.service.DevicesService
|
||||
@@ -27,8 +27,8 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.CredentialExchangeRegistryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.PasswordHistoryDiskSource
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.AuthenticatorProvider
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -48,6 +49,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
interface AuthRepository :
|
||||
AuthenticatorProvider,
|
||||
AuthRequestManager,
|
||||
BiometricsEncryptionManager,
|
||||
KdfManager,
|
||||
UserStateManager {
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.repository.error.MissingPropertyException
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
@@ -9,7 +11,6 @@ import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.data.datasource.disk.ConfigDiskSource
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrls
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.bitwarden.network.model.DeleteAccountResponseJson
|
||||
@@ -100,8 +101,8 @@ import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITER
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
@@ -157,6 +158,7 @@ class AuthRepositoryImpl(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRequestManager: AuthRequestManager,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val keyConnectorManager: KeyConnectorManager,
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
@@ -168,6 +170,7 @@ class AuthRepositoryImpl(
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager,
|
||||
BiometricsEncryptionManager by biometricsEncryptionManager,
|
||||
KdfManager by kdfManager,
|
||||
UserStateManager by userStateManager {
|
||||
/**
|
||||
@@ -1356,10 +1359,10 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
when (val json = it) {
|
||||
when (it) {
|
||||
VerifyEmailTokenResponseJson.Valid -> EmailTokenResult.Success
|
||||
is VerifyEmailTokenResponseJson.Invalid -> {
|
||||
EmailTokenResult.Error(message = json.message, error = null)
|
||||
EmailTokenResult.Error(message = it.message, error = null)
|
||||
}
|
||||
|
||||
VerifyEmailTokenResponseJson.TokenExpired -> EmailTokenResult.Expired
|
||||
@@ -1883,21 +1886,15 @@ class AuthRepositoryImpl(
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
val masterPassword = password ?: return null
|
||||
val privateKey = loginResponse.privateKeyOrNull() ?: return null
|
||||
val key = loginResponse.key ?: return null
|
||||
|
||||
val initUserCryptoMethod = loginResponse
|
||||
val masterPasswordUnlock = loginResponse
|
||||
.userDecryptionOptions
|
||||
?.masterPasswordUnlock
|
||||
?.let { masterPasswordUnlock ->
|
||||
InitUserCryptoMethod.MasterPasswordUnlock(
|
||||
password = masterPassword,
|
||||
masterPasswordUnlock = masterPasswordUnlock.toSdkMasterPasswordUnlock(),
|
||||
)
|
||||
}
|
||||
?: InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
userKey = key,
|
||||
)
|
||||
?: return null
|
||||
val initUserCryptoMethod = InitUserCryptoMethod.MasterPasswordUnlock(
|
||||
password = masterPassword,
|
||||
masterPasswordUnlock = masterPasswordUnlock.toSdkMasterPasswordUnlock(),
|
||||
)
|
||||
|
||||
return unlockVault(
|
||||
accountProfile = profile,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.di
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.data.datasource.disk.ConfigDiskSource
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.bitwarden.network.service.DevicesService
|
||||
import com.bitwarden.network.service.HaveIBeenPwnedService
|
||||
@@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.manager.UserStateManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
@@ -60,6 +61,7 @@ object AuthRepositoryModule {
|
||||
environmentRepository: EnvironmentRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
vaultRepository: VaultRepository,
|
||||
biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
keyConnectorManager: KeyConnectorManager,
|
||||
authRequestManager: AuthRequestManager,
|
||||
trustedDeviceManager: TrustedDeviceManager,
|
||||
@@ -85,6 +87,7 @@ object AuthRepositoryModule {
|
||||
environmentRepository = environmentRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
keyConnectorManager = keyConnectorManager,
|
||||
authRequestManager = authRequestManager,
|
||||
trustedDeviceManager = trustedDeviceManager,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of deleting an account.
|
||||
* Models result of leaving an organization.
|
||||
*/
|
||||
sealed class LeaveOrganizationResult {
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ fun Intent.getCompleteRegistrationDataIntentOrNull(): CompleteRegistrationData?
|
||||
newValue = "/",
|
||||
ignoreCase = true,
|
||||
)
|
||||
val uri = runCatching { Uri.parse(sanitizedUriString) }.getOrNull() ?: return null
|
||||
val uri = runCatching { sanitizedUriString.toUri() }.getOrNull() ?: return null
|
||||
uri.host ?: return null
|
||||
if (uri.path != "/finish-signup") return null
|
||||
val email = uri.getQueryParameter("email") ?: return null
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.PowerManager
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityAutofillManagerImpl
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.app.Activity
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.model.AccessibilityAction
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.toUriOrNull
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import java.net.URISyntaxException
|
||||
|
||||
@@ -10,7 +11,7 @@ import java.net.URISyntaxException
|
||||
@OmitFromCoverage
|
||||
fun String.toUriOrNull(): Uri? =
|
||||
try {
|
||||
Uri.parse(this)
|
||||
} catch (e: URISyntaxException) {
|
||||
this.toUri()
|
||||
} catch (_: URISyntaxException) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.di
|
||||
|
||||
import android.content.Context
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilderImpl
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilderImpl
|
||||
|
||||
@@ -93,7 +93,6 @@ class AutofillParserImpl(
|
||||
val urlBarWebsite = traversalDataList
|
||||
.flatMap { it.urlBarWebsites }
|
||||
.firstOrNull()
|
||||
?.takeIf { settingsRepository.isAutofillWebDomainCompatMode }
|
||||
|
||||
// Take only the autofill views from the node that currently has focus.
|
||||
// Then remove all the fields that cannot be filled with data.
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.service.autofill.FillCallback
|
||||
import android.service.autofill.FillRequest
|
||||
import android.service.autofill.SaveCallback
|
||||
import android.service.autofill.SaveRequest
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FillResponseBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
|
||||
@@ -11,10 +11,10 @@ import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.util.login
|
||||
import com.x8bit.bitwarden.data.credentials.manager.CredentialManagerPendingIntentManager
|
||||
import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [CredentialEntryBuilder].
|
||||
@@ -22,7 +22,7 @@ import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
class CredentialEntryBuilderImpl(
|
||||
private val context: Context,
|
||||
private val pendingIntentManager: CredentialManagerPendingIntentManager,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
private val authRepository: AuthRepository,
|
||||
) : CredentialEntryBuilder {
|
||||
|
||||
override fun buildPublicKeyCredentialEntries(
|
||||
@@ -82,7 +82,7 @@ class CredentialEntryBuilderImpl(
|
||||
.also { builder ->
|
||||
if (!isUserVerified) {
|
||||
builder.setBiometricPromptDataIfSupported(
|
||||
cipher = biometricsEncryptionManager.getOrCreateCipher(userId),
|
||||
cipher = authRepository.getOrCreateCipher(userId),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -113,8 +113,7 @@ class CredentialEntryBuilderImpl(
|
||||
.apply {
|
||||
if (!isUserVerified) {
|
||||
setBiometricPromptDataIfSupported(
|
||||
cipher = biometricsEncryptionManager
|
||||
.getOrCreateCipher(userId),
|
||||
cipher = authRepository.getOrCreateCipher(userId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.credentials.di
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.network.service.DigitalAssetLinkService
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@@ -22,8 +22,9 @@ import com.x8bit.bitwarden.data.credentials.processor.CredentialProviderProcesso
|
||||
import com.x8bit.bitwarden.data.credentials.processor.CredentialProviderProcessorImpl
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepository
|
||||
import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.credentials.sanitizer.PasskeyAttestationOptionsSanitizer
|
||||
import com.x8bit.bitwarden.data.credentials.sanitizer.PasskeyAttestationOptionsSanitizerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AssetManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -52,7 +53,6 @@ object CredentialProviderModule {
|
||||
bitwardenCredentialManager: BitwardenCredentialManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
pendingIntentManager: CredentialManagerPendingIntentManager,
|
||||
biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
clock: Clock,
|
||||
): CredentialProviderProcessor =
|
||||
CredentialProviderProcessorImpl(
|
||||
@@ -61,7 +61,6 @@ object CredentialProviderModule {
|
||||
bitwardenCredentialManager = bitwardenCredentialManager,
|
||||
pendingIntentManager = pendingIntentManager,
|
||||
clock = clock,
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@@ -75,15 +74,17 @@ object CredentialProviderModule {
|
||||
dispatcherManager: DispatcherManager,
|
||||
credentialEntryBuilder: CredentialEntryBuilder,
|
||||
cipherMatchingManager: CipherMatchingManager,
|
||||
passkeyAttestationOptionsSanitizer: PasskeyAttestationOptionsSanitizer,
|
||||
): BitwardenCredentialManager =
|
||||
BitwardenCredentialManagerImpl(
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
fido2CredentialStore = fido2CredentialStore,
|
||||
credentialEntryBuilder = credentialEntryBuilder,
|
||||
json = json,
|
||||
vaultRepository = vaultRepository,
|
||||
dispatcherManager = dispatcherManager,
|
||||
credentialEntryBuilder = credentialEntryBuilder,
|
||||
cipherMatchingManager = cipherMatchingManager,
|
||||
passkeyAttestationOptionsSanitizer = passkeyAttestationOptionsSanitizer,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -104,11 +105,11 @@ object CredentialProviderModule {
|
||||
fun provideCredentialEntryBuilder(
|
||||
@ApplicationContext context: Context,
|
||||
pendingIntentManager: CredentialManagerPendingIntentManager,
|
||||
biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
authRepository: AuthRepository,
|
||||
): CredentialEntryBuilder = CredentialEntryBuilderImpl(
|
||||
context = context,
|
||||
pendingIntentManager = pendingIntentManager,
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
authRepository = authRepository,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@@ -139,4 +140,9 @@ object CredentialProviderModule {
|
||||
CredentialManagerPendingIntentManagerImpl(
|
||||
context = context,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePasskeyAttestationOptionsSanitizer(): PasskeyAttestationOptionsSanitizer =
|
||||
PasskeyAttestationOptionsSanitizerImpl
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@ import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.takeUntilLoaded
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.fido.ClientData
|
||||
import com.bitwarden.fido.Fido2CredentialAutofillView
|
||||
import com.bitwarden.fido.Origin
|
||||
@@ -33,6 +33,7 @@ import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.credentials.sanitizer.PasskeyAttestationOptionsSanitizer
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppOrigin
|
||||
import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
|
||||
@@ -60,6 +61,7 @@ class BitwardenCredentialManagerImpl(
|
||||
private val json: Json,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val cipherMatchingManager: CipherMatchingManager,
|
||||
private val passkeyAttestationOptionsSanitizer: PasskeyAttestationOptionsSanitizer,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : BitwardenCredentialManager,
|
||||
Fido2CredentialStore by fido2CredentialStore {
|
||||
@@ -359,31 +361,46 @@ class BitwardenCredentialManagerImpl(
|
||||
selectedCipherView: CipherView,
|
||||
clientData: ClientData,
|
||||
callingPackageName: String,
|
||||
): Fido2RegisterCredentialResult = vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": ${createPublicKeyCredentialRequest.requestJson}}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
// `true` so that the SDK does not fail if the relying party requests UV.
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map {
|
||||
it.toAndroidAttestationResponse(callingPackageName = callingPackageName)
|
||||
}
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to register FIDO2 credential.")
|
||||
Fido2RegisterCredentialResult.Error.InternalError
|
||||
},
|
||||
)
|
||||
): Fido2RegisterCredentialResult {
|
||||
val requestJson =
|
||||
getPasskeyAttestationOptionsOrNull(createPublicKeyCredentialRequest.requestJson)
|
||||
?.let { passkeyAttestationOptionsSanitizer.sanitize(options = it) }
|
||||
?.runCatching { json.encodeToString(this) }
|
||||
?.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to sanitize passkey attestation options.")
|
||||
null
|
||||
},
|
||||
)
|
||||
?: return Fido2RegisterCredentialResult.Error.InternalError
|
||||
|
||||
return vaultSdkSource
|
||||
.registerFido2Credential(
|
||||
request = RegisterFido2CredentialRequest(
|
||||
userId = userId,
|
||||
origin = sdkOrigin,
|
||||
requestJson = """{"publicKey": $requestJson}""",
|
||||
clientData = clientData,
|
||||
selectedCipherView = selectedCipherView,
|
||||
// User verification is handled prior to engaging the SDK. We always respond
|
||||
// `true` so that the SDK does not fail if the relying party requests UV.
|
||||
isUserVerificationSupported = true,
|
||||
),
|
||||
fido2CredentialStore = this,
|
||||
)
|
||||
.map {
|
||||
it.toAndroidAttestationResponse(callingPackageName = callingPackageName)
|
||||
}
|
||||
.mapCatching { json.encodeToString(it) }
|
||||
.fold(
|
||||
onSuccess = { Fido2RegisterCredentialResult.Success(it) },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to register FIDO2 credential.")
|
||||
Fido2RegisterCredentialResult.Error.InternalError
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<BeginGetPasswordOption>.toPasswordCredentialEntries(
|
||||
userId: String,
|
||||
|
||||
@@ -58,6 +58,13 @@ interface CredentialManagerPendingIntentManager {
|
||||
userId: String,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing options for Password credential creation.
|
||||
*/
|
||||
fun createPasswordCreationPendingIntent(
|
||||
userId: String,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing options for Password credential filling.
|
||||
*/
|
||||
|
||||
@@ -75,6 +75,24 @@ class CredentialManagerPendingIntentManagerImpl(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing options for FIDO 2 credential creation.
|
||||
*/
|
||||
override fun createPasswordCreationPendingIntent(
|
||||
userId: String,
|
||||
): PendingIntent {
|
||||
val intent = Intent(CREATE_PASSWORD_ACTION)
|
||||
.setPackage(context.packageName)
|
||||
.putExtra(EXTRA_KEY_USER_ID, userId)
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
/* context = */ context,
|
||||
/* requestCode = */ Random.nextInt(),
|
||||
/* intent = */ intent,
|
||||
/* flags = */ PendingIntent.FLAG_UPDATE_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pending intent to use when providing options for Password credential filling.
|
||||
*/
|
||||
@@ -101,4 +119,5 @@ class CredentialManagerPendingIntentManagerImpl(
|
||||
private const val CREATE_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY"
|
||||
private const val UNLOCK_ACCOUNT_ACTION = "com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT"
|
||||
private const val GET_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY"
|
||||
private const val CREATE_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD"
|
||||
private const val GET_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD"
|
||||
|
||||
@@ -49,6 +49,7 @@ class OriginManagerImpl(
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
Timber.d("Digital asset link validation result: linked = ${it.linked}")
|
||||
if (it.linked) {
|
||||
ValidateOriginResult.Success(null)
|
||||
} else {
|
||||
@@ -56,6 +57,7 @@ class OriginManagerImpl(
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e("Failed to validate origin for calling app")
|
||||
ValidateOriginResult.Error.AssetLinkNotFound
|
||||
},
|
||||
)
|
||||
@@ -105,7 +107,7 @@ class OriginManagerImpl(
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to validate privileged app: ${callingAppInfo.packageName}")
|
||||
Timber.e(it, "Failed to validate calling app is privileged.")
|
||||
ValidateOriginResult.Error.Unknown
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.credentials.model
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
@@ -48,6 +49,15 @@ data class CreateCredentialRequest(
|
||||
providerRequest.callingRequest as? CreatePublicKeyCredentialRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* The [CreatePasswordRequest] of the [providerRequest], or null if the calling
|
||||
* request is not a [CreatePasswordRequest].
|
||||
*/
|
||||
@IgnoredOnParcel
|
||||
val createPasswordCredentialRequest: CreatePasswordRequest? by lazy {
|
||||
providerRequest.callingRequest as? CreatePasswordRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* The [requestJson] of the [createPublicKeyCredentialRequest], or null if the calling request
|
||||
* is not a [CreatePublicKeyCredentialRequest].
|
||||
|
||||
@@ -19,23 +19,24 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.AuthenticationAction
|
||||
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||
import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.BiometricPromptData
|
||||
import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.util.isBuildVersionAtLeast
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
|
||||
import com.x8bit.bitwarden.data.credentials.manager.CredentialManagerPendingIntentManager
|
||||
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.time.Clock
|
||||
import javax.crypto.Cipher
|
||||
|
||||
@@ -51,7 +52,6 @@ class CredentialProviderProcessorImpl(
|
||||
private val bitwardenCredentialManager: BitwardenCredentialManager,
|
||||
private val pendingIntentManager: CredentialManagerPendingIntentManager,
|
||||
private val clock: Clock,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : CredentialProviderProcessor {
|
||||
|
||||
@@ -62,21 +62,27 @@ class CredentialProviderProcessorImpl(
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
|
||||
) {
|
||||
Timber.d("Create credential request received.")
|
||||
val userId = authRepository.activeUserId
|
||||
if (userId == null) {
|
||||
Timber.w("No active user. Cannot create credential.")
|
||||
callback.onError(CreateCredentialUnknownException("Active user is required."))
|
||||
return
|
||||
}
|
||||
|
||||
val createCredentialJob = ioScope.launch {
|
||||
processCreateCredentialRequest(request = request)
|
||||
(handleCreatePasskeyQuery(request) ?: handleCreatePasswordQuery(request))
|
||||
?.let { callback.onResult(it) }
|
||||
?: callback.onError(CreateCredentialUnknownException())
|
||||
?: run {
|
||||
Timber.w("Unknown create credential request.")
|
||||
callback.onError(CreateCredentialUnknownException())
|
||||
}
|
||||
}
|
||||
cancellationSignal.setOnCancelListener {
|
||||
if (createCredentialJob.isActive) {
|
||||
createCredentialJob.cancel()
|
||||
}
|
||||
Timber.d("Create credential request cancelled by system.")
|
||||
callback.onError(CreateCredentialCancellationException())
|
||||
}
|
||||
}
|
||||
@@ -86,15 +92,18 @@ class CredentialProviderProcessorImpl(
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
||||
) {
|
||||
Timber.d("Get credential request received.")
|
||||
// If the user is not logged in, return an error.
|
||||
val userState = authRepository.userStateFlow.value
|
||||
if (userState == null) {
|
||||
Timber.w("No active user. Cannot get credentials.")
|
||||
callback.onError(GetCredentialUnknownException("Active user is required."))
|
||||
return
|
||||
}
|
||||
|
||||
// Return an unlock action if the current account is locked.
|
||||
if (!userState.activeAccount.isVaultUnlocked) {
|
||||
Timber.d("Vault is locked. Requesting unlock.")
|
||||
val authenticationAction = AuthenticationAction(
|
||||
title = context.getString(BitwardenString.unlock),
|
||||
pendingIntent = pendingIntentManager.createFido2UnlockPendingIntent(
|
||||
@@ -119,10 +128,17 @@ class CredentialProviderProcessorImpl(
|
||||
BeginGetCredentialRequest.asBundle(request),
|
||||
),
|
||||
)
|
||||
.onSuccess { callback.onResult(BeginGetCredentialResponse(credentialEntries = it)) }
|
||||
.onFailure { callback.onError(GetCredentialUnknownException(it.message)) }
|
||||
.onSuccess {
|
||||
Timber.d("Credentials retrieved.")
|
||||
callback.onResult(BeginGetCredentialResponse(credentialEntries = it))
|
||||
}
|
||||
.onFailure {
|
||||
Timber.w("Error getting credentials.")
|
||||
callback.onError(GetCredentialUnknownException(it.message))
|
||||
}
|
||||
}
|
||||
cancellationSignal.setOnCancelListener {
|
||||
Timber.d("Get credential request cancelled by system.")
|
||||
callback.onError(GetCredentialCancellationException())
|
||||
getCredentialJob.cancel()
|
||||
}
|
||||
@@ -134,24 +150,15 @@ class CredentialProviderProcessorImpl(
|
||||
callback: OutcomeReceiver<Void?, ClearCredentialException>,
|
||||
) {
|
||||
// no-op: RFU
|
||||
Timber.w("Unsupported clear credential state request received.")
|
||||
callback.onError(ClearCredentialUnsupportedException())
|
||||
}
|
||||
|
||||
private fun processCreateCredentialRequest(
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreateCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
return when (request) {
|
||||
is BeginCreatePublicKeyCredentialRequest -> {
|
||||
handleCreatePasskeyQuery(request)
|
||||
}
|
||||
if (request !is BeginCreatePublicKeyCredentialRequest) return null
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreatePublicKeyCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
val requestJson = request
|
||||
.candidateQueryData
|
||||
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
|
||||
@@ -161,14 +168,19 @@ class CredentialProviderProcessorImpl(
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
|
||||
.setCreateEntries(
|
||||
userState.accounts.toCreatePasskeyEntry(userState.activeUserId),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
|
||||
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
|
||||
private fun List<UserState.Account>.toCreatePasskeyEntry(
|
||||
activeUserId: String,
|
||||
): List<CreateEntry> = map { it.toCreatePasskeyEntry(isActive = activeUserId == it.userId) }
|
||||
|
||||
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry {
|
||||
private fun UserState.Account.toCreatePasskeyEntry(
|
||||
isActive: Boolean,
|
||||
): CreateEntry {
|
||||
val accountName = name ?: email
|
||||
val entryBuilder = CreateEntry
|
||||
.Builder(
|
||||
@@ -189,7 +201,55 @@ class CredentialProviderProcessorImpl(
|
||||
.setAutoSelectAllowed(true)
|
||||
|
||||
if (isVaultUnlocked) {
|
||||
biometricsEncryptionManager
|
||||
authRepository
|
||||
.getOrCreateCipher(userId)
|
||||
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
|
||||
}
|
||||
return entryBuilder.build()
|
||||
}
|
||||
|
||||
private fun handleCreatePasswordQuery(
|
||||
request: BeginCreateCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
if (request !is BeginCreatePasswordCredentialRequest) return null
|
||||
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(
|
||||
userState.accounts.toCreatePasswordEntry(userState.activeUserId),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreatePasswordEntry(
|
||||
activeUserId: String,
|
||||
) = map { it.toCreatePasswordEntry(isActive = activeUserId == it.userId) }
|
||||
|
||||
private fun UserState.Account.toCreatePasswordEntry(
|
||||
isActive: Boolean,
|
||||
): CreateEntry {
|
||||
val accountName = name ?: email
|
||||
val entryBuilder = CreateEntry
|
||||
.Builder(
|
||||
accountName = accountName,
|
||||
pendingIntent = pendingIntentManager.createPasswordCreationPendingIntent(
|
||||
userId = userId,
|
||||
),
|
||||
)
|
||||
.setDescription(
|
||||
context.getString(
|
||||
BitwardenString.your_password_will_be_saved_to_your_bitwarden_vault_for_x,
|
||||
accountName,
|
||||
),
|
||||
)
|
||||
// Set the last used time to "now" so the active account is the default option in the
|
||||
// system prompt.
|
||||
.setLastUsedTime(if (isActive) clock.instant() else null)
|
||||
.setAutoSelectAllowed(true)
|
||||
|
||||
if (isVaultUnlocked) {
|
||||
authRepository
|
||||
.getOrCreateCipher(userId)
|
||||
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.credentials.repository
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.combineDataStates
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.PrivilegedAppDiskSource
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.entity.PrivilegedAppEntity
|
||||
import com.x8bit.bitwarden.data.credentials.model.PrivilegedAppAllowListJson
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.credentials.sanitizer
|
||||
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
|
||||
|
||||
/**
|
||||
* Defines a contract for sanitizing [PasskeyAttestationOptions] received from applications.
|
||||
*
|
||||
* Sanitization applies workarounds for known issues with specific applications'
|
||||
* passkey implementations, ensuring the options are in the correct format before
|
||||
* being used to create a passkey credential.
|
||||
*/
|
||||
interface PasskeyAttestationOptionsSanitizer {
|
||||
|
||||
/**
|
||||
* Sanitizes the given [PasskeyAttestationOptions] in preparation for use in the
|
||||
* passkey creation process.
|
||||
*
|
||||
* @param options The [PasskeyAttestationOptions] to sanitize.
|
||||
* @return A new, sanitized instance of [PasskeyAttestationOptions].
|
||||
*/
|
||||
fun sanitize(options: PasskeyAttestationOptions): PasskeyAttestationOptions
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.x8bit.bitwarden.data.credentials.sanitizer
|
||||
|
||||
import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
|
||||
|
||||
/**
|
||||
* Default implementation of [PasskeyAttestationOptionsSanitizer].
|
||||
*/
|
||||
object PasskeyAttestationOptionsSanitizerImpl : PasskeyAttestationOptionsSanitizer {
|
||||
override fun sanitize(options: PasskeyAttestationOptions): PasskeyAttestationOptions {
|
||||
// The AliExpress Android app (com.alibaba.aliexpresshd) incorrectly appends a newline
|
||||
// to the user.id field when creating a passkey. This causes the operation to fail
|
||||
// downstream. As a workaround, we detect this specific scenario, trim the newline, and
|
||||
// re-serialize the JSON request.
|
||||
return if (options.relyingParty.id == ALIEXPRESS_RP_ID &&
|
||||
options.user.id.endsWith("\n")
|
||||
) {
|
||||
options.copy(
|
||||
user = options.user.copy(id = options.user.id.trimEnd('\n')),
|
||||
)
|
||||
} else {
|
||||
options
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ALIEXPRESS_RP_ID = "m.aliexpress.com"
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.network.model.OrganizationEventJson
|
||||
import com.bitwarden.network.model.OrganizationEventType
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.dao.OrganizationEventDao
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk
|
||||
|
||||
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
@@ -13,7 +13,7 @@ import java.time.Instant
|
||||
* Primary access point for general settings-related disk information.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface SettingsDiskSource {
|
||||
interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
|
||||
/**
|
||||
* The currently persisted app language (or `null` if not set).
|
||||
@@ -95,26 +95,11 @@ interface SettingsDiskSource {
|
||||
*/
|
||||
val hasUserLoggedInOrCreatedAccountFlow: Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* The current status of whether the flight recorder is enabled.
|
||||
*/
|
||||
var flightRecorderData: FlightRecorderDataSet?
|
||||
|
||||
/**
|
||||
* Emits updates that track [flightRecorderData].
|
||||
*/
|
||||
val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
|
||||
|
||||
/**
|
||||
* The time at which the browser autofill dialog is allowed to be shown to the user again.
|
||||
*/
|
||||
var browserAutofillDialogReshowTime: Instant?
|
||||
|
||||
/**
|
||||
* The current status of whether the web domain compatibility mode is enabled.
|
||||
*/
|
||||
var isAutofillWebDomainCompatMode: Boolean?
|
||||
|
||||
/**
|
||||
* Clears all the settings data for the given user.
|
||||
*/
|
||||
|
||||
@@ -5,8 +5,8 @@ import androidx.core.content.edit
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.datasource.disk.BaseDiskSource
|
||||
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
|
||||
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
@@ -50,7 +50,6 @@ private const val RESUME_SCREEN = "resumeScreen"
|
||||
private const val FLIGHT_RECORDER_KEY = "flightRecorderData"
|
||||
private const val IS_DYNAMIC_COLORS_ENABLED = "isDynamicColorsEnabled"
|
||||
private const val BROWSER_AUTOFILL_DIALOG_RESHOW_TIME = "browserAutofillDialogReshowTime"
|
||||
private const val AUTOFILL_WEB_DOMAIN_COMPATIBILITY = "autofillWebDomainCompatibility"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -235,12 +234,6 @@ class SettingsDiskSourceImpl(
|
||||
putLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME, value = value?.toEpochMilli())
|
||||
}
|
||||
|
||||
override var isAutofillWebDomainCompatMode: Boolean?
|
||||
get() = getBoolean(key = AUTOFILL_WEB_DOMAIN_COMPATIBILITY)
|
||||
set(value) {
|
||||
putBoolean(key = AUTOFILL_WEB_DOMAIN_COMPATIBILITY, value = value)
|
||||
}
|
||||
|
||||
override fun clearData(userId: String) {
|
||||
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
||||
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.room.Room
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.data.datasource.disk.di.EncryptedPreferences
|
||||
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,7 +32,6 @@ interface BiometricsEncryptionManager {
|
||||
*/
|
||||
fun isBiometricIntegrityValid(
|
||||
userId: String,
|
||||
cipher: Cipher?,
|
||||
): Boolean
|
||||
|
||||
/**
|
||||
|
||||
@@ -90,8 +90,8 @@ class BiometricsEncryptionManagerImpl(
|
||||
return cipher?.takeIf { isCipherInitialized }
|
||||
}
|
||||
|
||||
override fun isBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean =
|
||||
isSystemBiometricIntegrityValid(userId, cipher) && isAccountBiometricIntegrityValid(userId)
|
||||
override fun isBiometricIntegrityValid(userId: String): Boolean =
|
||||
isSystemBiometricIntegrityValid(userId) && isAccountBiometricIntegrityValid(userId)
|
||||
|
||||
override fun isAccountBiometricIntegrityValid(userId: String): Boolean {
|
||||
val systemBioIntegrityState = settingsDiskSource
|
||||
@@ -203,11 +203,13 @@ class BiometricsEncryptionManagerImpl(
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the keystore key and decrypts it using the user-provided [cipher].
|
||||
* Validates the keystore key and decrypts it, if decryption is successful `true` is returned,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
private fun isSystemBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean {
|
||||
private fun isSystemBiometricIntegrityValid(userId: String): Boolean {
|
||||
// Attempt to get the user scoped key. If that fails, we check to see if a legacy key
|
||||
// is available.
|
||||
val cipher = getOrCreateCipher(userId = userId)
|
||||
val secretKey = getSecretKeyOrNull(userId = userId) ?: getSecretKeyOrNull(userId = null)
|
||||
return if (cipher != null && secretKey != null) {
|
||||
cipher.initializeCipher(userId = userId, secretKey = secretKey)
|
||||
|
||||
@@ -7,9 +7,9 @@ import android.security.KeyChainException
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.core.data.repository.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
|
||||
@@ -25,4 +25,10 @@ interface PolicyManager {
|
||||
userId: String,
|
||||
type: PolicyTypeJson,
|
||||
): List<SyncResponseJson.Policy>
|
||||
|
||||
/**
|
||||
* Get the organization id of the personal ownership policy.
|
||||
* If multiple organizations enforce the policy, return the first to set it.
|
||||
*/
|
||||
fun getPersonalOwnershipPolicyOrganizationId(): String?
|
||||
}
|
||||
|
||||
@@ -66,6 +66,13 @@ class PolicyManagerImpl(
|
||||
)
|
||||
.orEmpty()
|
||||
|
||||
override fun getPersonalOwnershipPolicyOrganizationId(): String? =
|
||||
this
|
||||
.getActivePolicies(PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
.sortedBy { it.revisionDate }
|
||||
.firstOrNull()
|
||||
?.organizationId
|
||||
|
||||
/**
|
||||
* A helper method to filter policies.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.data.util.decodeFromStringOrNull
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.network.model.PushTokenRequest
|
||||
import com.bitwarden.network.service.PushService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
@@ -34,6 +34,8 @@ import java.time.Clock
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import javax.inject.Inject
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.toJavaDuration
|
||||
@@ -134,7 +136,6 @@ class PushManagerImpl @Inject constructor(
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun onMessageReceived(notification: BitwardenNotification) {
|
||||
if (authDiskSource.uniqueAppId == notification.contextId) return
|
||||
val userId = activeUserId ?: return
|
||||
Timber.d("Push Notification Received: ${notification.notificationType}")
|
||||
|
||||
when (val type = notification.notificationType) {
|
||||
@@ -179,11 +180,13 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncCipherNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.takeIf { it.cipherId != null && it.revisionDate != null }
|
||||
.takeIf {
|
||||
it.cipherId != null && it.revisionDate != null && isLoggedIn(it.userId)
|
||||
}
|
||||
?.let {
|
||||
mutableSyncCipherUpsertSharedFlow.tryEmit(
|
||||
SyncCipherUpsertData(
|
||||
userId = requireNotNull(it.userId),
|
||||
cipherId = requireNotNull(it.cipherId),
|
||||
revisionDate = requireNotNull(it.revisionDate),
|
||||
organizationId = it.organizationId,
|
||||
@@ -228,11 +231,13 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncFolderNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.takeIf { it.folderId != null && it.revisionDate != null }
|
||||
.takeIf {
|
||||
it.folderId != null && it.revisionDate != null && isLoggedIn(it.userId)
|
||||
}
|
||||
?.let {
|
||||
mutableSyncFolderUpsertSharedFlow.tryEmit(
|
||||
SyncFolderUpsertData(
|
||||
userId = requireNotNull(it.userId),
|
||||
folderId = requireNotNull(it.folderId),
|
||||
revisionDate = requireNotNull(it.revisionDate),
|
||||
isUpdate = type == NotificationType.SYNC_FOLDER_UPDATE,
|
||||
@@ -273,11 +278,13 @@ class PushManagerImpl @Inject constructor(
|
||||
.decodeFromString<NotificationPayload.SyncSendNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { isLoggedIn(userId) && it.userMatchesNotification(userId) }
|
||||
?.takeIf { it.sendId != null && it.revisionDate != null }
|
||||
.takeIf {
|
||||
it.sendId != null && it.revisionDate != null && isLoggedIn(it.userId)
|
||||
}
|
||||
?.let {
|
||||
mutableSyncSendUpsertSharedFlow.tryEmit(
|
||||
SyncSendUpsertData(
|
||||
userId = requireNotNull(it.userId),
|
||||
sendId = requireNotNull(it.sendId),
|
||||
revisionDate = requireNotNull(it.revisionDate),
|
||||
isUpdate = type == NotificationType.SYNC_SEND_UPDATE,
|
||||
@@ -361,11 +368,11 @@ class PushManagerImpl @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
private fun isLoggedIn(
|
||||
userId: String,
|
||||
): Boolean = authDiskSource.getAccountTokens(userId)?.isLoggedIn == true
|
||||
}
|
||||
|
||||
private fun NotificationPayload.userMatchesNotification(userId: String): Boolean {
|
||||
return this.userId != null && this.userId == userId
|
||||
userId: String?,
|
||||
): Boolean {
|
||||
contract { returns(true) implies (userId != null) }
|
||||
return userId?.let { authDiskSource.getAccountTokens(it) }?.isLoggedIn == true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.di
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
|
||||
@@ -3,15 +3,17 @@ package com.x8bit.bitwarden.data.platform.manager.di
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManagerImpl
|
||||
import com.bitwarden.core.data.manager.realtime.RealtimeManager
|
||||
import com.bitwarden.core.data.manager.realtime.RealtimeManagerImpl
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.manager.toast.ToastManagerImpl
|
||||
import com.bitwarden.cxf.registry.CredentialExchangeRegistry
|
||||
import com.bitwarden.cxf.registry.dsl.credentialExchangeRegistry
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.manager.DispatcherManagerImpl
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
|
||||
import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriter
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.service.EventService
|
||||
@@ -63,10 +65,6 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderWriter
|
||||
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderWriterImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
|
||||
@@ -84,7 +82,6 @@ import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.manager.FileManager
|
||||
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
@@ -109,18 +106,6 @@ object PlatformManagerModule {
|
||||
application: Application,
|
||||
): AppStateManager = AppStateManagerImpl(application = application)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFlightRecorderWriter(
|
||||
clock: Clock,
|
||||
fileManager: FileManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): FlightRecorderWriter = FlightRecorderWriterImpl(
|
||||
clock = clock,
|
||||
fileManager = fileManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFlightRecorderManager(
|
||||
@@ -129,11 +114,11 @@ object PlatformManagerModule {
|
||||
dispatcherManager: DispatcherManager,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
flightRecorderWriter: FlightRecorderWriter,
|
||||
): FlightRecorderManager = FlightRecorderManagerImpl(
|
||||
): FlightRecorderManager = FlightRecorderManager.create(
|
||||
context = context,
|
||||
clock = clock,
|
||||
dispatcherManager = dispatcherManager,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
flightRecorderDiskSource = settingsDiskSource,
|
||||
flightRecorderWriter = flightRecorderWriter,
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.platform.manager.event
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.network.model.OrganizationEventJson
|
||||
import com.bitwarden.network.service.EventService
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.flightrecorder
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manager class that handles recording logs for the flight recorder.
|
||||
*/
|
||||
interface FlightRecorderManager {
|
||||
/**
|
||||
* Returns a set of all flight recorder data currently stored on the device.
|
||||
*/
|
||||
val flightRecorderData: FlightRecorderDataSet
|
||||
|
||||
/**
|
||||
* Tracks changes to [FlightRecorderDataSet].
|
||||
*/
|
||||
val flightRecorderDataFlow: StateFlow<FlightRecorderDataSet>
|
||||
|
||||
/**
|
||||
* Dismisses the all flight recorder banners.
|
||||
*/
|
||||
fun dismissFlightRecorderBanner()
|
||||
|
||||
/**
|
||||
* Starts the flight recorder for the given [duration].
|
||||
*/
|
||||
fun startFlightRecorder(duration: FlightRecorderDuration)
|
||||
|
||||
/**
|
||||
* Cancels the active flight recorder if one is currently active.
|
||||
*/
|
||||
fun endFlightRecorder()
|
||||
|
||||
/**
|
||||
* Deletes the raw log file and metadata associated with the [data].
|
||||
*/
|
||||
fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData)
|
||||
|
||||
/**
|
||||
* Deletes the raw log files and metadata.
|
||||
*/
|
||||
fun deleteAllLogs()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.garbage
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user