mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 10:54:26 -05:00
Compare commits
6 Commits
v2026.4.0-
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8a9c596b2 | ||
|
|
e33c4df59c | ||
|
|
7f426f1037 | ||
|
|
9bde261007 | ||
|
|
37b336ee35 | ||
|
|
c9d28941c6 |
@@ -58,22 +58,23 @@ User Request (UI Action)
|
||||
|
||||
### Workflow Skills
|
||||
|
||||
> **Quick start**: Use the `android-architect` agent (or `/plan-android-work <task>`) to refine requirements and plan,
|
||||
> then the `android-implementer` agent (or `/work-on-android <task>`) for implementation,
|
||||
> then `/review-android <PR#>` to review the result.
|
||||
> **Quick start**: Use `/plan-android-work <task>` to refine requirements and plan,
|
||||
> then `/work-on-android <task>` for implementation.
|
||||
|
||||
Planning: 1–2 | Implementation: 3–7 | Review & PR: 8–10
|
||||
**Planning Phase:**
|
||||
|
||||
1. `refining-android-requirements` - Gap analysis and structured spec from any input source
|
||||
2. `planning-android-implementation` - Architecture design and phased task breakdown
|
||||
|
||||
**Implementation Phase:**
|
||||
|
||||
3. `implementing-android-code` - Patterns, gotchas, and templates for writing code
|
||||
4. `testing-android-code` - Test patterns and templates for verifying code
|
||||
5. `build-test-verify` - Build, test, lint, and deploy commands
|
||||
6. `perform-android-preflight-checklist` - Quality gate before committing
|
||||
7. `committing-android-changes` - Commit message format and pre-commit workflow
|
||||
8. `reviewing-changes` - Android-specific MVVM/Compose code review checklists (invoked by `/review-android`)
|
||||
9. `/review-android` - Full review workflow: PR context gathering → Android checklist → output
|
||||
10. `creating-android-pull-request` - PR creation workflow and templates
|
||||
8. `reviewing-changes` - Code review checklists for MVVM/Compose patterns
|
||||
9. `creating-android-pull-request` - PR creation workflow and templates
|
||||
|
||||
---
|
||||
|
||||
@@ -105,6 +106,7 @@ Planning: 1–2 | Implementation: 3–7 | Review & PR: 8–10
|
||||
In addition to the Key Principles above, follow these rules:
|
||||
|
||||
### DO
|
||||
- Use `remember(viewModel)` for lambdas passed to composables
|
||||
- Map async results to internal actions before updating state
|
||||
- Inject `Clock` for time-dependent operations
|
||||
- Return early to reduce nesting
|
||||
@@ -126,7 +128,7 @@ In addition to the Key Principles above, follow these rules:
|
||||
- **Before writing tests**: Use `testing-android-code` skill for test patterns and templates
|
||||
- **Building/testing**: Use `build-test-verify` skill | App tests: `./gradlew app:testStandardDebugUnitTest`
|
||||
- **Before committing**: Use `perform-android-preflight-checklist` skill, then `committing-android-changes` skill for message format
|
||||
- **Code review**: Use `/review-android` for the full review workflow; `reviewing-changes` skill for checklist-only
|
||||
- **Code review**: Use `reviewing-changes` skill for MVVM/Compose review checklists
|
||||
- **Creating PRs**: Use `creating-android-pull-request` skill for PR workflow and templates
|
||||
- **Troubleshooting**: See `docs/TROUBLESHOOTING.md`
|
||||
- **Architecture**: `docs/ARCHITECTURE.md` | [Bitwarden SDK](https://github.com/bitwarden/sdk) | [Jetpack Compose](https://developer.android.com/jetpack/compose) | [Hilt DI](https://dagger.dev/hilt/)
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
---
|
||||
name: android-architect
|
||||
description: "Plans, architects, and refines implementation details for Android features in the Bitwarden Android codebase before any code is written. Use at the START of any new feature, significant change, Jira ticket, or when requirements need clarification and gap analysis. Proactively suggest when the user describes a feature, shares a ticket, or asks to plan Android work. Produces a structured, phased implementation plan ready for the android-implementer agent."
|
||||
model: opus
|
||||
color: green
|
||||
tools: Read, Glob, Grep, Write, Edit, Agent, Skill(refining-android-requirements), Skill(planning-android-implementation), Skill(plan-android-work), mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__get_issue, mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__get_issue_comments, mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__search_issues, mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__search_confluence, mcp__plugin_bitwarden-atlassian-tools_bitwarden-atlassian__get_confluence_page
|
||||
---
|
||||
|
||||
You are the Android Architect — an elite software architect and senior Android engineer with deep mastery of the Bitwarden Android codebase. You operate as a planning and design authority, responsible for transforming vague requirements, tickets, or feature ideas into precise, actionable, phased implementation plans before any code is written.
|
||||
|
||||
Your primary workflow is `Skill(plan-android-work)`, which encompasses two sequential phases:
|
||||
1. **`Skill(refining-android-requirements)`** — Gap analysis, ambiguity resolution, and structured specification
|
||||
2. **`Skill(planning-android-implementation)`** — Architecture design, pattern selection, and phased task breakdown
|
||||
|
||||
---
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
### Phase 1: Requirements Refinement (`Skill(refining-android-requirements)`)
|
||||
|
||||
Before any planning begins, you must fully understand what is being built. You will:
|
||||
|
||||
1. **Parse and Extract Intent**: Identify the core feature request, affected modules (`:app`, `:authenticator`, shared), and user-facing vs. internal scope.
|
||||
|
||||
2. **Identify Gaps**: Actively look for missing information:
|
||||
- Ambiguous acceptance criteria
|
||||
- Undefined edge cases (empty states, error states, loading states, network failure)
|
||||
- Missing security or zero-knowledge implications
|
||||
- Unclear UI/UX behavior
|
||||
- Unspecified API contracts or SDK interactions
|
||||
- Missing test coverage expectations
|
||||
|
||||
3. **Produce Structured Specification**: Output a refined spec with:
|
||||
- Feature summary (1-2 sentences)
|
||||
- Affected modules and components
|
||||
- Functional requirements (numbered list)
|
||||
- Non-functional requirements (performance, security, accessibility)
|
||||
- Open questions that MUST be resolved before implementation (ask the user if needed)
|
||||
- Assumptions being made (document clearly)
|
||||
|
||||
### Phase 2: Implementation Planning (`Skill(planning-android-implementation)`)
|
||||
|
||||
With a refined spec, produce a comprehensive implementation plan:
|
||||
|
||||
1. **Architecture Design**:
|
||||
- Identify which ViewModel(s), Repository(ies), and data sources are involved
|
||||
- Define new interfaces and their `...Impl` counterparts
|
||||
- Map UDF flow: UI Actions → ViewModel → Repository → SDK/Network/Disk → DataState
|
||||
- Identify required State, Action, and Event sealed class members
|
||||
- Note any new Hilt modules or injection changes required
|
||||
|
||||
2. **Pattern Selection**:
|
||||
- Identify existing patterns in the codebase that apply
|
||||
- Flag any cases where a new pattern might be needed (rare — prefer established patterns)
|
||||
- Reference relevant existing files as implementation guides
|
||||
|
||||
3. **Phased Task Breakdown**: Organize work into logical phases:
|
||||
- Phase 1: Data layer (repositories, data sources, models)
|
||||
- Phase 2: Domain/business logic (ViewModel, state management)
|
||||
- Phase 3: UI layer (Compose screens, previews, navigation)
|
||||
- Phase 4: Tests (unit tests per component, integration where needed)
|
||||
- Phase 5: Polish (strings, accessibility, edge cases)
|
||||
|
||||
4. **Dependency and Risk Analysis**:
|
||||
- Identify blocking dependencies between tasks
|
||||
- Flag high-risk areas (security, crypto, SDK interactions)
|
||||
- Note areas requiring special care (e.g., DataState streaming, coroutine context)
|
||||
|
||||
5. **File Manifest**: List all files to be created or modified with brief descriptions.
|
||||
|
||||
---
|
||||
|
||||
## Bitwarden Android Expertise
|
||||
|
||||
You have deep knowledge of this codebase and must apply it in every plan:
|
||||
|
||||
### Architecture Constraints
|
||||
- **No exceptions from data layer**: All suspending functions must return `Result<T>` or sealed classes
|
||||
- **State hoisting**: All behavior-affecting state lives in ViewModel's state — never in composables
|
||||
- **Interface-based DI**: Every implementation has an interface counterpart with Hilt injection
|
||||
- **UDF strictly enforced**: State flows down, actions flow up — no bidirectional data flow
|
||||
- **Internal actions for coroutines**: Never update state directly inside `launch` blocks; map results to `Internal` actions first
|
||||
|
||||
### Zero-Knowledge Security Rules (NON-NEGOTIABLE)
|
||||
- Never transmit unencrypted vault data or master passwords to the server
|
||||
- All encryption via Bitwarden SDK — never implement custom crypto
|
||||
- Use scoped SDK sources (`ScopedVaultSdkSource`) to prevent cross-user context leakage
|
||||
- On logout, all sensitive data cleared via `UserLogoutManager.logout()`
|
||||
- Store sensitive data only via Android Keystore or SDK-encrypted storage
|
||||
|
||||
### Code Style Requirements
|
||||
- 100-character line limit
|
||||
- `camelCase` for vars/functions, `PascalCase` for classes, `SCREAMING_SNAKE_CASE` for constants
|
||||
- `...Impl` suffix for all implementations
|
||||
- KDoc required for all public APIs
|
||||
- Test constants at bottom of file — NO companion objects in tests
|
||||
- String resources in `:ui` module (`ui/src/main/res/values/strings.xml`) using typographic quotes
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
Your output must always be a structured planning document with these sections:
|
||||
|
||||
```
|
||||
# Implementation Plan: [Feature Name]
|
||||
|
||||
## Refined Requirements
|
||||
### Summary
|
||||
### Functional Requirements
|
||||
### Non-Functional Requirements
|
||||
### Assumptions
|
||||
### Open Questions (if any — request answers from user before proceeding)
|
||||
|
||||
## Architecture Design
|
||||
### Affected Components
|
||||
### New Interfaces & Implementations
|
||||
### UDF Flow Diagram (text-based)
|
||||
### State / Action / Event Definitions
|
||||
|
||||
## Phased Implementation Plan
|
||||
### Phase 1: [Name] — [Estimated scope]
|
||||
- Task 1.1: ...
|
||||
- Task 1.2: ...
|
||||
### Phase 2: ...
|
||||
...
|
||||
|
||||
## File Manifest
|
||||
### New Files
|
||||
### Modified Files
|
||||
|
||||
## Risk & Dependency Notes
|
||||
|
||||
## Handoff Notes for Implementer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Behavioral Guidelines
|
||||
|
||||
### DO
|
||||
- Explore the codebase (via sub-agents) to understand existing patterns before designing — never assume file locations or implementations
|
||||
- Ask clarifying questions BEFORE producing a plan if critical information is missing
|
||||
- Reference specific existing files and patterns as implementation guides in your plan
|
||||
- Apply security considerations proactively — flag any zero-knowledge implications
|
||||
- Produce plans detailed enough that an implementer needs no additional context
|
||||
- Note when existing patterns should be reused vs. when genuinely new patterns are warranted
|
||||
|
||||
### DON'T
|
||||
- Write implementation code — your job ends where the implementer's begins
|
||||
- Assume requirements are complete — always perform gap analysis
|
||||
- Invent new architectural patterns when established ones exist
|
||||
- Ignore security implications of any feature touching vault data, credentials, or keys
|
||||
- Produce vague tasks — every task must be concrete and actionable
|
||||
- Skip the requirements refinement phase even for seemingly simple requests
|
||||
|
||||
### Codebase Exploration Protocol
|
||||
Before designing any architecture, deploy exploration sub-agents to:
|
||||
- Locate relevant existing ViewModels, Repositories, and data sources
|
||||
- Understand current patterns for similar features
|
||||
- Identify reusable components and shared infrastructure
|
||||
- Check for existing test patterns to replicate
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
name: android-implementer
|
||||
description: "Autonomously implements features, fixes bugs, and completes development tasks on the Bitwarden Android project. Drives the full /work-on-android lifecycle (implement, test, build, preflight, commit) with self-review at each phase. Use when the user wants end-to-end implementation without manual phase approvals. Proactively suggest after /plan-android-work completes or when planning output is ready for implementation."
|
||||
model: opus
|
||||
color: green
|
||||
tools: Bash, Read, Edit, Write, Glob, Grep, LSP, Agent, Skill(implementing-android-code), Skill(testing-android-code), Skill(build-test-verify), Skill(perform-android-preflight-checklist), Skill(committing-android-changes), Skill(work-on-android)
|
||||
---
|
||||
|
||||
You are an elite Android implementation engineer specialized in the Bitwarden Android codebase. Your role is to autonomously drive implementation from start to finish, acting as both the implementer and the quality reviewer at each phase.
|
||||
|
||||
## First Action: Invoke `/work-on-android`
|
||||
|
||||
**Immediately invoke the `work-on-android` skill using the Skill tool.** This is your primary workflow — it defines the phases, invokes the correct sub-skills, and structures the entire implementation lifecycle. Do not manually orchestrate individual skills; let `/work-on-android` drive the phase sequence.
|
||||
|
||||
Your added value on top of `/work-on-android` is autonomy: where the skill asks for user confirmation between phases, you provide that confirmation yourself by applying the self-review protocol below. Do not wait for human approval between phases — evaluate your own output, refine if necessary, and advance.
|
||||
|
||||
## Self-Review Protocol
|
||||
|
||||
At each phase transition where `/work-on-android` would normally ask the user to confirm, apply this review instead:
|
||||
|
||||
```
|
||||
--- Phase Review: [Phase Name] ---
|
||||
Status: APPROVED / NEEDS REFINEMENT
|
||||
Findings: [brief assessment]
|
||||
Action: [Proceeding to next phase / Iterating on: X]
|
||||
---
|
||||
```
|
||||
|
||||
If status is NEEDS REFINEMENT, iterate up to 3 times before proceeding with the best available output and noting remaining concerns.
|
||||
|
||||
**Review criteria by phase:**
|
||||
- **Implementation**: Follows skill guidance and CLAUDE.md anti-patterns list?
|
||||
- **Testing**: Covers happy path, error cases, and edge cases?
|
||||
- **Build & Verify**: All tests pass? No compilation errors or warnings?
|
||||
- **Preflight**: Would this pass code review by a senior engineer?
|
||||
- **Commit**: Message clear, properly formatted, and accurate?
|
||||
|
||||
## Decision-Making Framework
|
||||
|
||||
- **When uncertain about a pattern**: Search the codebase for existing examples. Follow what exists rather than inventing.
|
||||
- **When finding multiple valid approaches**: Choose the one most consistent with nearby code in the same module.
|
||||
- **When discovering scope creep**: Note it as a follow-up item and stay focused on the original task.
|
||||
- **When tests fail**: Diagnose the root cause, fix it, and re-run. Don't skip failing tests.
|
||||
- **When a phase produces subpar output**: Iterate. Don't advance with known deficiencies unless you've exhausted reasonable refinement attempts.
|
||||
|
||||
## Communication Style
|
||||
|
||||
- Be concise and direct in phase transition summaries
|
||||
- Provide detailed technical reasoning only when making non-obvious decisions
|
||||
- Flag any genuine blockers that require human input clearly and specifically
|
||||
- At completion, provide a summary of what was implemented, what was tested, and any follow-up items
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Minimize user interruptions**: Only escalate for genuine ambiguities that codebase context cannot resolve.
|
||||
2. **Never skip testing**: Every implementation phase must have corresponding tests.
|
||||
3. **Never invent new patterns**: Use established codebase patterns. Search for examples first.
|
||||
4. **Never leave the codebase in a broken state**: If you can't complete a phase cleanly, revert and explain why.
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
description: Guided Android code review workflow through context gathering, Android-specific review, and output
|
||||
argument-hint: [PR# | PR URL | "local"]
|
||||
---
|
||||
|
||||
# Android Code Review Workflow
|
||||
|
||||
You are guiding the developer through a comprehensive Android code review for the Bitwarden Android project.
|
||||
|
||||
**Input**: $ARGUMENTS
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Jira/Confluence access**: The `bitwarden-atlassian-tools@bitwarden-marketplace` MCP plugin is required to fetch linked Jira tickets. If unavailable, skip ticket context.
|
||||
- **GitHub CLI**: Required for fetching PR metadata. Verify with `gh auth status`.
|
||||
|
||||
## Workflow Phases
|
||||
|
||||
Work through each phase sequentially. **Confirm with the user before advancing to the next phase.** The user may skip phases that are not applicable.
|
||||
|
||||
### Phase 1: Ingest
|
||||
|
||||
Parse the input to determine review context:
|
||||
|
||||
**Source Detection Rules:**
|
||||
- **PR number** (`123`, `PR #123`, `https://github.com/.../pull/123`): Extract the numeric ID. Fetch PR metadata via `gh pr view <N> --json title,body,headRefName,baseRefName,author,files`. Fetch existing review threads to avoid duplicate comments via `gh api graphql` with `reviewThreads(first: 100)`.
|
||||
- **"local"** or no argument: Review current branch changes via `git diff main...HEAD` and `git log main...HEAD --oneline --no-merges`.
|
||||
- **No input**: Ask the user whether to review a PR (provide number/URL) or local branch changes.
|
||||
|
||||
**Additional context:**
|
||||
- Detect Jira ticket references in PR title/body (patterns like `PM-\d+`, `BWA-\d+`). Fetch via `get_issue` if the MCP plugin is available.
|
||||
- Summarize what was fetched rather than dumping raw content.
|
||||
|
||||
**Present a structured summary:**
|
||||
1. What is being reviewed (PR title/number, branch, or local changes description)
|
||||
2. Jira ticket context if found (summary and acceptance criteria)
|
||||
3. Files changed (count and modules affected)
|
||||
4. Existing review thread count (PR reviews only — avoids duplicate comments)
|
||||
|
||||
**Gate**: User confirms the summary is complete before proceeding.
|
||||
|
||||
### Phase 2: Review
|
||||
|
||||
Invoke the `reviewing-changes` skill and use it to perform the Android-specific code review. Use the PR context from Phase 1 (change type, files affected, modules, Jira requirements) to inform the skill's change type detection and checklist selection.
|
||||
|
||||
The skill will:
|
||||
1. Detect the change type based on files and PR context from Phase 1
|
||||
2. Load the appropriate type-specific checklist
|
||||
3. Execute the multi-pass review strategy
|
||||
4. Consult reference materials as needed
|
||||
|
||||
**Before advancing**: Share a summary of key findings (critical issues if any, overall assessment) and confirm the user is ready to output the review.
|
||||
|
||||
### Phase 3: Output
|
||||
|
||||
Write the completed review to local files:
|
||||
|
||||
- `review-summary.md` — Overall assessment (APPROVE / REQUEST CHANGES) plus critical issues list
|
||||
- `review-inline-comments.md` — All inline findings with `<details>` tags
|
||||
|
||||
Follow the exact output format from `.claude/skills/reviewing-changes/examples/review-outputs.md`.
|
||||
|
||||
For PR reviews: offer to post the review to GitHub using `gh pr review <N> --comment -b "$(cat review-summary.md)"` for the summary. For inline comments, use the GitHub API or the `bitwarden-code-review` plugin if installed.
|
||||
|
||||
**Before advancing**: Confirm the files were written successfully and ask if the user wants to post to GitHub (PR reviews only).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Be explicit about which phase you are in at all times.
|
||||
- Never proceed to another phase without user confirmation.
|
||||
- If the user wants to skip a phase, acknowledge and move to the next applicable phase.
|
||||
- If starting from a partially completed review (e.g., review already written), skip to the appropriate phase.
|
||||
@@ -15,19 +15,19 @@ Work through each phase sequentially. **Confirm with the user before advancing t
|
||||
|
||||
### Phase 1: Implement
|
||||
|
||||
Invoke `Skill(implementing-android-code)` to guide your implementation of the task. Understand what needs to be done, explore the relevant code, and write the implementation.
|
||||
Invoke the `implementing-android-code` skill and use it to guide your implementation of the task. Understand what needs to be done, explore the relevant code, and write the implementation.
|
||||
|
||||
**Before advancing**: Summarize what was implemented and confirm the user is ready to move to testing.
|
||||
|
||||
### Phase 2: Test
|
||||
|
||||
Invoke `Skill(testing-android-code)` to write tests for the changes made in Phase 1. Follow the project's test patterns and conventions.
|
||||
Invoke the `testing-android-code` skill and use it to write tests for the changes made in Phase 1. Follow the project's test patterns and conventions.
|
||||
|
||||
**Before advancing**: Summarize what tests were written and confirm readiness for build verification.
|
||||
|
||||
### Phase 3: Build & Verify
|
||||
|
||||
Invoke `Skill(build-test-verify)` to run tests, lint, and detekt. Ensure everything passes.
|
||||
Invoke the `build-test-verify` skill to run tests, lint, and detekt. Ensure everything passes.
|
||||
|
||||
**If failures occur**: Fix the issues and re-run verification. Do not advance until all checks pass.
|
||||
|
||||
@@ -35,13 +35,13 @@ Invoke `Skill(build-test-verify)` to run tests, lint, and detekt. Ensure everyth
|
||||
|
||||
### Phase 4: Self-Review
|
||||
|
||||
Invoke `Skill(perform-android-preflight-checklist)` to perform a quality gate check on all changes. Address any issues found.
|
||||
Invoke the `perform-android-preflight-checklist` skill to perform a quality gate check on all changes. Address any issues found.
|
||||
|
||||
**Before advancing**: Share the self-review results and confirm readiness to commit.
|
||||
|
||||
### Phase 5: Commit
|
||||
|
||||
Invoke `Skill(committing-android-changes)` to stage and commit the changes with a properly formatted commit message.
|
||||
Invoke the `committing-android-changes` skill to stage and commit the changes with a properly formatted commit message.
|
||||
|
||||
**Before advancing**: Confirm the commit was successful and ask if the user wants to proceed to review and PR creation, or stop here.
|
||||
|
||||
@@ -56,7 +56,7 @@ Launch a subagent with the `/bitwarden-code-review:code-review-local` command to
|
||||
|
||||
### Phase 7: Pull Request
|
||||
|
||||
Prompt the user to invoke `Skill(creating-android-pull-request)` to create the pull request with proper description and formatting. **Create as a draft PR by default** unless the user has explicitly requested a ready-for-review PR.
|
||||
Prompt the user to invoke the `creating-android-pull-request` skill to create the pull request with proper description and formatting. **Create as a draft PR by default** unless the user has explicitly requested a ready-for-review PR.
|
||||
|
||||
## Guidelines
|
||||
|
||||
|
||||
@@ -39,14 +39,9 @@ If builds fail resolving the Bitwarden SDK, verify `GITHUB_TOKEN` in `user.prope
|
||||
|
||||
**IMPORTANT**: The app module uses the `standard` flavor. Always use `testStandardDebugUnitTest`, NOT `testDebugUnitTest`.
|
||||
|
||||
**IMPORTANT**: Always pipe test output through a filter that captures failures on the first run. Gradle suppresses detailed failure output by default, so use `2>&1 | grep -E "FAILED|BUILD|expected:|actual:|AssertionError|failures" | head -30` to see pass/fail results and assertion details without needing a second run.
|
||||
|
||||
```bash
|
||||
# App module tests (correct flavor!)
|
||||
./gradlew app:testStandardDebugUnitTest 2>&1 | grep -E "FAILED|BUILD|expected:|actual:|AssertionError|failures" | head -30
|
||||
|
||||
# Run specific test classes
|
||||
./gradlew app:testStandardDebugUnitTest --tests "com.x8bit.bitwarden.SomeTest" 2>&1 | grep -E "FAILED|BUILD|expected:|actual:|AssertionError|failures" | head -30
|
||||
./gradlew app:testStandardDebugUnitTest
|
||||
|
||||
# Run all unit tests across all modules
|
||||
./gradlew test
|
||||
@@ -61,17 +56,6 @@ If builds fail resolving the Bitwarden SDK, verify `GITHUB_TOKEN` in `user.prope
|
||||
./gradlew authenticator:testStandardDebugUnitTest
|
||||
```
|
||||
|
||||
### Reading Test Reports
|
||||
|
||||
If you need full failure details beyond what grep captures, check the HTML test report:
|
||||
|
||||
```bash
|
||||
# After a test run, open the report at:
|
||||
# app/build/reports/tests/testStandardDebugUnitTest/index.html
|
||||
# Or read individual failure XML:
|
||||
find app/build/test-results -name "*.xml" -exec grep -l "failure" {} \;
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
```
|
||||
@@ -94,16 +78,9 @@ ui/src/testFixtures/ # UI test utilities (BaseViewModelTest, BaseCom
|
||||
|
||||
## Lint & Static Analysis
|
||||
|
||||
**IMPORTANT**: Prefer running detekt on modified files only — a full project scan is slow and unnecessary during development. The project supports a `-Pprecommit=true` flag that limits detekt to staged files.
|
||||
|
||||
**IMPORTANT**: Always pipe detekt output through a filter to capture errors on the first run. Detekt prints violation details to stderr/stdout but Gradle can obscure them. Use the grep pattern below to see violations immediately.
|
||||
|
||||
```bash
|
||||
# Detekt on staged files only (preferred during development)
|
||||
git add -u && ./gradlew -Pprecommit=true detekt 2>&1 | grep -E "FAILED|BUILD|Line |Rule |Signature|detekt" | head -40
|
||||
|
||||
# Detekt on all files (full scan, use sparingly)
|
||||
./gradlew detekt 2>&1 | grep -E "FAILED|BUILD|Line |Rule |Signature|detekt" | head -40
|
||||
# Detekt (static analysis)
|
||||
./gradlew detekt
|
||||
|
||||
# Android Lint
|
||||
./gradlew lint
|
||||
@@ -112,10 +89,6 @@ git add -u && ./gradlew -Pprecommit=true detekt 2>&1 | grep -E "FAILED|BUILD|Lin
|
||||
./fastlane check
|
||||
```
|
||||
|
||||
### How `-Pprecommit=true` Works
|
||||
|
||||
The root `build.gradle.kts` configures detekt tasks to use `git diff --name-only --cached` when this property is set, limiting analysis to staged files only. This is the same mechanism used by the project's pre-commit hook. Stage your changes with `git add` before running.
|
||||
|
||||
---
|
||||
|
||||
## Codebase Discovery
|
||||
|
||||
@@ -58,21 +58,6 @@ gh pr create --draft --title "[PM-XXXXX] feat: Short summary" --body "<fill in f
|
||||
|
||||
---
|
||||
|
||||
## AI Review Label
|
||||
|
||||
Before running `gh pr create`, **always** use the `AskUserQuestion` tool to ask whether to add an AI review label:
|
||||
|
||||
- **Question**: "Would you like to add an AI review label to this PR?"
|
||||
- **Options**: `ai-review-vnext`, `ai-review`, `No label`
|
||||
|
||||
If the user selects a label, include it via the `--label` flag:
|
||||
|
||||
```bash
|
||||
gh pr create --draft --label "ai-review-vnext" --title "..." --body "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Base Branch
|
||||
|
||||
- Default target: `main`
|
||||
|
||||
@@ -148,7 +148,9 @@ fun ExampleScreen(
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(R.string.title),
|
||||
navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_back),
|
||||
onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExampleAction.BackClick) }
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
@@ -163,6 +165,7 @@ fun ExampleScreen(
|
||||
- ✅ Use `hiltViewModel()` for dependency injection
|
||||
- ✅ Use `collectAsStateWithLifecycle()` for state (not `collectAsState()`)
|
||||
- ✅ Use `EventsEffect(viewModel)` for one-shot events
|
||||
- ✅ Use `remember(viewModel) { }` for stable callbacks to prevent recomposition
|
||||
- ✅ Use `Bitwarden*` prefixed components from `:ui` module
|
||||
|
||||
**State Hoisting Rules:**
|
||||
|
||||
@@ -342,7 +342,9 @@ fun ExampleScreen(
|
||||
// Dialogs
|
||||
ExampleDialogs(
|
||||
dialogState = state.dialogState,
|
||||
onDismissRequest = { viewModel.trySendAction(ExampleAction.ErrorDialogDismiss) },
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExampleAction.ErrorDialogDismiss) }
|
||||
},
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
@@ -355,14 +357,20 @@ fun ExampleScreen(
|
||||
title = stringResource(id = BitwardenString.example),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
|
||||
onNavigationIconClick = { viewModel.trySendAction(ExampleAction.BackClick) },
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExampleAction.BackClick) }
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
ExampleScreenContent(
|
||||
state = state,
|
||||
onInputChanged = { viewModel.trySendAction(ExampleAction.InputChanged(it)) },
|
||||
onSubmitClick = { viewModel.trySendAction(ExampleAction.SubmitClick) },
|
||||
onInputChanged = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExampleAction.InputChanged(it)) }
|
||||
},
|
||||
onSubmitClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ExampleAction.SubmitClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: reviewing-changes
|
||||
description: Android-specific code review checklist and MVVM/Compose pattern validation for Bitwarden Android — use this for any review task, even if the user doesn't explicitly ask for a "checklist". Detects change type automatically and loads the right review strategy (feature additions, bug fixes, UI refinements, refactoring, dependency updates, infrastructure). Triggered by "review PR", "review changes", "review this code", "check this code", "Android review", code review requests on Kotlin/ViewModel/Composable/Repository/Gradle files, or any time someone asks to look at a diff, PR, or code changes in bitwarden/android.
|
||||
version: 3.0.0
|
||||
description: Guides Android code reviews with type-specific checklists and MVVM/Compose pattern validation. Use when reviewing Android PRs, pull requests, diffs, or local changes involving Kotlin, ViewModel, Composable, Repository, or Gradle files. Triggered by "review PR", "review changes", "check this code", "Android review", or code review requests mentioning bitwarden/android. Loads specialized checklists for feature additions, bug fixes, UI refinements, refactoring, dependency updates, and infrastructure changes.
|
||||
---
|
||||
|
||||
# Reviewing Changes - Android Additions
|
||||
@@ -9,10 +10,16 @@ This skill provides Android-specific workflow additions that complement the base
|
||||
|
||||
## Instructions
|
||||
|
||||
**IMPORTANT**: Work systematically through each step before providing feedback. Each checklist file includes structured thinking guidance for its review passes.
|
||||
**IMPORTANT**: Use structured thinking throughout your review process. Plan your analysis in `<thinking>` tags before providing final feedback.
|
||||
|
||||
### Step 1: Retrieve Additional Details
|
||||
|
||||
<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>
|
||||
|
||||
Retrieve any additional information linked to the pull request using available tools (JIRA MCP, GitHub API).
|
||||
|
||||
If pull request title and message do not provide enough context, request additional details from the reviewer:
|
||||
@@ -21,12 +28,16 @@ If pull request title and message do not provide enough context, request additio
|
||||
- Link to another pull request
|
||||
- Add more detail to the PR title or body
|
||||
|
||||
**Android metadata checks** — flag as ❓ if any of these are missing:
|
||||
- PR includes `*Screen.kt` or Composable changes but has no screenshots
|
||||
- PR adds new `ViewModel` or `Repository` but has no test plan or test file changes
|
||||
|
||||
### Step 2: Detect Change Type with Android Refinements
|
||||
|
||||
<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>
|
||||
|
||||
Use the base change type detection from the agent, with Android-specific refinements:
|
||||
|
||||
**Android-specific patterns:**
|
||||
@@ -54,13 +65,21 @@ The checklist provides:
|
||||
|
||||
### Step 4: Execute Review Following Checklist
|
||||
|
||||
<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>
|
||||
|
||||
Follow the checklist's multi-pass strategy, thinking through each pass systematically.
|
||||
|
||||
### Step 5: Consult Android Reference Materials As Needed
|
||||
|
||||
Load reference files only when needed for specific questions:
|
||||
|
||||
- **Re-reviews** → invoke `reviewing-incremental-changes` agent skill; scope to changed lines only, do not flag new issues in unchanged code
|
||||
- **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)
|
||||
@@ -72,7 +91,6 @@ Load reference files only when needed for specific questions:
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Priority order**: Security → Correctness → Breaking Changes → Performance → Maintainability
|
||||
- **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
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -20,6 +29,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -83,7 +101,16 @@ Use `reference/priority-framework.md` to classify findings as Critical/Important
|
||||
|
||||
## Output Format
|
||||
|
||||
See `examples/review-outputs.md` for the required output format and inline comment structure.
|
||||
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
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -16,6 +25,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -74,7 +92,16 @@ Use `reference/priority-framework.md` to classify findings as Critical/Important
|
||||
|
||||
## Output Format
|
||||
|
||||
See `examples/review-outputs.md` for the required output format and inline comment structure.
|
||||
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
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
### 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
|
||||
@@ -21,6 +30,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -42,6 +60,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -59,13 +86,144 @@
|
||||
|
||||
## Architecture Review
|
||||
|
||||
Read `reference/architectural-patterns.md` for full patterns and code examples.
|
||||
### MVVM Pattern Compliance
|
||||
|
||||
**Check these four areas:**
|
||||
- **MVVM/UDF**: ViewModel exposes `StateFlow` (not `MutableStateFlow`), business logic in Repository, UI is stateless
|
||||
- **Hilt DI**: `@HiltViewModel` + `@Inject constructor`, inject interfaces not implementations, no manual instantiation
|
||||
- **Module placement**: UI in `:ui`/`:app`, data in `:data`, network in `:network`, no circular dependencies
|
||||
- **Error handling**: `Result<T>` / `runCatching` throughout — no thrown exceptions from data layer
|
||||
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
|
||||
|
||||
@@ -208,4 +366,15 @@ Use `reference/review-psychology.md` for phrasing guidance.
|
||||
|
||||
## Output Format
|
||||
|
||||
See `examples/review-outputs.md` for the required output format and inline comment structure.
|
||||
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.
|
||||
```
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -21,6 +30,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -171,7 +189,16 @@ Use `reference/priority-framework.md` to classify findings as Critical/Important
|
||||
|
||||
## Output Format
|
||||
|
||||
See `examples/review-outputs.md` for the required output format and inline comment structure.
|
||||
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
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -21,6 +30,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -151,7 +169,16 @@ Use `reference/priority-framework.md` to classify findings as Critical/Important
|
||||
|
||||
## Output Format
|
||||
|
||||
See `examples/review-outputs.md` for the required output format and inline comment structure.
|
||||
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
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -16,6 +25,15 @@
|
||||
|
||||
### 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?
|
||||
@@ -169,7 +187,16 @@ Use `reference/priority-framework.md` to classify findings as Critical/Important
|
||||
|
||||
## Output Format
|
||||
|
||||
See `examples/review-outputs.md` for the required output format and inline comment structure.
|
||||
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
|
||||
|
||||
|
||||
@@ -50,34 +50,21 @@ Reference: [docs link if applicable]
|
||||
- ⚠️ **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)
|
||||
- 💭 **QUESTION** - Seeking clarification (requirements, design decisions)
|
||||
|
||||
### Summary Comment Format
|
||||
|
||||
Uses the agent's `posting-review-summary` skill format. Surface ❌ CRITICAL issues at the top level for immediate visibility, wrap the full findings list in `<details>` for scannability.
|
||||
|
||||
**Required format for ALL PRs:**
|
||||
```
|
||||
**Overall Assessment:** APPROVE / REQUEST CHANGES
|
||||
|
||||
[1-2 neutral sentences describing what was reviewed]
|
||||
|
||||
**Critical Issues** (if any):
|
||||
- ❌ [One-line summary with file:line]
|
||||
- [issue with file:line]
|
||||
|
||||
<details>
|
||||
<summary>All findings</summary>
|
||||
|
||||
- ❌ **CRITICAL**: [description] (`file:line`)
|
||||
- ⚠️ **IMPORTANT**: [description] (`file:line`)
|
||||
- ♻️ **DEBT**: [description] (`file:line`)
|
||||
- 🎨 **SUGGESTED**: [description] (`file:line`)
|
||||
- ❓ **QUESTION**: [description] (`file:line`)
|
||||
</details>
|
||||
See inline comments for details.
|
||||
```
|
||||
|
||||
For clean PRs with no findings, omit both sections entirely — verdict + 1-2 sentences is sufficient.
|
||||
|
||||
**GitHub pitfall**: Never use `#` followed by a number in comment text (e.g., `#42`, `#PR123`). GitHub autolinks these to issues/PRs. Use `Finding 1:` or `item 42` instead.
|
||||
All PRs use the same minimal format - no exceptions for size or complexity. Summary must be 5-10 lines maximum.
|
||||
|
||||
---
|
||||
|
||||
@@ -281,7 +268,7 @@ Would add security layer against brute force. Consider discussing threat model w
|
||||
|
||||
**Inline Comment 5** (on `app/vault/unlock/UnlockScreen.kt:134`):
|
||||
```markdown
|
||||
❓ **QUESTION**: Can we use BitwardenTextField?
|
||||
💭 **QUESTION**: Can we use BitwardenTextField?
|
||||
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ Use this framework to classify findings during code review. Clear prioritization
|
||||
- [⚠️ 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)
|
||||
- [💭 QUESTION (Seeking Clarification)](#question-seeking-clarification)
|
||||
- [Optional (Acknowledge But Don't Require)](#optional-acknowledge-but-dont-require)
|
||||
|
||||
**Guidelines:**
|
||||
@@ -170,12 +170,13 @@ Will require rework when experimentation framework launches.
|
||||
|
||||
## 🎨 **SUGGESTED** (Nice to Have)
|
||||
|
||||
Improvements with measurable value only. A finding qualifies as SUGGESTED if it provides: security gain, cyclomatic complexity reduction, bug class prevention, or elimination of an O(n²) pattern. Subjective style preferences, vague simplifications, and naming nitpicks do not qualify — leave those out entirely or raise in conversation.
|
||||
These are improvement opportunities but not required. Consider the effort vs. benefit before requesting changes.
|
||||
|
||||
### Code Quality
|
||||
- Extractable duplicated logic that reduces measurable complexity or improves testability
|
||||
- Patterns that would prevent a recurring bug class in this module
|
||||
- Architecture improvements that eliminate tight coupling with measurable impact
|
||||
- Minor style inconsistencies (if not caught by linter)
|
||||
- Opportunities for DRY improvements
|
||||
- Better variable naming for clarity
|
||||
- Simplification opportunities
|
||||
|
||||
**Example**:
|
||||
```
|
||||
@@ -207,7 +208,7 @@ Could be extracted to separate validator class for reusability and testing.
|
||||
|
||||
---
|
||||
|
||||
## ❓ **QUESTION** (Seeking Clarification)
|
||||
## 💭 **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.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Effective code review feedback is clear, actionable, and constructive. This guid
|
||||
## Table of Contents
|
||||
|
||||
**Guidelines:**
|
||||
- [Core Directives](#core-directives)
|
||||
- [Phrasing Templates](#phrasing-templates)
|
||||
- [Critical Issues (Prescriptive)](#critical-issues-prescriptive)
|
||||
- [Suggested Improvements (Exploratory)](#suggested-improvements-exploratory)
|
||||
@@ -15,6 +16,17 @@ Effective code review feedback is clear, actionable, and constructive. This guid
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -245,8 +245,6 @@ fun `test exception`() {
|
||||
|
||||
Common testing mistakes in Bitwarden. **For complete details and examples:** See `references/critical-gotchas.md`
|
||||
|
||||
> **⛔ STOP — `@Suppress("MaxLineLength")`**: Do NOT add this annotation unless the `fun` declaration line **actually exceeds 100 characters**. Count the characters first. Do not copy it from nearby tests. Detekt will tell you if it's needed — when in doubt, leave it off.
|
||||
|
||||
**Core Patterns:**
|
||||
- **assertCoroutineThrows + runTest** - Never wrap in `runTest`; call directly
|
||||
- **Static mock cleanup** - Always `unmockkStatic()` in `@After`
|
||||
@@ -284,10 +282,6 @@ module/src/testFixtures/kotlin/com/bitwarden/.../
|
||||
└── model/*Util.kt
|
||||
```
|
||||
|
||||
### Test Constants Placement
|
||||
|
||||
Declare test constants as top-level `private const val` at the **bottom** of the file, after the class closing brace. Do NOT use `companion object` for test constants.
|
||||
|
||||
### Test Naming
|
||||
|
||||
- Classes: `*Test.kt`, `*ScreenTest.kt`, `*ViewModelTest.kt`
|
||||
|
||||
150
.github/scripts/gh_release_update_issues.py
vendored
150
.github/scripts/gh_release_update_issues.py
vendored
@@ -1,150 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Requires Python 3.9+
|
||||
"""
|
||||
Comment GitHub issues linked to Pull Requests mentioned in a given release.
|
||||
|
||||
Usage:
|
||||
python gh_release_update_issues.py <release_url> [--dry-run]
|
||||
|
||||
Arguments:
|
||||
release-url: The URL of the release to comment on
|
||||
--dry-run: Run without actually updating issues
|
||||
|
||||
Examples:
|
||||
python gh_release_update_issues.py https://github.com/owner/repo/releases/tag/v1.0.0
|
||||
python gh_release_update_issues.py https://github.com/owner/repo/releases/tag/v1.0.0 --dry-run
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import json
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
from typing import List, Tuple, Dict
|
||||
|
||||
def parse_release_url(release_url: str) -> Tuple[str, str, str]:
|
||||
"""Extract owner, repo name, and tag from a GitHub release URL.
|
||||
|
||||
Returns:
|
||||
Tuple of (owner, repo_name, release_tag)
|
||||
"""
|
||||
match = re.search(r'github\.com/([\w-]+)/([\w.-]+)/releases/tag/(.+)$', release_url)
|
||||
if not match:
|
||||
raise ValueError(f"Cannot parse release URL: {release_url}")
|
||||
return match.group(1), match.group(2), match.group(3)
|
||||
|
||||
def extract_pr_numbers(release_notes: str) -> List[int]:
|
||||
return [int(n) for n in re.findall(r'/pull/(\d+)', release_notes)]
|
||||
|
||||
def build_issue_comment(repo: str, release_name: str, release_link: str, pr_numbers: List[int]) -> str:
|
||||
if len(pr_numbers) == 0:
|
||||
return ""
|
||||
|
||||
pr_links = [f"* https://github.com/{repo}/pull/{pr_number}" for pr_number in pr_numbers]
|
||||
|
||||
return f":shipit: Pull Request(s) linked to this issue released in [{release_name}]({release_link}):\n\n"+ "\n".join(pr_links)
|
||||
|
||||
def gh_fetch_release(repo: str, release_tag: str) -> Tuple[str, str]:
|
||||
result = subprocess.run(
|
||||
['gh', 'release', 'view', release_tag, '--repo', repo, '--json', 'name,body'],
|
||||
capture_output=True, text=True, check=True
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
return data['name'], data['body']
|
||||
|
||||
def gh_comment_issue(repo: str, issue_number: int, comment: str) -> None:
|
||||
"""Use GitHub CLI to comment on an issue.
|
||||
"""
|
||||
subprocess.run([
|
||||
'gh', 'issue', 'comment', str(issue_number), '--body', comment, '--repo', repo
|
||||
], check=True)
|
||||
|
||||
def gh_fetch_linked_issues_batched(owner: str, repo_name: str, pr_numbers: List[int]) -> Dict[int, List[int]]:
|
||||
"""Batch-fetch linked issues for all PRs in a single GraphQL call.
|
||||
|
||||
Returns:
|
||||
Dict mapping each PR number to its list of linked issue numbers.
|
||||
"""
|
||||
if not pr_numbers:
|
||||
return {}
|
||||
|
||||
tmpl = 'pr_%d: pullRequest(number: %d) { closingIssuesReferences(first: 100) { nodes { number } } }'
|
||||
pr_fragments = "\n".join(tmpl % (pr, pr) for pr in pr_numbers)
|
||||
query = """
|
||||
query ($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
%s
|
||||
}
|
||||
}
|
||||
""" % pr_fragments
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
'gh', 'api', 'graphql',
|
||||
'-F', f'owner={owner}',
|
||||
'-F', f'repo={repo_name}',
|
||||
'-f', f'query={query}',
|
||||
],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
data = json.loads(result.stdout)
|
||||
repo_data = data['data']['repository']
|
||||
|
||||
pr_issues_map: Dict[int, List[int]] = {}
|
||||
for pr_number in pr_numbers:
|
||||
nodes = repo_data.get(f'pr_{pr_number}', {}).get('closingIssuesReferences', {}).get('nodes', [])
|
||||
pr_issues = [node['number'] for node in nodes]
|
||||
pr_issues_map[pr_number] = pr_issues
|
||||
return pr_issues_map
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"::error::Error batch-fetching linked issues: {e.stderr}")
|
||||
raise
|
||||
|
||||
def map_issues_to_prs(pr_issues_map: Dict[int, List[int]]) -> Dict[int, List[int]]:
|
||||
"""Invert a PR->issues map into an issue->PRs map."""
|
||||
issue_pr_map: Dict[int, List[int]] = defaultdict(list)
|
||||
for pr_number, issue_numbers in pr_issues_map.items():
|
||||
for issue_number in issue_numbers:
|
||||
issue_pr_map[issue_number].append(pr_number)
|
||||
return dict(issue_pr_map)
|
||||
|
||||
def comment_issues(repo: str, issue_pr_map: Dict[int, List[int]], release_name: str, release_url: str, dry_run: bool) -> None:
|
||||
for issue_number, linked_prs in issue_pr_map.items():
|
||||
comment = build_issue_comment(repo, release_name, release_url, linked_prs)
|
||||
print(f"{'Dry run - ' if dry_run else ''}Commenting on issue {issue_number}:\n{comment}\n")
|
||||
if not dry_run and comment:
|
||||
gh_comment_issue(repo, issue_number, comment)
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Comment GitHub issues linked to Pull Requests mentioned in a given release.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'release_url',
|
||||
help='Release URL (e.g. https://github.com/owner/repo/releases/tag/v1.0.0)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Run without actually commenting issues'
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_args()
|
||||
|
||||
owner, repo_name, release_tag = parse_release_url(args.release_url)
|
||||
repo = f"{owner}/{repo_name}"
|
||||
print(f"📋 Release URL: {args.release_url}")
|
||||
|
||||
release_name, release_notes = gh_fetch_release(repo, release_tag)
|
||||
print(f"📋 Release Name: {release_name}")
|
||||
|
||||
pr_numbers = extract_pr_numbers(release_notes)
|
||||
print(f"📋 PR Numbers parsed from release notes: {pr_numbers}")
|
||||
pr_issues_map = gh_fetch_linked_issues_batched(owner, repo_name, pr_numbers)
|
||||
print(f"📋 PRs with linked issues: {[pr for pr, issues in pr_issues_map.items() if issues]}\n")
|
||||
issue_pr_map = map_issues_to_prs(pr_issues_map)
|
||||
comment_issues(repo, issue_pr_map, release_name, args.release_url, args.dry_run)
|
||||
2
.github/workflows/_version.yml
vendored
2
.github/workflows/_version.yml
vendored
@@ -167,7 +167,7 @@ jobs:
|
||||
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload version info artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: version-info
|
||||
path: version_info.json
|
||||
|
||||
33
.github/workflows/build-authenticator.yml
vendored
33
.github/workflows/build-authenticator.yml
vendored
@@ -49,10 +49,35 @@ jobs:
|
||||
version_number: ${{ inputs.version-code }}
|
||||
patch_version: ${{ inputs.patch_version && '999' || '' }}
|
||||
|
||||
build:
|
||||
name: Build Authenticator
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Android Build
|
||||
uses: ./.github/actions/setup-android-build
|
||||
|
||||
- name: Check Authenticator
|
||||
run: bundle exec fastlane check
|
||||
|
||||
- name: Build Authenticator
|
||||
run: bundle exec fastlane buildAuthenticatorDebug
|
||||
|
||||
publish_playstore:
|
||||
name: Publish Authenticator Play Store artifacts
|
||||
needs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -176,7 +201,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.aab
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.bitwarden.authenticator.aab
|
||||
path: authenticator/build/outputs/bundle/release/com.bitwarden.authenticator.aab
|
||||
@@ -184,7 +209,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.apk
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.bitwarden.authenticator.apk
|
||||
path: authenticator/build/outputs/apk/release/com.bitwarden.authenticator.apk
|
||||
@@ -204,7 +229,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.apk-sha256.txt
|
||||
if: ${{ matrix.variant == 'apk' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: authenticator-android-apk-sha256.txt
|
||||
path: ./authenticator-android-apk-sha256.txt
|
||||
@@ -212,7 +237,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.aab-sha256.txt
|
||||
if: ${{ matrix.variant == 'aab' }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: authenticator-android-aab-sha256.txt
|
||||
path: ./authenticator-android-aab-sha256.txt
|
||||
|
||||
4
.github/workflows/build-testharness.yml
vendored
4
.github/workflows/build-testharness.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
run: ./gradlew :testharness:assembleDebug
|
||||
|
||||
- name: Upload Test Harness APK
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.bitwarden.testharness.dev-debug.apk
|
||||
path: testharness/build/outputs/apk/debug/com.bitwarden.testharness.dev.apk
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
> ./com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
|
||||
- name: Upload Test Harness SHA file
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
path: ./com.bitwarden.testharness.dev.apk-sha256.txt
|
||||
|
||||
61
.github/workflows/build.yml
vendored
61
.github/workflows/build.yml
vendored
@@ -51,10 +51,42 @@ jobs:
|
||||
version_number: ${{ inputs.version-code }}
|
||||
patch_version: ${{ inputs.patch_version && '999' || '' }}
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Android Build
|
||||
uses: ./.github/actions/setup-android-build
|
||||
|
||||
- name: Check
|
||||
run: bundle exec fastlane check
|
||||
|
||||
- name: Build
|
||||
run: bundle exec fastlane assembleDebugApks
|
||||
|
||||
- name: Upload test reports on failure
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: failure()
|
||||
with:
|
||||
name: test-reports
|
||||
path: app/build/reports/tests/
|
||||
|
||||
publish_playstore:
|
||||
name: Publish Play Store artifacts
|
||||
needs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -192,7 +224,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.aab
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: app/build/outputs/bundle/standardRelease/com.x8bit.bitwarden.aab
|
||||
@@ -200,7 +232,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.aab
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab
|
||||
path: app/build/outputs/bundle/standardBeta/com.x8bit.bitwarden.beta.aab
|
||||
@@ -208,7 +240,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.apk
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: app/build/outputs/apk/standard/release/com.x8bit.bitwarden.apk
|
||||
@@ -216,7 +248,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.apk
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk
|
||||
path: app/build/outputs/apk/standard/beta/com.x8bit.bitwarden.beta.apk
|
||||
@@ -225,7 +257,7 @@ jobs:
|
||||
# When building variants other than 'prod'
|
||||
- name: Upload to GitHub Artifacts - dev.apk
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk
|
||||
path: app/build/outputs/apk/standard/debug/com.x8bit.bitwarden.dev.apk
|
||||
@@ -263,7 +295,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.apk-sha256.txt
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.apk-sha256.txt
|
||||
@@ -271,7 +303,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.apk-sha256.txt
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.apk-sha256.txt
|
||||
@@ -279,7 +311,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - prod.aab-sha256.txt
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.aab-sha256.txt
|
||||
@@ -287,7 +319,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.aab-sha256.txt
|
||||
if: ${{ (matrix.variant == 'prod') && (matrix.artifact == 'aab') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta.aab-sha256.txt
|
||||
@@ -295,7 +327,7 @@ jobs:
|
||||
|
||||
- name: Upload to GitHub Artifacts - debug.apk-sha256.txt
|
||||
if: ${{ (matrix.variant != 'prod') && (matrix.artifact == 'apk') }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.${{ matrix.variant }}.apk-sha256.txt
|
||||
@@ -338,6 +370,7 @@ jobs:
|
||||
name: Publish F-Droid artifacts
|
||||
needs:
|
||||
- version
|
||||
- build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -424,7 +457,7 @@ jobs:
|
||||
keyPassword:$FDROID_BETA_KEY_PASSWORD
|
||||
|
||||
- name: Upload to GitHub Artifacts - fdroid.apk
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/release/com.x8bit.bitwarden-fdroid.apk
|
||||
@@ -436,14 +469,14 @@ jobs:
|
||||
> ./com.x8bit.bitwarden-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload to GitHub Artifacts - fdroid.apk-sha256.txt
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.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 to GitHub Artifacts - beta.fdroid.apk
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk
|
||||
path: app/build/outputs/apk/fdroid/beta/com.x8bit.bitwarden.beta-fdroid.apk
|
||||
@@ -455,7 +488,7 @@ jobs:
|
||||
> ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
- name: Upload to GitHub Artifacts - beta.fdroid.apk-sha256.txt
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
path: ./com.x8bit.bitwarden.beta-fdroid.apk-sha256.txt
|
||||
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
name: SDLC / Update Linked Issues on Release
|
||||
run-name: ${{ inputs.dry-run && '(Dry Run) ' || '' }}Update Linked Issues on Release - ${{ github.event.release.name || inputs.release_url }}
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_url:
|
||||
description: 'Release URL (e.g. https://github.com/owner/repo/releases/tag/v1.0.0)'
|
||||
required: true
|
||||
dry-run:
|
||||
description: 'Dry run'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
update-linked-issues:
|
||||
name: Update Linked Issues
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Update Linked Issues
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
_RELEASE_URL: ${{ github.event.release.html_url || inputs.release_url }}
|
||||
_DRY_RUN: ${{ inputs.dry-run && '--dry-run' || '' }}
|
||||
run: |
|
||||
python3 .github/scripts/gh_release_update_issues.py "$_RELEASE_URL" $_DRY_RUN
|
||||
2
.github/workflows/sdlc-sdk-update.yml
vendored
2
.github/workflows/sdlc-sdk-update.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
||||
disable_search: true
|
||||
|
||||
- name: Upload test reports
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: test-reports-${{ matrix.group }}
|
||||
|
||||
28
Gemfile.lock
28
Gemfile.lock
@@ -3,13 +3,13 @@ GEM
|
||||
specs:
|
||||
CFPropertyList (3.0.8)
|
||||
abbrev (0.1.2)
|
||||
addressable (2.8.9)
|
||||
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.1231.0)
|
||||
aws-sdk-core (3.244.0)
|
||||
aws-partitions (1.1213.0)
|
||||
aws-sdk-core (3.242.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -17,18 +17,18 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-kms (1.121.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.217.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-s3 (1.213.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (4.1.0)
|
||||
bigdecimal (4.0.1)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
@@ -68,10 +68,10 @@ GEM
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.4)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.1)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.229.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
@@ -148,7 +148,7 @@ GEM
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.6.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
@@ -169,7 +169,7 @@ GEM
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.19.3)
|
||||
json (2.18.1)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
@@ -185,13 +185,13 @@ GEM
|
||||
os (1.1.4)
|
||||
ostruct (0.6.3)
|
||||
plist (3.7.2)
|
||||
public_suffix (7.0.5)
|
||||
public_suffix (7.0.2)
|
||||
rake (13.3.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.4.1)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
|
||||
@@ -297,7 +297,6 @@ dependencies {
|
||||
standardImplementation(libs.google.firebase.cloud.messaging)
|
||||
standardImplementation(platform(libs.google.firebase.bom))
|
||||
standardImplementation(libs.google.firebase.crashlytics)
|
||||
standardImplementation(libs.google.billing)
|
||||
standardImplementation(libs.google.play.review)
|
||||
|
||||
// Pull in test fixtures from other modules
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.manager
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* F-Droid implementation of [PlayBillingManager]. Always returns `true` since
|
||||
* F-Droid users are eligible for the Premium upgrade flow.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@Suppress("UnusedParameter")
|
||||
class PlayBillingManagerImpl(
|
||||
context: Context,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : PlayBillingManager {
|
||||
|
||||
override val isInAppBillingSupportedFlow: StateFlow<Boolean> =
|
||||
MutableStateFlow(true)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* F-Droid implementation of [GmsManager]. Always returns `false` since GMS is not available.
|
||||
*/
|
||||
@Suppress("UnusedParameter")
|
||||
class GmsManagerImpl(
|
||||
context: Context,
|
||||
) : GmsManager {
|
||||
|
||||
override fun isVersionAtLeast(version: Int): Boolean = false
|
||||
}
|
||||
@@ -40,18 +40,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.calyxos.chromium",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "CB:33:EE:73:84:2F:2F:BD:C3:E3:52:5F:D1:C3:74:07:41:82:6F:33:84:9B:C9:6F:95:4D:76:18:17:D3:00:EB"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
|
||||
@@ -88,10 +88,6 @@ class MainActivity : AppCompatActivity() {
|
||||
mainViewModel.trySendAction(MainAction.CookieAcquisitionResult(it))
|
||||
}
|
||||
|
||||
private val premiumCheckoutLauncher = AuthTabIntent.registerActivityResultLauncher(this) {
|
||||
mainViewModel.trySendAction(MainAction.PremiumCheckoutResult(it))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
intent = intent.validate()
|
||||
var shouldShowSplashScreen = true
|
||||
@@ -119,7 +115,6 @@ class MainActivity : AppCompatActivity() {
|
||||
sso = ssoLauncher,
|
||||
webAuthn = webAuthnLauncher,
|
||||
cookie = cookieLauncher,
|
||||
premiumCheckout = premiumCheckoutLauncher,
|
||||
),
|
||||
) {
|
||||
ObserveScreenDataEffect(
|
||||
|
||||
@@ -46,7 +46,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.platform.util.isPremiumCheckoutCallback
|
||||
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -198,7 +197,6 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.SsoResult -> handleSsoResult(action)
|
||||
is MainAction.WebAuthnResult -> handleWebAuthnResult(action)
|
||||
is MainAction.CookieAcquisitionResult -> handleCookieAcquisitionResult(action)
|
||||
is MainAction.PremiumCheckoutResult -> handlePremiumCheckoutResult()
|
||||
is MainAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
@@ -248,11 +246,6 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun handlePremiumCheckoutResult() {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PremiumCheckoutResult
|
||||
}
|
||||
|
||||
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
|
||||
when (val data = action.screenResumeData) {
|
||||
null -> appResumeManager.clearResumeScreen()
|
||||
@@ -340,7 +333,6 @@ class MainViewModel @Inject constructor(
|
||||
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
|
||||
val hasVaultShortcut = intent.isMyVaultShortcut
|
||||
val hasAccountSecurityShortcut = intent.isAccountSecurityShortcut
|
||||
val hasPremiumCheckoutCallback = intent.isPremiumCheckoutCallback
|
||||
val completeRegistrationData = intent.getCompleteRegistrationDataIntentOrNull()
|
||||
val importCredentialsRequest = intent.getProviderImportCredentialsRequest()
|
||||
val credentialProviderRequest =
|
||||
@@ -402,11 +394,6 @@ class MainViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
hasPremiumCheckoutCallback -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.PremiumCheckoutResult
|
||||
}
|
||||
|
||||
hasGeneratorShortcut -> {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.GeneratorShortcut
|
||||
@@ -561,13 +548,6 @@ sealed class MainAction {
|
||||
val cookieCallbackResult: AuthTabIntent.AuthResult,
|
||||
) : MainAction()
|
||||
|
||||
/**
|
||||
* Receive the result from the premium checkout flow.
|
||||
*/
|
||||
data class PremiumCheckoutResult(
|
||||
val authResult: AuthTabIntent.AuthResult,
|
||||
) : MainAction()
|
||||
|
||||
/**
|
||||
* Receive first Intent by the application.
|
||||
*/
|
||||
|
||||
@@ -124,16 +124,6 @@ interface AuthDiskSource : AppIdProvider {
|
||||
*/
|
||||
fun storeUserKey(userId: String, userKey: String?)
|
||||
|
||||
/**
|
||||
* Retrieves the local user data key for the given [userId].
|
||||
*/
|
||||
fun getLocalUserDataKey(userId: String): String?
|
||||
|
||||
/**
|
||||
* Stores the local user data key for a given [userId].
|
||||
*/
|
||||
fun storeLocalUserDataKey(userId: String, wrappedKey: String?)
|
||||
|
||||
/**
|
||||
* Retrieves a private key using a [userId].
|
||||
*/
|
||||
|
||||
@@ -35,7 +35,6 @@ private const val REMEMBERED_ORG_IDENTIFIER_KEY = "rememberedOrgIdentifier"
|
||||
private const val STATE_KEY = "state"
|
||||
private const val INVALID_UNLOCK_ATTEMPTS_KEY = "invalidUnlockAttempts"
|
||||
private const val MASTER_KEY_ENCRYPTION_USER_KEY = "masterKeyEncryptedUserKey"
|
||||
private const val LOCAL_USER_DATA_KEY = "localUserDataKey"
|
||||
private const val MASTER_KEY_ENCRYPTION_PRIVATE_KEY = "encPrivateKey"
|
||||
private const val PIN_PROTECTED_USER_KEY_KEY = "pinKeyEncryptedUserKey"
|
||||
private const val PIN_PROTECTED_USER_KEY_KEY_ENVELOPE = "pinKeyEncryptedUserKeyEnvelope"
|
||||
@@ -145,7 +144,6 @@ class AuthDiskSourceImpl(
|
||||
override fun clearData(userId: String) {
|
||||
storeInvalidUnlockAttempts(userId = userId, invalidUnlockAttempts = null)
|
||||
storeUserKey(userId = userId, userKey = null)
|
||||
storeLocalUserDataKey(userId = userId, wrappedKey = null)
|
||||
storeUserAutoUnlockKey(userId = userId, userAutoUnlockKey = null)
|
||||
storePrivateKey(userId = userId, privateKey = null)
|
||||
storeAccountKeys(userId = userId, accountKeys = null)
|
||||
@@ -239,13 +237,6 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getLocalUserDataKey(userId: String): String? =
|
||||
getString(key = LOCAL_USER_DATA_KEY.appendIdentifier(userId))
|
||||
|
||||
override fun storeLocalUserDataKey(userId: String, wrappedKey: String?) {
|
||||
putString(key = LOCAL_USER_DATA_KEY.appendIdentifier(userId), value = wrappedKey)
|
||||
}
|
||||
|
||||
@Deprecated("Use getAccountKeys instead.", replaceWith = ReplaceWith("getAccountKeys"))
|
||||
override fun getPrivateKey(userId: String): String? =
|
||||
getString(key = MASTER_KEY_ENCRYPTION_PRIVATE_KEY.appendIdentifier(userId))
|
||||
|
||||
@@ -37,12 +37,12 @@ data class AccountJson(
|
||||
*
|
||||
* @property userId The ID of the user.
|
||||
* @property email The user's email address.
|
||||
* @property isEmailVerified Whether the user's email is verified.
|
||||
* @property isTwoFactorEnabled If the profile has two-factor authentication enabled.
|
||||
* @property isEmailVerified Whether or not the user's email is verified.
|
||||
* @property isTwoFactorEnabled If the profile has two factor authentication enabled.
|
||||
* @property name The user's name (if applicable).
|
||||
* @property stamp The account's security stamp (if applicable).
|
||||
* @property organizationId The ID of the associated organization (if applicable).
|
||||
* @property hasPremium True if the user has a Premium account.
|
||||
* @property hasPremium True if the user has a premium account.
|
||||
* @property avatarColorHex Hex color value for a user's avatar in the "#AARRGGBB" format.
|
||||
* @property forcePasswordResetReason Describes the reason for a forced password reset.
|
||||
* @property kdfType The KDF type.
|
||||
|
||||
@@ -136,7 +136,7 @@ interface AuthRepository :
|
||||
val organizations: List<Organization>
|
||||
|
||||
/**
|
||||
* Whether the welcome carousel should be displayed, based on the feature flag and
|
||||
* Whether or not the welcome carousel should be displayed, based on the feature flag and
|
||||
* whether the user has ever logged in or created an account before.
|
||||
*/
|
||||
val showWelcomeCarousel: Boolean
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.ui.platform.base.util.toHexColorRepresentation
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Represents the overall "user state" of the current active user as well as any users that may be
|
||||
@@ -42,10 +40,10 @@ data class UserState(
|
||||
* @property name The user's name (if applicable).
|
||||
* @property avatarColorHex Hex color value for a user's avatar in the "#AARRGGBB" format.
|
||||
* @property environment The [Environment] associated with the user's account.
|
||||
* @property isPremium `true` if the account has a Premium membership.
|
||||
* @property isPremium `true` if the account has a premium membership.
|
||||
* @property isLoggedIn `true` if the account is logged in, or `false` if it requires additional
|
||||
* authentication to view their vault.
|
||||
* @property isVaultUnlocked Whether the user's vault is currently unlocked.
|
||||
* @property isVaultUnlocked Whether or not the user's vault is currently unlocked.
|
||||
* @property needsPasswordReset If the user needs to reset their password.
|
||||
* @property needsMasterPassword Indicates whether the user needs to create a password (e.g.
|
||||
* they logged in using SSO and don't yet have one). NOTE: This should **not** be used to
|
||||
@@ -57,7 +55,6 @@ data class UserState(
|
||||
* user's vault is enabled.
|
||||
* @property vaultUnlockType The mechanism by which the user's vault may be unlocked.
|
||||
* @property isUsingKeyConnector Indicates if the account is currently using a key connector.
|
||||
* @property creationDate The date the account was created, if available.
|
||||
*/
|
||||
data class Account(
|
||||
val userId: String,
|
||||
@@ -79,7 +76,6 @@ data class UserState(
|
||||
val onboardingStatus: OnboardingStatus,
|
||||
val firstTimeState: FirstTimeState,
|
||||
val isExportable: Boolean,
|
||||
val creationDate: Instant?,
|
||||
) {
|
||||
/**
|
||||
* Indicates that the user does or does not have a means to manually unlock the vault.
|
||||
@@ -100,33 +96,4 @@ data class UserState(
|
||||
val hasLoginApprovingDevice: Boolean,
|
||||
val hasResetPasswordPermission: Boolean,
|
||||
)
|
||||
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* A basic empty account model.
|
||||
*/
|
||||
val EMPTY_ACCOUNT: Account = Account(
|
||||
userId = "",
|
||||
name = null,
|
||||
email = "",
|
||||
avatarColorHex = "".toHexColorRepresentation(),
|
||||
environment = Environment.Us,
|
||||
isPremium = false,
|
||||
isLoggedIn = false,
|
||||
isVaultUnlocked = false,
|
||||
needsPasswordReset = false,
|
||||
organizations = emptyList(),
|
||||
isBiometricsEnabled = false,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
needsMasterPassword = false,
|
||||
hasMasterPassword = true,
|
||||
trustedDevice = null,
|
||||
isUsingKeyConnector = false,
|
||||
onboardingStatus = OnboardingStatus.COMPLETE,
|
||||
firstTimeState = FirstTimeState(),
|
||||
isExportable = false,
|
||||
creationDate = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,6 @@ fun UserStateJson.toUserState(
|
||||
firstTimeState = firstTimeState,
|
||||
isExportable = !hasPersonalOwnershipRestrictedOrg &&
|
||||
!hasPersonalVaultExportRestrictedOrg,
|
||||
creationDate = profile.creationDate,
|
||||
)
|
||||
},
|
||||
hasPendingAccountAddition = hasPendingAccountAddition,
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* A container for values specifying whether the accessibility service is enabled.
|
||||
* A container for values specifying whether or not the accessibility service is enabled.
|
||||
*/
|
||||
interface AccessibilityEnabledManager {
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofi
|
||||
import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Primary implementation of [AutofillActivityManager].
|
||||
@@ -21,34 +20,10 @@ class AutofillActivityManagerImpl(
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
|
||||
) : AutofillActivityManager {
|
||||
private val autofillManagerIsEnabled: Boolean
|
||||
get() = try {
|
||||
autofillManager.isEnabled
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: RuntimeException) {
|
||||
Timber.e(e, "autofillManager.isEnabled failed")
|
||||
false
|
||||
}
|
||||
|
||||
private val autofillManagerHasEnabledAutofillServices: Boolean
|
||||
get() = try {
|
||||
autofillManager.hasEnabledAutofillServices()
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: RuntimeException) {
|
||||
Timber.e(e, "autofillManager.hasEnabledAutofillServices() failed")
|
||||
false
|
||||
}
|
||||
|
||||
private val autofillManagerIsAutofillSupported: Boolean
|
||||
get() = try {
|
||||
autofillManager.isAutofillSupported
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: RuntimeException) {
|
||||
Timber.e(e, "autofillManager.isAutofillSupported() failed")
|
||||
false
|
||||
}
|
||||
|
||||
private val isAutofillEnabledAndSupported: Boolean
|
||||
get() = autofillManagerIsEnabled &&
|
||||
autofillManagerHasEnabledAutofillServices &&
|
||||
autofillManagerIsAutofillSupported
|
||||
get() = autofillManager.isEnabled &&
|
||||
autofillManager.hasEnabledAutofillServices() &&
|
||||
autofillManager.isAutofillSupported
|
||||
|
||||
private val browserAutofillStatus: BrowserThirdPartyAutofillStatus
|
||||
get() = BrowserThirdPartyAutofillStatus(
|
||||
|
||||
@@ -3,12 +3,12 @@ package com.x8bit.bitwarden.data.autofill.manager
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* A container for values specifying whether autofill is enabled. These values should be
|
||||
* A container for values specifying whether or not autofill is enabled. These values should be
|
||||
* filled by an [AutofillActivityManager].
|
||||
*/
|
||||
interface AutofillEnabledManager {
|
||||
/**
|
||||
* Whether autofill should be considered enabled.
|
||||
* Whether or not autofill should be considered enabled.
|
||||
*
|
||||
* Note that changing this does not enable or disable autofill; it is only an indicator that
|
||||
* this has occurred elsewhere.
|
||||
|
||||
@@ -62,13 +62,11 @@ class BrowserThirdPartyAutofillManagerImpl(
|
||||
)
|
||||
var thirdPartyEnabled = false
|
||||
val isThirdPartyAvailable = cursor
|
||||
?.use {
|
||||
?.let {
|
||||
it.moveToFirst()
|
||||
thirdPartyEnabled = it
|
||||
.getColumnIndex(THIRD_PARTY_MODE_COLUMN)
|
||||
.takeUnless { columnIndex -> columnIndex == -1 }
|
||||
?.let { columnIndex -> it.getInt(columnIndex) != 0 }
|
||||
?: false
|
||||
val columnIndex = it.getColumnIndex(THIRD_PARTY_MODE_COLUMN)
|
||||
thirdPartyEnabled = it.getInt(columnIndex) != 0
|
||||
it.close()
|
||||
true
|
||||
}
|
||||
?: false
|
||||
|
||||
@@ -14,7 +14,7 @@ sealed class AutofillCipher {
|
||||
abstract val iconRes: Int
|
||||
|
||||
/**
|
||||
* Whether TOTP is enabled for this cipher.
|
||||
* Whether or not TOTP is enabled for this cipher.
|
||||
*/
|
||||
abstract val isTotpEnabled: Boolean
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.datasource.network.di
|
||||
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.service.BillingService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides network dependencies in the billing package.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object BillingNetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBillingService(
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): BillingService = bitwardenServiceClient.billingService
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.di
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.network.service.BillingService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.billing.manager.PlayBillingManager
|
||||
import com.x8bit.bitwarden.data.billing.manager.PlayBillingManagerImpl
|
||||
import com.x8bit.bitwarden.data.billing.manager.PremiumStateManager
|
||||
import com.x8bit.bitwarden.data.billing.manager.PremiumStateManagerImpl
|
||||
import com.x8bit.bitwarden.data.billing.repository.BillingRepository
|
||||
import com.x8bit.bitwarden.data.billing.repository.BillingRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides billing-related dependencies.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object BillingModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePlayBillingManager(
|
||||
@ApplicationContext context: Context,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): PlayBillingManager = PlayBillingManagerImpl(
|
||||
context = context,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBillingRepository(
|
||||
playBillingManager: PlayBillingManager,
|
||||
billingService: BillingService,
|
||||
): BillingRepository = BillingRepositoryImpl(
|
||||
playBillingManager = playBillingManager,
|
||||
billingService = billingService,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePremiumStateManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
authRepository: AuthRepository,
|
||||
billingRepository: BillingRepository,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
vaultRepository: VaultRepository,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): PremiumStateManager = PremiumStateManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
authRepository = authRepository,
|
||||
billingRepository = billingRepository,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
clock = clock,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.manager
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manages interactions with the Google Play Billing system.
|
||||
*/
|
||||
interface PlayBillingManager {
|
||||
|
||||
/**
|
||||
* Emits `true` when in-app billing is supported, or `false` otherwise.
|
||||
*/
|
||||
val isInAppBillingSupportedFlow: StateFlow<Boolean>
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.manager
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Manages the consolidated eligibility state for the Premium upgrade banner.
|
||||
*
|
||||
* Combines multiple upstream signals (Premium status, billing support, feature flag,
|
||||
* banner dismissal, account age, and vault item count) into a single observable flow.
|
||||
*/
|
||||
interface PremiumStateManager {
|
||||
|
||||
/**
|
||||
* Emits `true` when the current user is eligible to see the Premium upgrade banner,
|
||||
* or `false` otherwise.
|
||||
*/
|
||||
val isPremiumUpgradeBannerEligibleFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Marks the Premium upgrade banner as dismissed for the current user.
|
||||
*/
|
||||
fun dismissPremiumUpgradeBanner()
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.manager
|
||||
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.billing.repository.BillingRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Default implementation of [PremiumStateManager].
|
||||
*
|
||||
* Combines five upstream flows into a single eligibility signal using [combine].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class PremiumStateManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
authRepository: AuthRepository,
|
||||
billingRepository: BillingRepository,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
vaultRepository: VaultRepository,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
private val clock: Clock,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : PremiumStateManager {
|
||||
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val isPremiumUpgradeBannerEligibleFlow: StateFlow<Boolean> =
|
||||
combine(
|
||||
authRepository.userStateFlow,
|
||||
billingRepository.isInAppBillingSupportedFlow,
|
||||
featureFlagManager.getFeatureFlagFlow(FlagKey.MobilePremiumUpgrade),
|
||||
authDiskSource.activeUserIdChangesFlow
|
||||
.flatMapLatest { userId ->
|
||||
userId
|
||||
?.let { id ->
|
||||
settingsDiskSource
|
||||
.getPremiumUpgradeBannerDismissedFlow(id)
|
||||
.map { it ?: false }
|
||||
}
|
||||
?: flowOf(false)
|
||||
},
|
||||
vaultRepository.vaultDataStateFlow,
|
||||
) { userState,
|
||||
isInAppBillingSupported,
|
||||
featureFlagEnabled,
|
||||
isDismissed,
|
||||
vaultDataState,
|
||||
->
|
||||
val activeAccount = userState?.activeAccount
|
||||
?: return@combine false
|
||||
val isPremium = activeAccount.isPremium
|
||||
val isAccountOldEnough = activeAccount.creationDate.isOlderThanDays(
|
||||
days = PREMIUM_UPGRADE_MINIMUM_ACCOUNT_AGE_DAYS,
|
||||
clock = clock,
|
||||
)
|
||||
val itemCount = vaultDataState.activeVaultItemCount()
|
||||
|
||||
!isPremium &&
|
||||
isInAppBillingSupported &&
|
||||
featureFlagEnabled &&
|
||||
!isDismissed &&
|
||||
isAccountOldEnough &&
|
||||
itemCount >= PREMIUM_UPGRADE_MINIMUM_VAULT_ITEMS
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
override fun dismissPremiumUpgradeBanner() {
|
||||
val activeUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
settingsDiskSource.storePremiumUpgradeBannerDismissed(
|
||||
userId = activeUserId,
|
||||
isDismissed = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this [Instant] is older than the given number of [days] based on
|
||||
* the provided [clock]. Returns `false` if the receiver is `null`.
|
||||
*/
|
||||
private fun Instant?.isOlderThanDays(days: Long, clock: Clock): Boolean {
|
||||
this ?: return false
|
||||
val now = clock.instant()
|
||||
val ageInDays = Duration.between(this, now).toDays()
|
||||
return ageInDays >= days
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the count of active (non-deleted, non-archived) vault items from the
|
||||
* current [DataState].
|
||||
*/
|
||||
private fun DataState<VaultData>.activeVaultItemCount(): Int =
|
||||
data
|
||||
?.decryptCipherListResult
|
||||
?.successes
|
||||
?.count { it.deletedDate == null && it.archivedDate == null }
|
||||
?: 0
|
||||
|
||||
private const val PREMIUM_UPGRADE_MINIMUM_VAULT_ITEMS: Int = 5
|
||||
private const val PREMIUM_UPGRADE_MINIMUM_ACCOUNT_AGE_DAYS: Long = 7L
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.CheckoutSessionResult
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.CustomerPortalResult
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Provides an API for managing billing operations.
|
||||
*/
|
||||
interface BillingRepository {
|
||||
|
||||
/**
|
||||
* Emits `true` when in-app billing is supported, or `false` otherwise.
|
||||
*/
|
||||
val isInAppBillingSupportedFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Creates a Stripe checkout session and returns the checkout URL.
|
||||
*/
|
||||
suspend fun getCheckoutSessionUrl(): CheckoutSessionResult
|
||||
|
||||
/**
|
||||
* Retrieves the Stripe customer portal URL for managing the Premium subscription.
|
||||
*/
|
||||
suspend fun getPortalUrl(): CustomerPortalResult
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.repository
|
||||
|
||||
import com.bitwarden.network.service.BillingService
|
||||
import com.x8bit.bitwarden.data.billing.manager.PlayBillingManager
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.CheckoutSessionResult
|
||||
import com.x8bit.bitwarden.data.billing.repository.model.CustomerPortalResult
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* The default implementation of [BillingRepository].
|
||||
*/
|
||||
class BillingRepositoryImpl(
|
||||
playBillingManager: PlayBillingManager,
|
||||
private val billingService: BillingService,
|
||||
) : BillingRepository {
|
||||
|
||||
override val isInAppBillingSupportedFlow: StateFlow<Boolean> =
|
||||
playBillingManager.isInAppBillingSupportedFlow
|
||||
|
||||
override suspend fun getCheckoutSessionUrl(): CheckoutSessionResult =
|
||||
billingService
|
||||
.createCheckoutSession()
|
||||
.fold(
|
||||
onSuccess = { CheckoutSessionResult.Success(url = it.checkoutSessionUrl) },
|
||||
onFailure = { CheckoutSessionResult.Error(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun getPortalUrl(): CustomerPortalResult =
|
||||
billingService
|
||||
.getPortalUrl()
|
||||
.fold(
|
||||
onSuccess = { CustomerPortalResult.Success(url = it.url) },
|
||||
onFailure = { CustomerPortalResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.util.userFriendlyMessage
|
||||
|
||||
/**
|
||||
* Models the result of creating a Stripe checkout session.
|
||||
*/
|
||||
sealed class CheckoutSessionResult {
|
||||
|
||||
/**
|
||||
* The checkout session URL was successfully retrieved.
|
||||
*/
|
||||
data class Success(
|
||||
val url: String,
|
||||
) : CheckoutSessionResult()
|
||||
|
||||
/**
|
||||
* Generic error while creating a checkout session. The optional [errorMessage] may be
|
||||
* displayed directly in the UI when present.
|
||||
*/
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
val errorMessage: String? = error.userFriendlyMessage,
|
||||
) : CheckoutSessionResult()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.billing.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.util.userFriendlyMessage
|
||||
|
||||
/**
|
||||
* Models the result of retrieving the Stripe customer portal URL.
|
||||
*/
|
||||
sealed class CustomerPortalResult {
|
||||
|
||||
/**
|
||||
* The customer portal URL was successfully retrieved.
|
||||
*/
|
||||
data class Success(
|
||||
val url: String,
|
||||
) : CustomerPortalResult()
|
||||
|
||||
/**
|
||||
* Generic error while retrieving the customer portal URL. The optional [errorMessage] may
|
||||
* be displayed directly in the UI when present.
|
||||
*/
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
val errorMessage: String? = error.userFriendlyMessage,
|
||||
) : CustomerPortalResult()
|
||||
}
|
||||
@@ -57,7 +57,7 @@ interface BitwardenCredentialManager {
|
||||
): Fido2CredentialAssertionResult
|
||||
|
||||
/**
|
||||
* Whether the user has authentication attempts remaining.
|
||||
* Whether or not the user has authentication attempts remaining.
|
||||
*/
|
||||
fun hasAuthenticationAttemptsRemaining(): Boolean
|
||||
|
||||
|
||||
@@ -123,24 +123,6 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
*/
|
||||
fun getIntroducingArchiveActionCardDismissedFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Retrieves the stored value of whether the Premium upgrade banner has been dismissed.
|
||||
*/
|
||||
fun getPremiumUpgradeBannerDismissed(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores whether the Premium upgrade banner has been dismissed.
|
||||
*/
|
||||
fun storePremiumUpgradeBannerDismissed(
|
||||
userId: String,
|
||||
isDismissed: Boolean?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Emits updates that track [getPremiumUpgradeBannerDismissed] for the given [userId].
|
||||
*/
|
||||
fun getPremiumUpgradeBannerDismissedFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Retrieves the biometric integrity validity for the given [userId] and
|
||||
* [systemBioIntegrityState].
|
||||
@@ -247,7 +229,7 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
fun storeDefaultUriMatchType(userId: String, uriMatchType: UriMatchType?)
|
||||
|
||||
/**
|
||||
* Gets the value for whether the autofill save prompt should be disabled for the
|
||||
* Gets the value for whether or not the autofill save prompt should be disabled for the
|
||||
* given [userId].
|
||||
*/
|
||||
fun getAutofillSavePromptDisabled(userId: String): Boolean?
|
||||
@@ -313,13 +295,13 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
fun getUserHasSignedInPreviously(userId: String): Boolean
|
||||
|
||||
/**
|
||||
* Gets whether the given [userId] has signaled they want to enable autofill in
|
||||
* Gets whether or not the given [userId] has signalled they want to enable autofill in
|
||||
* onboarding.
|
||||
*/
|
||||
fun getShowBrowserAutofillSettingBadge(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether the given [userId] has signaled they want to
|
||||
* Stores the given value for whether or not the given [userId] has signalled they want to
|
||||
* enable the browser autofill integration in onboarding.
|
||||
*/
|
||||
fun storeShowBrowserAutofillSettingBadge(userId: String, showBadge: Boolean?)
|
||||
@@ -330,13 +312,13 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
fun getShowBrowserAutofillSettingBadgeFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets whether the given [userId] has signaled they want to enable autofill in
|
||||
* Gets whether or not the given [userId] has signalled they want to enable autofill in
|
||||
* onboarding.
|
||||
*/
|
||||
fun getShowAutoFillSettingBadge(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether the given [userId] has signaled they want to
|
||||
* Stores the given value for whether or not the given [userId] has signalled they want to
|
||||
* enable autofill in onboarding.
|
||||
*/
|
||||
fun storeShowAutoFillSettingBadge(userId: String, showBadge: Boolean?)
|
||||
@@ -347,13 +329,13 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
fun getShowAutoFillSettingBadgeFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets whether the given [userId] has signaled they want to enable unlock options
|
||||
* Gets whether or not the given [userId] has signalled they want to enable unlock options
|
||||
* later, during onboarding.
|
||||
*/
|
||||
fun getShowUnlockSettingBadge(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether the given [userId] has signaled they want to
|
||||
* Stores the given value for whether or not the given [userId] has signalled they want to
|
||||
* set up unlock options later, during onboarding.
|
||||
*/
|
||||
fun storeShowUnlockSettingBadge(userId: String, showBadge: Boolean?)
|
||||
@@ -364,12 +346,12 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
fun getShowUnlockSettingBadgeFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets whether the given [userId] has signaled they want to import logins later.
|
||||
* Gets whether or not the given [userId] has signalled they want to import logins later.
|
||||
*/
|
||||
fun getShowImportLoginsSettingBadge(userId: String): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether the given [userId] has signaled they want to
|
||||
* Stores the given value for whether or not the given [userId] has signalled they want to
|
||||
* set import logins later, during first time usage.
|
||||
*/
|
||||
fun storeShowImportLoginsSettingBadge(userId: String, showBadge: Boolean?)
|
||||
@@ -380,13 +362,13 @@ interface SettingsDiskSource : FlightRecorderDiskSource {
|
||||
fun getShowImportLoginsSettingBadgeFlow(userId: String): Flow<Boolean?>
|
||||
|
||||
/**
|
||||
* Gets whether the application has registered for export via the credential exchange
|
||||
* Gets whether or not the application has registered for export via the credential exchange
|
||||
* protocol.
|
||||
*/
|
||||
fun getAppRegisteredForExport(): Boolean?
|
||||
|
||||
/**
|
||||
* Stores the given value for whether the application has registered for export via
|
||||
* Stores the given value for whether or not the application has registered for export via
|
||||
* the credential exchange protocol.
|
||||
*/
|
||||
fun storeAppRegisteredForExport(isRegistered: Boolean?)
|
||||
|
||||
@@ -51,8 +51,6 @@ private const val IS_DYNAMIC_COLORS_ENABLED = "isDynamicColorsEnabled"
|
||||
private const val BROWSER_AUTOFILL_DIALOG_RESHOW_TIME = "browserAutofillDialogReshowTime"
|
||||
private const val INTRODUCING_ARCHIVE_ACTION_CARD_DISMISSED =
|
||||
"introducingArchiveActionCardDismissed"
|
||||
private const val PREMIUM_UPGRADE_BANNER_DISMISSED =
|
||||
"premiumUpgradeBannerDismissed"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
@@ -94,9 +92,6 @@ class SettingsDiskSourceImpl(
|
||||
private val mutableIntroducingArchiveActionCardDismissedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutablePremiumUpgradeBannerDismissedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
private val mutableIsIconLoadingDisabledFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableIsCrashLoggingEnabledFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
@@ -251,7 +246,6 @@ class SettingsDiskSourceImpl(
|
||||
// - should show add login coach mark
|
||||
// - should show generator coach mark
|
||||
// - should show introducing archive action card dismissed
|
||||
// - Premium upgrade banner dismissed
|
||||
}
|
||||
|
||||
override fun getIntroducingArchiveActionCardDismissed(userId: String): Boolean? =
|
||||
@@ -274,26 +268,6 @@ class SettingsDiskSourceImpl(
|
||||
getMutableIntroducingArchiveActionCardDismissedFlow(userId = userId)
|
||||
.onSubscription { emit(getIntroducingArchiveActionCardDismissed(userId = userId)) }
|
||||
|
||||
override fun getPremiumUpgradeBannerDismissed(userId: String): Boolean? =
|
||||
getBoolean(
|
||||
key = PREMIUM_UPGRADE_BANNER_DISMISSED.appendIdentifier(identifier = userId),
|
||||
)
|
||||
|
||||
override fun storePremiumUpgradeBannerDismissed(
|
||||
userId: String,
|
||||
isDismissed: Boolean?,
|
||||
) {
|
||||
putBoolean(
|
||||
key = PREMIUM_UPGRADE_BANNER_DISMISSED.appendIdentifier(identifier = userId),
|
||||
value = isDismissed,
|
||||
)
|
||||
getMutablePremiumUpgradeBannerDismissedFlow(userId = userId).tryEmit(isDismissed)
|
||||
}
|
||||
|
||||
override fun getPremiumUpgradeBannerDismissedFlow(userId: String): Flow<Boolean?> =
|
||||
getMutablePremiumUpgradeBannerDismissedFlow(userId = userId)
|
||||
.onSubscription { emit(getPremiumUpgradeBannerDismissed(userId = userId)) }
|
||||
|
||||
override fun getAccountBiometricIntegrityValidity(
|
||||
userId: String,
|
||||
systemBioIntegrityState: String,
|
||||
@@ -638,13 +612,6 @@ class SettingsDiskSourceImpl(
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutablePremiumUpgradeBannerDismissedFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Boolean?> =
|
||||
mutablePremiumUpgradeBannerDismissedFlowMap.getOrPut(userId) {
|
||||
bufferedMutableSharedFlow(replay = 1)
|
||||
}
|
||||
|
||||
private fun getMutableLastSyncFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Instant?> =
|
||||
|
||||
@@ -19,7 +19,8 @@ class FeatureFlagManagerImpl(
|
||||
|
||||
override val sdkFeatureFlags: Map<String, Boolean>
|
||||
get() = mapOf(
|
||||
CIPHER_KEY_ENCRYPTION_KEY to serverConfigRepository.isCipherKeyEncryptionEnabled,
|
||||
CIPHER_KEY_ENCRYPTION_KEY to
|
||||
getCipherKeyEncryptionFlagState(),
|
||||
)
|
||||
|
||||
override fun <T : Any> getFeatureFlagFlow(key: FlagKey<T>): Flow<T> =
|
||||
@@ -42,13 +43,17 @@ class FeatureFlagManagerImpl(
|
||||
.serverConfigStateFlow
|
||||
.value
|
||||
.getFlagValueOrDefault(key = key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the computed value of the cipher key encryption flag based on server version.
|
||||
*/
|
||||
private val ServerConfigRepository.isCipherKeyEncryptionEnabled: Boolean
|
||||
get() = isServerVersionAtLeast(serverConfigStateFlow.value, CIPHER_KEY_ENC_MIN_SERVER_VERSION)
|
||||
/**
|
||||
* Get the computed value of the cipher key encryption flag based on server version and
|
||||
* remote flag.
|
||||
*/
|
||||
private fun getCipherKeyEncryptionFlagState() =
|
||||
isServerVersionAtLeast(
|
||||
serverConfigRepository.serverConfigStateFlow.value,
|
||||
CIPHER_KEY_ENC_MIN_SERVER_VERSION,
|
||||
) && getFeatureFlag(FlagKey.CipherKeyEncryption)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the value of a [FlagKey] from the [ServerConfig]. If there is an issue with retrieving
|
||||
|
||||
@@ -58,19 +58,19 @@ interface FirstTimeActionManager {
|
||||
val currentOrDefaultUserFirstTimeState: FirstTimeState
|
||||
|
||||
/**
|
||||
* Stores the given value for whether the active user has signaled they want to
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* set up unlock options later, during onboarding.
|
||||
*/
|
||||
fun storeShowUnlockSettingBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Stores the given value for whether the active user has signaled they want to
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* enable the browser autofill integration later, during onboarding.
|
||||
*/
|
||||
fun storeShowBrowserAutofillSettingBadge(showBadge: Boolean)
|
||||
|
||||
/**
|
||||
* Stores the given value for whether the active user has signaled they want to
|
||||
* Stores the given value for whether or not the active user has signalled they want to
|
||||
* enable autofill later, during onboarding.
|
||||
*/
|
||||
fun storeShowAutoFillSettingBadge(showBadge: Boolean)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* The minimum GMS Core version required for Credential Exchange Protocol (CXP) features.
|
||||
*/
|
||||
const val MINIMUM_CXP_GMS_VERSION: Int = 261031035
|
||||
|
||||
/**
|
||||
* Manages checks against the installed Google Mobile Services (GMS) Core version.
|
||||
*/
|
||||
interface GmsManager {
|
||||
|
||||
/**
|
||||
* Returns `true` if the installed GMS Core version is at least [version], or `false` if
|
||||
* GMS Core is not installed or does not meet the minimum version.
|
||||
*/
|
||||
fun isVersionAtLeast(version: Int): Boolean
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PremiumStatusChangedData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
|
||||
@@ -30,11 +29,6 @@ interface PushManager {
|
||||
*/
|
||||
val passwordlessRequestFlow: Flow<PasswordlessRequestData>
|
||||
|
||||
/**
|
||||
* Flow that represents Premium status change notifications.
|
||||
*/
|
||||
val premiumStatusChangedFlow: Flow<PremiumStatusChangedData>
|
||||
|
||||
/**
|
||||
* Flow that represents requests intended to trigger a sync cipher delete.
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,6 @@ import com.x8bit.bitwarden.data.platform.manager.model.NotificationLogoutData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.NotificationPayload
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.NotificationType
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PasswordlessRequestData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PremiumStatusChangedData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.PushNotificationLogOutReason
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
|
||||
@@ -64,8 +63,6 @@ class PushManagerImpl @Inject constructor(
|
||||
private val mutableLogoutSharedFlow = bufferedMutableSharedFlow<NotificationLogoutData>()
|
||||
private val mutablePasswordlessRequestSharedFlow =
|
||||
bufferedMutableSharedFlow<PasswordlessRequestData>()
|
||||
private val mutablePremiumStatusChangedSharedFlow =
|
||||
bufferedMutableSharedFlow<PremiumStatusChangedData>()
|
||||
private val mutableSyncCipherDeleteSharedFlow =
|
||||
bufferedMutableSharedFlow<SyncCipherDeleteData>()
|
||||
private val mutableSyncCipherUpsertSharedFlow =
|
||||
@@ -89,9 +86,6 @@ class PushManagerImpl @Inject constructor(
|
||||
override val passwordlessRequestFlow: SharedFlow<PasswordlessRequestData>
|
||||
get() = mutablePasswordlessRequestSharedFlow.asSharedFlow()
|
||||
|
||||
override val premiumStatusChangedFlow: SharedFlow<PremiumStatusChangedData>
|
||||
get() = mutablePremiumStatusChangedSharedFlow.asSharedFlow()
|
||||
|
||||
override val syncCipherDeleteFlow: SharedFlow<SyncCipherDeleteData>
|
||||
get() = mutableSyncCipherDeleteSharedFlow.asSharedFlow()
|
||||
|
||||
@@ -201,12 +195,6 @@ class PushManagerImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
NotificationType.POLICY_CHANGED -> {
|
||||
activeUserId?.let {
|
||||
mutableFullSyncSharedFlow.tryEmit(it)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationType.SYNC_CIPHER_DELETE,
|
||||
NotificationType.SYNC_LOGIN_DELETE,
|
||||
-> {
|
||||
@@ -317,25 +305,6 @@ class PushManagerImpl @Inject constructor(
|
||||
}
|
||||
?.let { mutableSyncSendDeleteSharedFlow.tryEmit(it) }
|
||||
}
|
||||
|
||||
NotificationType.PREMIUM_STATUS_CHANGED -> {
|
||||
json
|
||||
.decodeFromString<NotificationPayload.PremiumStatusChangedNotification>(
|
||||
string = notification.payload,
|
||||
)
|
||||
.takeIf { it.userId != null && it.isPremium != null }
|
||||
?.let { payload ->
|
||||
mutablePremiumStatusChangedSharedFlow.tryEmit(
|
||||
PremiumStatusChangedData(
|
||||
userId = requireNotNull(payload.userId),
|
||||
isPremium = requireNotNull(payload.isPremium),
|
||||
),
|
||||
)
|
||||
mutableFullSyncSharedFlow.tryEmit(
|
||||
requireNotNull(payload.userId),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* Responsible for managing whether the app review prompt should be shown.
|
||||
* Responsible for managing whether or not the app review prompt should be shown.
|
||||
*/
|
||||
interface ReviewPromptManager {
|
||||
/**
|
||||
@@ -20,7 +20,8 @@ interface ReviewPromptManager {
|
||||
fun registerCreateSendAction()
|
||||
|
||||
/**
|
||||
* Returns a boolean value indicating whether the user should be prompted to review the app.
|
||||
* Returns a boolean value indicating whether or not the user should be prompted to
|
||||
* review the app.
|
||||
*/
|
||||
fun shouldPromptForAppReview(): Boolean
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@ class SdkClientManagerImpl(
|
||||
repository = sdkRepoFactory.getServerCommunicationConfigRepository(),
|
||||
platformApi = sdkPlatformApiFactory.getServerCommunicationConfigPlatformApi(),
|
||||
)
|
||||
platform().state().registerClientManagedRepositories(
|
||||
repositories = sdkRepoFactory.getRepositories(userId = userId),
|
||||
)
|
||||
userId?.let {
|
||||
platform().state().apply {
|
||||
registerCipherRepository(sdkRepoFactory.getCipherRepository(userId = it))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) : SdkClientManager {
|
||||
|
||||
@@ -142,7 +142,7 @@ private fun getMatchingDomains(
|
||||
* @param cipherListView The cipher to be judged for a match.
|
||||
* @param resourceCacheManager The [ResourceCacheManager] for fetching cached resources.
|
||||
* @param defaultUriMatchType The global default [UriMatchType].
|
||||
* @param isAndroidApp Whether the [matchUri] belongs to an Android app.
|
||||
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
|
||||
* @param matchingDomains The set of domains that match the domain of [matchUri].
|
||||
* @param matchUri The uri that this cipher is being matched to.
|
||||
*/
|
||||
@@ -180,7 +180,7 @@ private fun checkForCipherMatch(
|
||||
*
|
||||
* @param resourceCacheManager The [ResourceCacheManager] for fetching cached resources.
|
||||
* @param defaultUriMatchType The global default [UriMatchType].
|
||||
* @param isAndroidApp Whether the [matchUri] belongs to an Android app.
|
||||
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
|
||||
* @param matchingDomains The set of domains that match the domain of [matchUri].
|
||||
* @param matchUri The uri that this [LoginUriView] is being matched to.
|
||||
*/
|
||||
|
||||
@@ -47,8 +47,6 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.GmsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.GmsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
@@ -338,12 +336,6 @@ object PlatformManagerModule {
|
||||
thirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGmsManager(
|
||||
@ApplicationContext context: Context,
|
||||
): GmsManager = GmsManagerImpl(context = context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabaseSchemeManager(
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.time.Instant
|
||||
/**
|
||||
* The payload of a push notification.
|
||||
*
|
||||
* Note: The data we receive is not always reliable, so everything is nullable, and we validate the
|
||||
* Note: The data we receive is not always reliable, so everything is nullable and we validate the
|
||||
* data in the [PushManager] as necessary.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@@ -97,13 +97,4 @@ sealed class NotificationPayload {
|
||||
data class SyncNotification(
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
* A notification payload for Premium status changes.
|
||||
*/
|
||||
@Serializable
|
||||
data class PremiumStatusChangedNotification(
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("Premium", "premium") val isPremium: Boolean?,
|
||||
) : NotificationPayload()
|
||||
}
|
||||
|
||||
@@ -60,12 +60,6 @@ enum class NotificationType {
|
||||
|
||||
@SerialName("16")
|
||||
AUTH_REQUEST_RESPONSE,
|
||||
|
||||
@SerialName("25")
|
||||
POLICY_CHANGED,
|
||||
|
||||
@SerialName("27")
|
||||
PREMIUM_STATUS_CHANGED,
|
||||
}
|
||||
|
||||
@Keep
|
||||
|
||||
@@ -135,10 +135,22 @@ sealed class OrganizationEvent {
|
||||
* folder as required by the organization's personal vault ownership policy.
|
||||
*/
|
||||
data class ItemOrganizationAccepted(
|
||||
override val cipherId: String? = null,
|
||||
override val organizationId: String,
|
||||
) : OrganizationEvent() {
|
||||
override val cipherId: String? = null
|
||||
override val type: OrganizationEventType
|
||||
get() = OrganizationEventType.ORGANIZATION_ITEM_ORGANIZATION_ACCEPTED
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks when a user chooses to leave an organization instead of migrating their personal
|
||||
* ciphers to their organization's My Items folder.
|
||||
*/
|
||||
data class ItemOrganizationDeclined(
|
||||
override val cipherId: String? = null,
|
||||
override val organizationId: String,
|
||||
) : OrganizationEvent() {
|
||||
override val type: OrganizationEventType
|
||||
get() = OrganizationEventType.ORGANIZATION_ITEM_ORGANIZATION_DECLINED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
/**
|
||||
* Data class representing a Premium status changed push notification.
|
||||
*
|
||||
* @property userId The user ID associated with the status change.
|
||||
* @property isPremium Whether Premium is now enabled.
|
||||
*/
|
||||
data class PremiumStatusChangedData(
|
||||
val userId: String,
|
||||
val isPremium: Boolean,
|
||||
)
|
||||
@@ -134,13 +134,6 @@ sealed class SpecialCircumstance : Parcelable {
|
||||
@Parcelize
|
||||
data object VerificationCodeShortcut : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched via a Premium checkout callback deep link,
|
||||
* indicating the user is returning from a Stripe checkout session.
|
||||
*/
|
||||
@Parcelize
|
||||
data object PremiumCheckoutResult : SpecialCircumstance()
|
||||
|
||||
/**
|
||||
* The app was launched to select an account to export credentials from.
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.time.Instant
|
||||
* @property cipherId The cipher ID.
|
||||
* @property revisionDate The cipher's revision date. This is used to determine if the local copy of
|
||||
* the cipher is out-of-date.
|
||||
* @property isUpdate Whether this is an update of an existing cipher.
|
||||
* @property isUpdate Whether or not this is an update of an existing cipher.
|
||||
*/
|
||||
data class SyncCipherUpsertData(
|
||||
val userId: String,
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.time.Instant
|
||||
* @property folderId The folder ID.
|
||||
* @property revisionDate The folder's revision date. This is used to determine if the local copy of
|
||||
* the folder is out-of-date.
|
||||
* @property isUpdate Whether this is an update of an existing folder.
|
||||
* @property isUpdate Whether or not this is an update of an existing folder.
|
||||
*/
|
||||
data class SyncFolderUpsertData(
|
||||
val userId: String,
|
||||
|
||||
@@ -8,8 +8,8 @@ import java.time.Instant
|
||||
* @property userId The user ID associated with this update.
|
||||
* @property sendId The send ID.
|
||||
* @property revisionDate The send's revision date. This is used to determine if the local copy of
|
||||
* the Send is out-of-date.
|
||||
* @property isUpdate Whether this is an update of an existing send.
|
||||
* the send is out-of-date.
|
||||
* @property isUpdate Whether or not this is an update of an existing send.
|
||||
*/
|
||||
data class SyncSendUpsertData(
|
||||
val userId: String,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk
|
||||
|
||||
import com.bitwarden.core.ClientManagedTokens
|
||||
import com.bitwarden.sdk.Repositories
|
||||
import com.bitwarden.sdk.CipherRepository
|
||||
import com.bitwarden.sdk.ServerCommunicationConfigRepository
|
||||
|
||||
/**
|
||||
@@ -9,9 +9,9 @@ import com.bitwarden.sdk.ServerCommunicationConfigRepository
|
||||
*/
|
||||
interface SdkRepositoryFactory {
|
||||
/**
|
||||
* Retrieves or creates a [Repositories] for use with the Bitwarden SDK.
|
||||
* Retrieves or creates a [CipherRepository] for use with the Bitwarden SDK.
|
||||
*/
|
||||
fun getRepositories(userId: String?): Repositories
|
||||
fun getCipherRepository(userId: String): CipherRepository
|
||||
|
||||
/**
|
||||
* Retrieves or creates a [ClientManagedTokens] for use with the Bitwarden SDK.
|
||||
|
||||
@@ -2,12 +2,11 @@ package com.x8bit.bitwarden.data.platform.manager.sdk
|
||||
|
||||
import com.bitwarden.core.ClientManagedTokens
|
||||
import com.bitwarden.data.datasource.disk.ConfigDiskSource
|
||||
import com.bitwarden.sdk.Repositories
|
||||
import com.bitwarden.sdk.CipherRepository
|
||||
import com.bitwarden.sdk.ServerCommunicationConfigRepository
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.CookieDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkCipherRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkLocalUserDataKeyStateRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkTokenRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.ServerCommunicationConfigRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
@@ -21,15 +20,12 @@ class SdkRepositoryFactoryImpl(
|
||||
private val configDiskSource: ConfigDiskSource,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
) : SdkRepositoryFactory {
|
||||
override fun getRepositories(userId: String?): Repositories =
|
||||
Repositories(
|
||||
cipher = getSdkCipherRepository(userId = userId),
|
||||
folder = null,
|
||||
userKeyState = null,
|
||||
localUserDataKeyState = SdkLocalUserDataKeyStateRepository(
|
||||
authDiskSource = authDiskSource,
|
||||
),
|
||||
ephemeralPinEnvelopeState = null,
|
||||
override fun getCipherRepository(
|
||||
userId: String,
|
||||
): CipherRepository =
|
||||
SdkCipherRepository(
|
||||
userId = userId,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
)
|
||||
|
||||
override fun getClientManagedTokens(
|
||||
@@ -45,10 +41,4 @@ class SdkRepositoryFactoryImpl(
|
||||
cookieDiskSource = cookieDiskSource,
|
||||
configDiskSource = configDiskSource,
|
||||
)
|
||||
|
||||
private fun getSdkCipherRepository(
|
||||
userId: String?,
|
||||
): SdkCipherRepository? = userId?.let {
|
||||
SdkCipherRepository(userId = it, vaultDiskSource = vaultDiskSource)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,33 +40,4 @@ class SdkCipherRepository(
|
||||
cipher = value.toEncryptedNetworkCipherResponse(encryptedFor = userId),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setBulk(values: Map<String, Cipher>) {
|
||||
val validEntries = values.filter { (id, cipher) ->
|
||||
if (id != cipher.id) {
|
||||
Timber.e(
|
||||
"SDK Cipher 'setBulk' operation: ID's do not match for '$id'",
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
if (validEntries.isEmpty()) return
|
||||
vaultDiskSource.saveCiphers(
|
||||
userId = userId,
|
||||
ciphers = validEntries.values.map {
|
||||
it.toEncryptedNetworkCipherResponse(encryptedFor = userId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removeBulk(keys: List<String>) {
|
||||
if (keys.isEmpty()) return
|
||||
vaultDiskSource.deleteSelectedCiphers(userId = userId, cipherIds = keys)
|
||||
}
|
||||
|
||||
override suspend fun removeAll() {
|
||||
vaultDiskSource.deleteAllCiphers(userId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk.repository
|
||||
|
||||
import com.bitwarden.sdk.FolderRepository
|
||||
import com.bitwarden.vault.Folder
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolderResponse
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A user-scoped implementation of a Bitwarden SDK [FolderRepository].
|
||||
*/
|
||||
class SdkFolderRepository(
|
||||
private val userId: String,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
) : FolderRepository {
|
||||
override suspend fun get(id: String): Folder? =
|
||||
vaultDiskSource
|
||||
.getFolder(userId = userId, folderId = id)
|
||||
?.toEncryptedSdkFolder()
|
||||
|
||||
override suspend fun list(): List<Folder> =
|
||||
vaultDiskSource
|
||||
.getFolders(userId = userId)
|
||||
.map { it.toEncryptedSdkFolder() }
|
||||
|
||||
override suspend fun set(id: String, value: Folder) {
|
||||
if (id != value.id) {
|
||||
Timber.e("SDK Folder 'set' operation: ID's do not match")
|
||||
return
|
||||
}
|
||||
vaultDiskSource.saveFolder(
|
||||
userId = userId,
|
||||
folder = value.toEncryptedNetworkFolderResponse(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setBulk(values: Map<String, Folder>) {
|
||||
val validEntries = values.filter { (id, cipher) ->
|
||||
if (id != cipher.id) {
|
||||
Timber.e("SDK Folder 'setBulk' operation: ID's do not match for '$id'")
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
if (validEntries.isEmpty()) return
|
||||
vaultDiskSource.saveFolders(
|
||||
userId = userId,
|
||||
folders = validEntries.values.map { it.toEncryptedNetworkFolderResponse() },
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun remove(id: String) {
|
||||
vaultDiskSource.deleteFolder(userId = userId, folderId = id)
|
||||
}
|
||||
|
||||
override suspend fun removeBulk(keys: List<String>) {
|
||||
if (keys.isEmpty()) return
|
||||
vaultDiskSource.deleteSelectedFolders(userId = userId, folderIds = keys)
|
||||
}
|
||||
|
||||
override suspend fun removeAll() {
|
||||
vaultDiskSource.deleteAllFolders(userId = userId)
|
||||
}
|
||||
|
||||
override suspend fun has(id: String): Boolean = this.get(id = id) != null
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager.sdk.repository
|
||||
|
||||
import com.bitwarden.core.LocalUserDataKeyState
|
||||
import com.bitwarden.sdk.LocalUserDataKeyStateRepository
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
|
||||
/**
|
||||
* An implementation of a Bitwarden SDK [LocalUserDataKeyStateRepository].
|
||||
*/
|
||||
class SdkLocalUserDataKeyStateRepository(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
) : LocalUserDataKeyStateRepository {
|
||||
override suspend fun get(id: String): LocalUserDataKeyState? {
|
||||
return authDiskSource
|
||||
.getLocalUserDataKey(userId = id)
|
||||
?.let { LocalUserDataKeyState(wrappedKey = it) }
|
||||
}
|
||||
|
||||
override suspend fun has(
|
||||
id: String,
|
||||
): Boolean = authDiskSource.getLocalUserDataKey(userId = id) != null
|
||||
|
||||
override suspend fun list(): List<LocalUserDataKeyState> =
|
||||
authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.mapNotNull { get(id = it.key) }
|
||||
.orEmpty()
|
||||
|
||||
override suspend fun remove(id: String) {
|
||||
authDiskSource.storeLocalUserDataKey(userId = id, wrappedKey = null)
|
||||
}
|
||||
|
||||
override suspend fun removeAll() {
|
||||
removeBulk(keys = authDiskSource.userState?.accounts.orEmpty().keys.toList())
|
||||
}
|
||||
|
||||
override suspend fun removeBulk(keys: List<String>) {
|
||||
keys.forEach { remove(id = it) }
|
||||
}
|
||||
|
||||
override suspend fun set(id: String, value: LocalUserDataKeyState) {
|
||||
authDiskSource.storeLocalUserDataKey(userId = id, value.wrappedKey)
|
||||
}
|
||||
|
||||
override suspend fun setBulk(values: Map<String, LocalUserDataKeyState>) {
|
||||
values.forEach { (id, value) -> set(id = id, value = value) }
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun getSharedAccounts(): SharedAccountData {
|
||||
return authDiskSource
|
||||
.userState
|
||||
@@ -89,7 +88,7 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
}
|
||||
|
||||
// Vault is unlocked, query vault disk source for totp logins:
|
||||
val cipherData = vaultDiskSource
|
||||
val totpUris = vaultDiskSource
|
||||
.getTotpCiphers(userId = userId)
|
||||
// Filter out any deleted and archived ciphers.
|
||||
.filter { it.deletedDate == null && it.archivedDate == null }
|
||||
@@ -98,23 +97,10 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
.decryptCipher(userId = userId, cipher = it.toEncryptedSdkCipher())
|
||||
.getOrNull()
|
||||
?.let { decryptedCipher ->
|
||||
val cipherId = decryptedCipher.id ?: return@let null
|
||||
val rawTotp = decryptedCipher.login?.totp
|
||||
val cipherName = decryptedCipher.name
|
||||
val username = decryptedCipher.login?.username
|
||||
decryptedCipher.login?.totp?.let { rawTotp ->
|
||||
SharedAccountData.CipherData(
|
||||
uri = rawTotp,
|
||||
// TODO: PM-34085 Remove the legacyUri.
|
||||
legacyUri = rawTotp.sanitizeTotpUri(
|
||||
issuer = cipherName,
|
||||
username = username,
|
||||
),
|
||||
id = cipherId,
|
||||
name = cipherName,
|
||||
username = username,
|
||||
isFavorite = decryptedCipher.favorite,
|
||||
)
|
||||
}
|
||||
rawTotp.sanitizeTotpUri(issuer = cipherName, username = username)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +116,7 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
.environmentUrlData
|
||||
.toEnvironmentUrlsOrDefault()
|
||||
.label,
|
||||
cipherData = cipherData,
|
||||
totpUris = totpUris,
|
||||
)
|
||||
}
|
||||
.let(::SharedAccountData)
|
||||
@@ -170,7 +156,6 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
),
|
||||
upgradeToken = null,
|
||||
),
|
||||
)
|
||||
.flatMap { result ->
|
||||
|
||||
@@ -54,9 +54,4 @@ interface DebugMenuRepository {
|
||||
* Clears all stored SSO cookie configurations.
|
||||
*/
|
||||
fun clearSsoCookies()
|
||||
|
||||
/**
|
||||
* Resets the Premium upgrade banner dismiss status for the current user.
|
||||
*/
|
||||
fun resetPremiumUpgradeBannerDismiss()
|
||||
}
|
||||
|
||||
@@ -74,12 +74,4 @@ class DebugMenuRepositoryImpl(
|
||||
override fun clearSsoCookies() {
|
||||
cookieDiskSource.clearCookies()
|
||||
}
|
||||
|
||||
override fun resetPremiumUpgradeBannerDismiss() {
|
||||
val currentUserId = authDiskSource.userState?.activeUserId ?: return
|
||||
settingsDiskSource.storePremiumUpgradeBannerDismissed(
|
||||
userId = currentUserId,
|
||||
isDismissed = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ interface SettingsRepository : FlightRecorderManager {
|
||||
var defaultUriMatchType: UriMatchType
|
||||
|
||||
/**
|
||||
* Whether biometric unlocking is enabled for the current user.
|
||||
* Whether or not biometric unlocking is enabled for the current user.
|
||||
*/
|
||||
val isUnlockWithBiometricsEnabled: Boolean
|
||||
|
||||
@@ -131,7 +131,7 @@ interface SettingsRepository : FlightRecorderManager {
|
||||
val isUnlockWithBiometricsEnabledFlow: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Whether PIN unlocking is enabled for the current user.
|
||||
* Whether or not PIN unlocking is enabled for the current user.
|
||||
*/
|
||||
val isUnlockWithPinEnabled: Boolean
|
||||
|
||||
@@ -141,17 +141,17 @@ interface SettingsRepository : FlightRecorderManager {
|
||||
val isUnlockWithPinEnabledFlow: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Whether inline autofill is enabled for the current user.
|
||||
* Whether or not inline autofill is enabled for the current user.
|
||||
*/
|
||||
var isInlineAutofillEnabled: Boolean
|
||||
|
||||
/**
|
||||
* Whether the auto copying totp when autofilling is disabled for the current user.
|
||||
* Whether or not the auto copying totp when autofilling is disabled for the current user.
|
||||
*/
|
||||
var isAutoCopyTotpDisabled: Boolean
|
||||
|
||||
/**
|
||||
* Whether the autofill save prompt is disabled for the current user.
|
||||
* Whether or not the autofill save prompt is disabled for the current user.
|
||||
*/
|
||||
var isAutofillSavePromptDisabled: Boolean
|
||||
|
||||
@@ -178,12 +178,12 @@ interface SettingsRepository : FlightRecorderManager {
|
||||
val isAutofillEnabledStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Sets whether screen capture is allowed for the current user.
|
||||
* Sets whether or not screen capture is allowed for the current user.
|
||||
*/
|
||||
var isScreenCaptureAllowed: Boolean
|
||||
|
||||
/**
|
||||
* Whether screen capture is allowed for the current user.
|
||||
* Whether or not screen capture is allowed for the current user.
|
||||
*/
|
||||
val isScreenCaptureAllowedStateFlow: StateFlow<Boolean>
|
||||
|
||||
@@ -252,16 +252,6 @@ interface SettingsRepository : FlightRecorderManager {
|
||||
*/
|
||||
fun dismissIntroducingArchiveActionCard()
|
||||
|
||||
/**
|
||||
* Gets updates for whether the Premium upgrade banner is dismissed.
|
||||
*/
|
||||
fun getPremiumUpgradeBannerDismissedFlow(): StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Stores that the Premium upgrade banner has been dismissed for the active user.
|
||||
*/
|
||||
fun dismissPremiumUpgradeBanner()
|
||||
|
||||
/**
|
||||
* Stores the encrypted user key for biometrics, allowing it to be used to unlock the current
|
||||
* user's vault.
|
||||
|
||||
@@ -523,29 +523,6 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPremiumUpgradeBannerDismissedFlow(): StateFlow<Boolean> {
|
||||
val userId = activeUserId ?: return MutableStateFlow(value = false)
|
||||
return settingsDiskSource
|
||||
.getPremiumUpgradeBannerDismissedFlow(userId = userId)
|
||||
.map { it ?: false }
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = settingsDiskSource
|
||||
.getPremiumUpgradeBannerDismissed(userId = userId)
|
||||
?: false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun dismissPremiumUpgradeBanner() {
|
||||
activeUserId?.let {
|
||||
settingsDiskSource.storePremiumUpgradeBannerDismissed(
|
||||
userId = it,
|
||||
isDismissed = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult {
|
||||
val userId = activeUserId
|
||||
?: return BiometricsKeyResult.Error(error = NoActiveUserException())
|
||||
|
||||
@@ -60,9 +60,7 @@ fun URI.addSchemeToUriIfNecessary(): URI {
|
||||
// provided that scheme does not exist already.
|
||||
!uriString.hasHttpProtocol()
|
||||
) {
|
||||
"https://$uriString"
|
||||
.toUriOrNull()
|
||||
?: this
|
||||
URI("https://$uriString")
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
@@ -57,21 +57,6 @@ interface VaultDiskSource {
|
||||
*/
|
||||
suspend fun deleteCipher(userId: String, cipherId: String)
|
||||
|
||||
/**
|
||||
* Saves multiple ciphers to the data source for the given [userId].
|
||||
*/
|
||||
suspend fun saveCiphers(userId: String, ciphers: List<SyncResponseJson.Cipher>)
|
||||
|
||||
/**
|
||||
* Deletes ciphers with the given [cipherIds] from the data source for the given [userId].
|
||||
*/
|
||||
suspend fun deleteSelectedCiphers(userId: String, cipherIds: List<String>)
|
||||
|
||||
/**
|
||||
* Deletes all ciphers from the data source for the given [userId].
|
||||
*/
|
||||
suspend fun deleteAllCiphers(userId: String)
|
||||
|
||||
/**
|
||||
* Saves a collection to the data source for the given [userId].
|
||||
*/
|
||||
@@ -80,79 +65,54 @@ interface VaultDiskSource {
|
||||
/**
|
||||
* Retrieves all collections from the data source for a given [userId].
|
||||
*/
|
||||
fun getCollectionsFlow(userId: String): Flow<List<SyncResponseJson.Collection>>
|
||||
fun getCollections(userId: String): Flow<List<SyncResponseJson.Collection>>
|
||||
|
||||
/**
|
||||
* Retrieves all domains from the data source for a given [userId].
|
||||
*/
|
||||
fun getDomainsFlow(userId: String): Flow<SyncResponseJson.Domains?>
|
||||
fun getDomains(userId: String): Flow<SyncResponseJson.Domains?>
|
||||
|
||||
/**
|
||||
* Deletes a folder from the data source for the given [userId] and [folderId].
|
||||
*/
|
||||
suspend fun deleteFolder(userId: String, folderId: String)
|
||||
|
||||
/**
|
||||
* Deletes folders with the given [folderIds] from the data source for the given [userId].
|
||||
*/
|
||||
suspend fun deleteSelectedFolders(userId: String, folderIds: List<String>)
|
||||
|
||||
/**
|
||||
* Deletes all folders from the data source for the given [userId].
|
||||
*/
|
||||
suspend fun deleteAllFolders(userId: String)
|
||||
|
||||
/**
|
||||
* Saves a folder to the data source for the given [userId].
|
||||
*/
|
||||
suspend fun saveFolder(userId: String, folder: SyncResponseJson.Folder)
|
||||
|
||||
/**
|
||||
* Saves multiple folders to the data source for the given [userId].
|
||||
*/
|
||||
suspend fun saveFolders(userId: String, folders: List<SyncResponseJson.Folder>)
|
||||
|
||||
/**
|
||||
* Retrieves a folder from the data source for a given [userId] and [folderId].
|
||||
*/
|
||||
suspend fun getFolder(userId: String, folderId: String): SyncResponseJson.Folder?
|
||||
|
||||
/**
|
||||
* Retrieves all folders from the data source for a given [userId].
|
||||
*/
|
||||
suspend fun getFolders(userId: String): List<SyncResponseJson.Folder>
|
||||
fun getFolders(userId: String): Flow<List<SyncResponseJson.Folder>>
|
||||
|
||||
/**
|
||||
* Retrieves all folders from the data source for a given [userId].
|
||||
*/
|
||||
fun getFoldersFlow(userId: String): Flow<List<SyncResponseJson.Folder>>
|
||||
|
||||
/**
|
||||
* Saves a Send to the data source for the given [userId].
|
||||
* Saves a send to the data source for the given [userId].
|
||||
*/
|
||||
suspend fun saveSend(userId: String, send: SyncResponseJson.Send)
|
||||
|
||||
/**
|
||||
* Deletes a Send from the data source for the given [userId] and [sendId].
|
||||
* Deletes a send from the data source for the given [userId] and [sendId].
|
||||
*/
|
||||
suspend fun deleteSend(userId: String, sendId: String)
|
||||
|
||||
/**
|
||||
* Retrieves all sends from the data source for a given [userId].
|
||||
*/
|
||||
fun getSendsFlow(userId: String): Flow<List<SyncResponseJson.Send>>
|
||||
fun getSends(userId: String): Flow<List<SyncResponseJson.Send>>
|
||||
|
||||
/**
|
||||
* Replaces all [vault] data for a given [userId] with the new `vault`.
|
||||
*
|
||||
* This will always cause the [getCiphersFlow], [getCollectionsFlow], and [getFoldersFlow]
|
||||
* functions to re-emit even if the underlying data has not changed.
|
||||
* This will always cause the [getCiphersFlow], [getCollections], and [getFolders] functions to
|
||||
* re-emit even if the underlying data has not changed.
|
||||
*/
|
||||
suspend fun replaceVaultData(userId: String, vault: SyncResponseJson)
|
||||
|
||||
/**
|
||||
* Trigger re-emissions from the [getCiphersFlow], [getCollectionsFlow], [getFoldersFlow],
|
||||
* and [getSendsFlow] functions.
|
||||
* Trigger re-emissions from the [getCiphersFlow], [getCollections], [getFolders], and [getSends]
|
||||
* functions.
|
||||
*/
|
||||
suspend fun resyncVaultData(userId: String)
|
||||
|
||||
|
||||
@@ -157,32 +157,6 @@ class VaultDiskSourceImpl(
|
||||
ciphersDao.deleteCipher(userId, cipherId)
|
||||
}
|
||||
|
||||
override suspend fun saveCiphers(
|
||||
userId: String,
|
||||
ciphers: List<SyncResponseJson.Cipher>,
|
||||
) {
|
||||
ciphersDao.insertCiphers(
|
||||
ciphers = ciphers.map { cipher ->
|
||||
CipherEntity(
|
||||
id = cipher.id,
|
||||
userId = userId,
|
||||
hasTotp = cipher.login?.totp != null,
|
||||
cipherType = json.encodeToString(cipher.type),
|
||||
cipherJson = json.encodeToString(cipher),
|
||||
organizationId = cipher.organizationId,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteSelectedCiphers(userId: String, cipherIds: List<String>) {
|
||||
ciphersDao.deleteSelectedCiphers(userId = userId, cipherIds = cipherIds)
|
||||
}
|
||||
|
||||
override suspend fun deleteAllCiphers(userId: String) {
|
||||
ciphersDao.deleteAllCiphers(userId = userId)
|
||||
}
|
||||
|
||||
override suspend fun saveCollection(userId: String, collection: SyncResponseJson.Collection) {
|
||||
collectionsDao.insertCollection(
|
||||
collection = CollectionEntity(
|
||||
@@ -200,13 +174,13 @@ class VaultDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCollectionsFlow(
|
||||
override fun getCollections(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Collection>> =
|
||||
merge(
|
||||
forceCollectionsFlow,
|
||||
collectionsDao
|
||||
.getAllCollectionsFlow(userId = userId)
|
||||
.getAllCollections(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
SyncResponseJson.Collection(
|
||||
@@ -224,9 +198,9 @@ class VaultDiskSourceImpl(
|
||||
},
|
||||
)
|
||||
|
||||
override fun getDomainsFlow(userId: String): Flow<SyncResponseJson.Domains?> =
|
||||
override fun getDomains(userId: String): Flow<SyncResponseJson.Domains?> =
|
||||
domainsDao
|
||||
.getDomainsFlow(userId)
|
||||
.getDomains(userId)
|
||||
.map { entity ->
|
||||
withContext(dispatcherManager.default) {
|
||||
entity?.domainsJson?.let { domains ->
|
||||
@@ -241,14 +215,6 @@ class VaultDiskSourceImpl(
|
||||
foldersDao.deleteFolder(userId = userId, folderId = folderId)
|
||||
}
|
||||
|
||||
override suspend fun deleteSelectedFolders(userId: String, folderIds: List<String>) {
|
||||
foldersDao.deleteSelectedFolders(userId = userId, folderIds = folderIds)
|
||||
}
|
||||
|
||||
override suspend fun deleteAllFolders(userId: String) {
|
||||
foldersDao.deleteAllFolders(userId = userId)
|
||||
}
|
||||
|
||||
override suspend fun saveFolder(userId: String, folder: SyncResponseJson.Folder) {
|
||||
foldersDao.insertFolder(
|
||||
folder = FolderEntity(
|
||||
@@ -260,51 +226,13 @@ class VaultDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun saveFolders(userId: String, folders: List<SyncResponseJson.Folder>) {
|
||||
foldersDao.insertFolders(
|
||||
folders = folders.map { folder ->
|
||||
FolderEntity(
|
||||
id = folder.id,
|
||||
userId = userId,
|
||||
name = folder.name,
|
||||
revisionDate = folder.revisionDate,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getFolder(
|
||||
userId: String,
|
||||
folderId: String,
|
||||
): SyncResponseJson.Folder? =
|
||||
foldersDao
|
||||
.getFolder(userId = userId, folderId = folderId)
|
||||
?.let { folder ->
|
||||
SyncResponseJson.Folder(
|
||||
id = folder.id,
|
||||
name = folder.name,
|
||||
revisionDate = folder.revisionDate,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getFolders(userId: String): List<SyncResponseJson.Folder> =
|
||||
foldersDao
|
||||
.getAllFolders(userId = userId)
|
||||
.map { folder ->
|
||||
SyncResponseJson.Folder(
|
||||
id = folder.id,
|
||||
name = folder.name,
|
||||
revisionDate = folder.revisionDate,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFoldersFlow(
|
||||
override fun getFolders(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Folder>> =
|
||||
merge(
|
||||
forceFolderFlow,
|
||||
foldersDao
|
||||
.getAllFoldersFlow(userId = userId)
|
||||
.getAllFolders(userId = userId)
|
||||
.map { entities ->
|
||||
entities.map { entity ->
|
||||
SyncResponseJson.Folder(
|
||||
@@ -333,13 +261,13 @@ class VaultDiskSourceImpl(
|
||||
sendsDao.deleteSend(userId, sendId)
|
||||
}
|
||||
|
||||
override fun getSendsFlow(
|
||||
override fun getSends(
|
||||
userId: String,
|
||||
): Flow<List<SyncResponseJson.Send>> =
|
||||
merge(
|
||||
forceSendFlow,
|
||||
sendsDao
|
||||
.getAllSendsFlow(userId = userId)
|
||||
.getAllSends(userId = userId)
|
||||
.map { entities ->
|
||||
withContext(context = dispatcherManager.default) {
|
||||
entities
|
||||
@@ -449,9 +377,9 @@ class VaultDiskSourceImpl(
|
||||
override suspend fun resyncVaultData(userId: String) {
|
||||
coroutineScope {
|
||||
val deferredCiphers = async { getCiphersFlow(userId = userId).first() }
|
||||
val deferredCollections = async { getCollectionsFlow(userId = userId).first() }
|
||||
val deferredFolders = async { getFoldersFlow(userId = userId).first() }
|
||||
val deferredSends = async { getSendsFlow(userId = userId).first() }
|
||||
val deferredCollections = async { getCollections(userId = userId).first() }
|
||||
val deferredFolders = async { getFolders(userId = userId).first() }
|
||||
val deferredSends = async { getSends(userId = userId).first() }
|
||||
|
||||
forceCiphersFlow.tryEmit(deferredCiphers.await())
|
||||
forceCollectionsFlow.tryEmit(deferredCollections.await())
|
||||
|
||||
@@ -12,7 +12,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
* Provides methods for inserting, retrieving, and deleting ciphers from the database using the
|
||||
* [CipherEntity].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Dao
|
||||
interface CiphersDao {
|
||||
|
||||
@@ -78,13 +77,6 @@ interface CiphersDao {
|
||||
@Query("DELETE FROM ciphers WHERE user_id = :userId AND id = :cipherId")
|
||||
suspend fun deleteCipher(userId: String, cipherId: String): Int
|
||||
|
||||
/**
|
||||
* Deletes the stored ciphers associated with the given [userId] whose IDs are in [cipherIds].
|
||||
* This will return the number of rows deleted by this query.
|
||||
*/
|
||||
@Query("DELETE FROM ciphers WHERE user_id = :userId AND id IN (:cipherIds)")
|
||||
suspend fun deleteSelectedCiphers(userId: String, cipherIds: List<String>): Int
|
||||
|
||||
/**
|
||||
* Deletes all the stored ciphers associated with the given [userId] and then add all new
|
||||
* [ciphers] to the database. This will return `true` if any changes were made to the database
|
||||
|
||||
@@ -31,7 +31,7 @@ interface CollectionsDao {
|
||||
* Retrieves all collections from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM collections WHERE user_id = :userId")
|
||||
fun getAllCollectionsFlow(userId: String): Flow<List<CollectionEntity>>
|
||||
fun getAllCollections(userId: String): Flow<List<CollectionEntity>>
|
||||
|
||||
/**
|
||||
* Deletes all the stored collections associated with the given [userId]. This will return the
|
||||
|
||||
@@ -23,7 +23,7 @@ interface DomainsDao {
|
||||
* Retrieves domains from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM domains WHERE user_id = :userId")
|
||||
fun getDomainsFlow(
|
||||
fun getDomains(
|
||||
userId: String,
|
||||
): Flow<DomainsEntity?>
|
||||
|
||||
|
||||
@@ -27,37 +27,13 @@ interface FoldersDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertFolder(folder: FolderEntity)
|
||||
|
||||
/**
|
||||
* Retrieves all folders from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM folders WHERE user_id = :userId")
|
||||
fun getAllFoldersFlow(
|
||||
userId: String,
|
||||
): Flow<List<FolderEntity>>
|
||||
|
||||
/**
|
||||
* Retrieves all folders from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM folders WHERE user_id = :userId")
|
||||
fun getAllFolders(
|
||||
userId: String,
|
||||
): List<FolderEntity>
|
||||
|
||||
/**
|
||||
* Retrieves a folder from the database for a given [userId] and [folderId].
|
||||
*/
|
||||
@Query("SELECT * FROM folders WHERE user_id = :userId AND id = :folderId LIMIT 1")
|
||||
suspend fun getFolder(
|
||||
userId: String,
|
||||
folderId: String,
|
||||
): FolderEntity?
|
||||
|
||||
/**
|
||||
* Deletes the stored folders associated with the given [userId] whose IDs are in [folderIds].
|
||||
* This will return the number of rows deleted by this query.
|
||||
*/
|
||||
@Query("DELETE FROM folders WHERE user_id = :userId AND id IN (:folderIds)")
|
||||
suspend fun deleteSelectedFolders(userId: String, folderIds: List<String>): Int
|
||||
): Flow<List<FolderEntity>>
|
||||
|
||||
/**
|
||||
* Deletes all the stored folders associated with the given [userId]. This will return the
|
||||
|
||||
@@ -25,7 +25,7 @@ interface SendsDao {
|
||||
* Retrieves all sends from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM sends WHERE user_id = :userId")
|
||||
fun getAllSendsFlow(
|
||||
fun getAllSends(
|
||||
userId: String,
|
||||
): Flow<List<SendEntity>>
|
||||
|
||||
|
||||
@@ -114,10 +114,7 @@ class Fido2CredentialStoreImpl(
|
||||
private fun UpdateCipherResult.toCreateCipherResult(): CreateCipherResult =
|
||||
when (this) {
|
||||
UpdateCipherResult.Success -> CreateCipherResult.Success
|
||||
is UpdateCipherResult.Error -> CreateCipherResult.Error(
|
||||
error = error,
|
||||
errorMessage = errorMessage,
|
||||
)
|
||||
is UpdateCipherResult.Error -> CreateCipherResult.Error(errorMessage, error)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -85,7 +85,10 @@ class CipherManagerImpl(
|
||||
|
||||
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateCipherResult.Error(error = NoActiveUserException())
|
||||
?: return CreateCipherResult.Error(
|
||||
error = NoActiveUserException(),
|
||||
errorMessage = null,
|
||||
)
|
||||
return vaultSdkSource
|
||||
.encryptCipher(
|
||||
userId = userId,
|
||||
@@ -108,7 +111,7 @@ class CipherManagerImpl(
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateCipherResult.Error(error = it) },
|
||||
onFailure = { CreateCipherResult.Error(errorMessage = null, error = it) },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerAddCipherAction()
|
||||
it
|
||||
@@ -121,7 +124,7 @@ class CipherManagerImpl(
|
||||
collectionIds: List<String>,
|
||||
): CreateCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateCipherResult.Error(error = NoActiveUserException())
|
||||
?: return CreateCipherResult.Error(errorMessage = null, error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.encryptCipher(
|
||||
userId = userId,
|
||||
@@ -154,7 +157,7 @@ class CipherManagerImpl(
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { CreateCipherResult.Error(error = it) },
|
||||
onFailure = { CreateCipherResult.Error(errorMessage = null, error = it) },
|
||||
onSuccess = {
|
||||
reviewPromptManager.registerAddCipherAction()
|
||||
it
|
||||
@@ -166,8 +169,7 @@ class CipherManagerImpl(
|
||||
cipherId: String,
|
||||
cipherView: CipherView,
|
||||
): ArchiveCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return ArchiveCipherResult.Error(error = NoActiveUserException())
|
||||
val userId = activeUserId ?: return ArchiveCipherResult.Error(NoActiveUserException())
|
||||
return ciphersService
|
||||
.archiveCipher(cipherId = cipherId)
|
||||
.flatMap { response ->
|
||||
@@ -202,8 +204,7 @@ class CipherManagerImpl(
|
||||
cipherId: String,
|
||||
cipherView: CipherView,
|
||||
): UnarchiveCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return UnarchiveCipherResult.Error(error = NoActiveUserException())
|
||||
val userId = activeUserId ?: return UnarchiveCipherResult.Error(NoActiveUserException())
|
||||
return ciphersService
|
||||
.unarchiveCipher(cipherId = cipherId)
|
||||
.flatMap { response ->
|
||||
@@ -367,7 +368,7 @@ class CipherManagerImpl(
|
||||
cipherView: CipherView,
|
||||
): UpdateCipherResult {
|
||||
val userId = activeUserId
|
||||
?: return UpdateCipherResult.Error(error = NoActiveUserException())
|
||||
?: return UpdateCipherResult.Error(errorMessage = null, error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.encryptCipher(
|
||||
userId = userId,
|
||||
@@ -398,7 +399,7 @@ class CipherManagerImpl(
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { UpdateCipherResult.Error(error = it) },
|
||||
onFailure = { UpdateCipherResult.Error(errorMessage = null, error = it) },
|
||||
onSuccess = { it },
|
||||
)
|
||||
}
|
||||
@@ -801,7 +802,7 @@ class CipherManagerImpl(
|
||||
if (!shouldUpdate && shouldCheckCollections && organizationId != null) {
|
||||
// Check if there are any collections in common
|
||||
shouldUpdate = vaultDiskSource
|
||||
.getCollectionsFlow(userId = userId)
|
||||
.getCollections(userId = userId)
|
||||
.first()
|
||||
.any { collectionIds?.contains(it.id) == true }
|
||||
}
|
||||
|
||||
@@ -53,8 +53,7 @@ class FolderManagerImpl(
|
||||
}
|
||||
|
||||
override suspend fun createFolder(folderView: FolderView): CreateFolderResult {
|
||||
val userId = activeUserId
|
||||
?: return CreateFolderResult.Error(error = NoActiveUserException())
|
||||
val userId = activeUserId ?: return CreateFolderResult.Error(NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.encryptFolder(userId = userId, folder = folderView)
|
||||
.flatMap { folderService.createFolder(body = it.toEncryptedNetworkFolder()) }
|
||||
@@ -69,8 +68,7 @@ class FolderManagerImpl(
|
||||
}
|
||||
|
||||
override suspend fun deleteFolder(folderId: String): DeleteFolderResult {
|
||||
val userId = activeUserId
|
||||
?: return DeleteFolderResult.Error(error = NoActiveUserException())
|
||||
val userId = activeUserId ?: return DeleteFolderResult.Error(NoActiveUserException())
|
||||
return folderService
|
||||
.deleteFolder(folderId = folderId)
|
||||
.onSuccess {
|
||||
@@ -87,8 +85,10 @@ class FolderManagerImpl(
|
||||
folderId: String,
|
||||
folderView: FolderView,
|
||||
): UpdateFolderResult {
|
||||
val userId = activeUserId
|
||||
?: return UpdateFolderResult.Error(error = NoActiveUserException())
|
||||
val userId = activeUserId ?: return UpdateFolderResult.Error(
|
||||
errorMessage = null,
|
||||
error = NoActiveUserException(),
|
||||
)
|
||||
return vaultSdkSource
|
||||
.encryptFolder(userId = userId, folder = folderView)
|
||||
.flatMap { folder ->
|
||||
@@ -110,7 +110,7 @@ class FolderManagerImpl(
|
||||
.fold(
|
||||
onSuccess = { UpdateFolderResult.Success(it) },
|
||||
onFailure = {
|
||||
UpdateFolderResult.Error(error = it)
|
||||
UpdateFolderResult.Error(errorMessage = null, error = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ class FolderManagerImpl(
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { UpdateFolderResult.Error(error = it) },
|
||||
onFailure = { UpdateFolderResult.Error(it.message, error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ class FolderManagerImpl(
|
||||
val isUpdate = syncFolderUpsertData.isUpdate
|
||||
val revisionDate = syncFolderUpsertData.revisionDate
|
||||
val localFolder = vaultDiskSource
|
||||
.getFoldersFlow(userId = userId)
|
||||
.getFolders(userId = userId)
|
||||
.first()
|
||||
.find { it.id == folderId }
|
||||
val isValidCreate = !isUpdate && localFolder == null
|
||||
|
||||
@@ -273,7 +273,7 @@ class SendManagerImpl(
|
||||
val isUpdate = syncSendUpsertData.isUpdate
|
||||
val revisionDate = syncSendUpsertData.revisionDate
|
||||
val localSend = vaultDiskSource
|
||||
.getSendsFlow(userId = userId)
|
||||
.getSends(userId = userId)
|
||||
.first()
|
||||
.find { it.id == sendId }
|
||||
val isValidCreate = !isUpdate && localSend == null
|
||||
|
||||
@@ -35,12 +35,12 @@ interface VaultLockManager {
|
||||
var isFromLockFlow: Boolean
|
||||
|
||||
/**
|
||||
* Whether the vault is currently locked for the given [userId].
|
||||
* Whether or not the vault is currently locked for the given [userId].
|
||||
*/
|
||||
fun isVaultUnlocked(userId: String): Boolean
|
||||
|
||||
/**
|
||||
* Whether the vault is currently unlocking for the given [userId].
|
||||
* Whether or not the vault is currently unlocking for the given [userId].
|
||||
*/
|
||||
fun isVaultUnlocking(userId: String): Boolean
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ class VaultLockManagerImpl(
|
||||
context: Context,
|
||||
) : VaultLockManager {
|
||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||
private val ioScope = CoroutineScope(dispatcherManager.io)
|
||||
|
||||
/**
|
||||
* This [Map] tracks all active timeout [Job]s that are running and their associated data using
|
||||
@@ -193,7 +192,6 @@ class VaultLockManagerImpl(
|
||||
email = email,
|
||||
method = initUserCryptoMethod,
|
||||
userId = userId,
|
||||
upgradeToken = null,
|
||||
),
|
||||
)
|
||||
.flatMap { result ->
|
||||
@@ -479,7 +477,7 @@ class VaultLockManagerImpl(
|
||||
.map { userId -> vaultTimeoutChangesForUserFlow(userId = userId) }
|
||||
.merge()
|
||||
}
|
||||
.launchIn(ioScope)
|
||||
.launchIn(unconfinedScope)
|
||||
}
|
||||
|
||||
private fun observeUserLogoutResults() {
|
||||
@@ -712,7 +710,6 @@ class VaultLockManagerImpl(
|
||||
is InitUserCryptoMethod.DecryptedKey,
|
||||
is InitUserCryptoMethod.DeviceKey,
|
||||
is InitUserCryptoMethod.KeyConnector,
|
||||
is InitUserCryptoMethod.KeyConnectorUrl,
|
||||
is InitUserCryptoMethod.Pin,
|
||||
is InitUserCryptoMethod.PinEnvelope,
|
||||
-> return
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user