Skip to content

Conversation

@andrebispo5
Copy link
Contributor

@andrebispo5 andrebispo5 commented Dec 12, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-28997

📔 Objective

This pull request introduces the initial implementation of the "Migrate to My Items" feature, which guides users through transferring ownership of vault items to their organization or leaving the organization if they decline. It includes new UI views, state management, processor logic, localized strings, and supporting assets.

These changes lay the groundwork for the "Migrate to My Items" feature, providing a interface and state-driven logic for handling organization item ownership policies.

📸 Screenshots

In the screenshots the organization name is missing, it will be added later on.

Simulator Screenshot - iPhone 17 Pro - 2025-12-12 at 11 05 24 Simulator Screenshot - iPhone 17 Pro - 2025-12-12 at 11 05 28 Simulator Screenshot - iPhone 16 Pro - 2025-12-12 at 10 56 27 Simulator Screenshot - iPhone 16 Pro - 2025-12-12 at 10 56 24

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Dec 12, 2025

Logo
Checkmarx One – Scan Summary & Details6b65719f-1d26-43e3-968e-82bddd18dc2e

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Dec 15, 2025

Codecov Report

❌ Patch coverage is 58.89831% with 97 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.18%. Comparing base (a39361e) to head (7a3d92a).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...t/MigrateToMyItems/MigrateToMyItemsProcessor.swift 0.00% 55 Missing ⚠️
...ToMyItems/MigrateToMyItemsView+SnapshotTests.swift 0.00% 27 Missing ⚠️
...Platform/Application/Extensions/View+Toolbar.swift 50.00% 10 Missing ⚠️
.../Vault/MigrateToMyItems/MigrateToMyItemsView.swift 95.74% 4 Missing ⚠️
...Vault/MigrateToMyItems/MigrateToMyItemsState.swift 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2196      +/-   ##
==========================================
- Coverage   85.53%   84.18%   -1.36%     
==========================================
  Files        1737     1984     +247     
  Lines      147232   162582   +15350     
==========================================
+ Hits       125942   136875   +10933     
- Misses      21290    25707    +4417     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@andrebispo5 andrebispo5 marked this pull request as ready for review December 15, 2025 18:16
@andrebispo5 andrebispo5 requested review from a team and matt-livefront as code owners December 15, 2025 18:16
Copilot AI review requested due to automatic review settings December 15, 2025 18:16
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the initial implementation of the "Migrate to My Items" feature, enabling users to transfer vault item ownership to their organization or leave if they decline. The implementation includes a two-page flow (transfer acceptance and decline confirmation) with comprehensive test coverage, localized strings, and supporting UI components.

Key changes:

  • New two-page user flow for handling organization item ownership migration with accept/decline options
  • State management architecture using processor pattern with delegate for coordination
  • Complete test coverage including ViewInspector and snapshot tests

Reviewed changes

Copilot reviewed 12 out of 20 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
MigrateToMyItemsView.swift Main view component implementing the two-page transfer flow UI
MigrateToMyItemsView+ViewInspectorTests.swift Unit tests for view interactions and button tap behaviors
MigrateToMyItemsView+SnapshotTests.swift Snapshot tests for both transfer and decline confirmation pages
MigrateToMyItemsState.swift State model defining pages and organization data
MigrateToMyItemsProcessor.swift Business logic processor handling transfer acceptance and organization departure
MigrateToMyItemsEffect.swift Effect definitions for async operations
MigrateToMyItemsAction.swift Action definitions for synchronous state updates
ExternalLinksConstants.swift Added transfer ownership help URL
Localizable.strings Added localized strings for all UI text
View+Toolbar.swift Added reusable back button toolbar component
item-transfer.imageset/Contents.json Image asset configuration for transfer illustration
item-transfer-warning.imageset/Contents.json Image asset configuration for warning illustration

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

guard let organizationId = organizationIds.first else { return }

let organization = try await services.vaultRepository.fetchOrganization(withId: organizationId)
state.organizationName = organization?.name ?? "Test Org"
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a hardcoded fallback value 'Test Org' in production code could be misleading. Consider using an empty string or a localized placeholder message instead.

Suggested change
state.organizationName = organization?.name ?? "Test Org"
state.organizationName = organization?.name ?? Localizations.organizationNamePlaceholder

Copilot uses AI. Check for mistakes.
/// A markdown link to Bitwarden's terms of service.
static let termsOfService = URL(string: "https://bitwarden.com/terms/")!

/// A markdown link to Bitwarden's terms of service.
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment incorrectly states 'terms of service' when the constant is actually for transfer ownership help documentation.

Suggested change
/// A markdown link to Bitwarden's terms of service.
/// A markdown link to Bitwarden's help page for transferring ownership.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

@claude
Copy link

claude bot commented Dec 15, 2025

Claude finished @andrebispo5's task in 3m 21s —— View job


Code Review Complete

I've completed a thorough review of the "Migrate to My Items" feature implementation. Here's a summary of findings:

Assessment: REQUEST CHANGES

Critical Issues (Must Fix Before Merge):

  1. Snapshot tests are disabled - Both test methods are prefixed with disabletest, preventing them from running in CI. This defeats the purpose of visual regression testing.

  2. Missing processor unit tests - The MigrateToMyItemsProcessor contains business logic but has no corresponding test file. Per Testing.md guidelines, "Every type containing logic must be tested." (Note: Author indicated this will be addressed in separate tickets PM-29709/PM-29710)

Important Issues:

  1. Error handling doesn't dismiss view - When no organization is found (line 115-117), the code shows an alert but leaves the user stuck on the screen with empty organization name. Should add navigation to dismiss.

  2. Silent error handling - Errors in the catch block (line 123-125) are logged but the user receives no feedback and remains on a broken screen.

  3. Empty organization name fallback - Falls back to empty string when organization fetch fails, resulting in malformed UI text like "Transfer items to ".

Suggested Improvements:

  1. Documentation typo - Extra period in ExternalLinksConstants.swift doc comment
  2. Inconsistent accessibility labeling - iOS 26+ back button has accessibility label, but pre-iOS 26 version doesn't

Positive Notes:

  • ✅ Architecture follows unidirectional data flow pattern correctly
  • ✅ Proper dependency injection via ServiceContainer
  • ✅ Good DocC documentation coverage
  • ✅ ViewInspector tests are well-structured

The agent attempted to post inline comments but requires approval. The main blocking issues are the disabled snapshot tests and missing processor tests. The error handling issues should also be addressed before merge.


@andrebispo5
Copy link
Contributor Author

ℹ️ FYI: Processor tests will be addressed with other Tickets/PRs.

@claude
Copy link

claude bot commented Dec 15, 2025

Code Review: Migrate to My Items Feature

Overall Assessment: REQUEST CHANGES

Critical Issues

1. Missing Processor Unit Tests

File: BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsProcessor.swift
Line: 20

The MigrateToMyItemsProcessor contains business logic but has no corresponding MigrateToMyItemsProcessorTests.swift file. According to Docs/Testing.md, "Every type containing logic must be tested."

Required test coverage
// Create: BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsProcessorTests.swift

class MigrateToMyItemsProcessorTests: BitwardenTestCase {
    // Test cases needed:
    // - test_perform_appeared_loadsOrganizationName()
    // - test_perform_appeared_noOrganizationFound_showsAlert()
    // - test_perform_appeared_multipleOrganizations_selectsFirst()
    // - test_perform_appeared_errorFetchingOrganization_logsError()
    // - test_perform_acceptTransferTapped_showsLoadingAndDismisses()
    // - test_perform_leaveOrganizationTapped_callsDelegate()
    // - test_receive_backTapped_navigatesToTransferPage()
    // - test_receive_declineAndLeaveTapped_navigatesToConfirmationPage()
}

Note: Author indicated in PR description that processor tests will be addressed in separate tickets (PM-29709/PM-29710).

2. Silent Failure When No Organization Found

File: BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsProcessor.swift
Line: 115-117

guard let organizationId = organizationIds.first else {
    coordinator.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
    return
}

When no organization applies the personal ownership policy, the code shows a generic error alert but doesn't dismiss the view or navigate back. This leaves the user stuck on a screen with empty organization name.

Recommended fix
guard let organizationId = organizationIds.first else {
    coordinator.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
    coordinator.navigate(to: .dismiss())
    return
}

Or better yet, provide a more specific error message and handle this case explicitly in the UI.

3. Organization Name Defaults to Empty String on Error

File: BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsProcessor.swift
Line: 122-125

state.organizationName = organization?.name ?? ""
} catch {
    services.errorReporter.log(error: error)
}

When fetchOrganization fails, the error is logged but organizationName remains empty. The UI will display localized strings with empty placeholders (e.g., "Transfer items to ").

Recommended fix
guard let organizationName = organization?.name, !organizationName.isEmpty else {
    coordinator.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
    coordinator.navigate(to: .dismiss())
    return
}
state.organizationName = organizationName
} catch {
    services.errorReporter.log(error: error)
    coordinator.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
    coordinator.navigate(to: .dismiss())
}

Important Issues

4. Breaking Change to PolicyService Return Type

File: BitwardenShared/Core/Vault/Services/PolicyService.swift
Line: 20

The return type of fetchTimeoutPolicyValues() changed from (action: SessionTimeoutAction?, value: Int)? to SessionTimeoutPolicy?. This is a breaking change that could affect callers.

Impact analysis

Verified that all call sites were updated:

  • AccountSecurityProcessor.swift:121
  • SyncService.swift:457

The change is properly handled, but future refactoring tip: Consider deprecating the old signature before removing it to make breaking changes more gradual.

5. Complex Computed Property for Available Timeout Options

File: BitwardenShared/UI/Platform/Settings/Settings/AccountSecurity/AccountSecurityState.swift
Line: 124-145

The availableTimeoutOptions computed property contains significant conditional logic that determines which timeout options to show based on policy type and value.

Suggestion

Consider extracting this logic into a static helper method or separate type for better testability:

// In AccountSecurityState or separate helper
static func availableTimeoutOptions(
    for policyType: SessionTimeoutType?,
    policyValue: Int
) -> [SessionTimeoutValue] {
    // Logic here
}

var availableTimeoutOptions: [SessionTimeoutValue] {
    Self.availableTimeoutOptions(for: policyTimeoutType, policyValue: policyTimeoutValue)
}

This makes the logic easier to unit test independently.

Reference: Create AccountSecurityStateTests.swift with test cases for this computed property.


Suggested Improvements

6. Navigation Title Accessibility

File: BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView.swift
Line: 31

The navigation title uses .inline display mode, which is appropriate. Consider adding an accessibility identifier for UI testing:

.navigationBar(title: Localizations.itemTransfer, titleDisplayMode: .inline)
.accessibilityIdentifier("MigrateToMyItemsView")

7. Missing Coordinator Integration

The MigrateToMyItemsProcessor references VaultItemRoute and VaultItemEvent, but there's no corresponding route in VaultItemRoute.swift to navigate to this screen. How is this feature triggered?

Expected integration

Should there be a route added like:

// In VaultItemRoute.swift
case migrateToMyItems

Or is this triggered from a different coordinator? Please clarify the entry point for this flow.

8. Policy Service Method Documentation

File: BitwardenShared/Core/Vault/Services/PolicyService.swift
Line: 16-19

The DocC comment was updated but could be more explicit about the return value structure:

/// Fetches the maximum vault timeout policy values if the policy is enabled.
///
/// - Returns: A `SessionTimeoutPolicy` containing:
///   - `timeoutAction`: The required action (.lock or .logout), or nil if user can choose
///   - `timeoutType`: The type of timeout (.custom, .immediately, .never, .onAppRestart)
///   - `timeoutValue`: The maximum timeout duration
///   Returns `nil` if no maximum vault timeout policies apply to the user.

Security Considerations

Zero-Knowledge Architecture: No encryption keys or sensitive data are exposed in the new code.
Policy Enforcement: The code correctly fetches organization policies via PolicyService.
⚠️ Authorization: TODO comments indicate that API calls for accept/leave are not yet implemented. Ensure these endpoints properly validate user permissions when implemented.


Architecture Compliance

Unidirectional Data Flow: Properly uses State/Action/Effect pattern
Dependency Injection: Uses Services typealias and ServiceContainer
Coordinator Pattern: Delegates navigation to coordinator
File Organization: Files properly located in BitwardenShared/UI/Vault/MigrateToMyItems/


Testing Coverage

Implemented:

  • ✅ ViewInspector tests for button interactions
  • ✅ Snapshot tests for both pages (transfer and decline confirmation)

Missing:

  • ❌ Processor unit tests (noted by author as future work)
  • ❌ Integration with coordinator (no route in VaultItemRoute)
  • ❌ Tests for AccountSecurityState computed properties

Documentation

✅ DocC comments present on:

  • MigrateToMyItemsState
  • MigrateToMyItemsAction
  • MigrateToMyItemsEffect
  • MigrateToMyItemsProcessor
  • MigrateToMyItemsView
  • MigrateToMyItemsProcessorDelegate

✅ All public properties and methods have documentation


Code Style

✅ Follows Swift code style guidelines
✅ Proper use of guard clauses
✅ SwiftUI best practices followed
✅ Naming conventions consistent with codebase


Action Items

  1. Before merge:

    • Fix critical issue Finding 2: Add navigation after error alert
    • Fix critical issue Finding 3: Handle empty organization name
    • Clarify how this screen is accessed (missing route)
  2. Follow-up tickets (as noted by author):

    • PM-29709: Implement accept transfer API call
    • PM-29710: Implement leave organization API call
    • Add processor unit tests
    • Add tests for AccountSecurityState computed properties
  3. Consider for future:

    • Extract availableTimeoutOptions logic for better testability
    • Add accessibility identifiers for UI testing
    • Add more specific error messages for organization loading failures

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

"EncryptionSettingsUpdated" = "Encryption settings updated";
"ItemTransfer" = "Item transfer";
"TransferItemsToX" = "Transfer items to %1$@";
"XIsRequiringAllItemsToBeOwnedByTheOrganization" = "%1$@ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Append DescriptionLong to the key to be consistent with other long strings such as TheNewRecommendedEncryptionSettingsDescriptionLong

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add it but I notice we have some long strings not following this suffix rule that maybe we could address as tech dept for consistency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, those were added before we started appending the suffix and I agree they should be updated at some moment as tech-debt.

"DeclineAndLeave" = "Decline and leave";
"WhyAmISeeingThis" = "Why am I seeing this?";
"AreYouSureYouWantToLeave" = "Are you sure you want to leave?";
"ByDecliningYourPersonalItemsWillStayInYourAccount" = "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features.";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Append DescriptionLong to the key to be consistent with other long strings such as TheNewRecommendedEncryptionSettingsDescriptionLong

/// A markdown link to Bitwarden's terms of service.
static let termsOfService = URL(string: "https://bitwarden.com/terms/")!

/// A markdown link to Bitwarden's terms of service.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Comment on lines 52 to 53
/// A link to Bitwarden's help page for My vault items migration.
static let websiteIconsHelp = URL(string: "https://bitwarden.com/help/website-icons/")!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This link is not for "My vault items migration".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the wrong one 😶‍🌫️

Comment on lines +95 to +96
defer { coordinator.hideLoadingOverlay() }
coordinator.navigate(to: .dismiss())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Should hide loading be deferred to after the coordinator dismisses the view?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as below

Comment on lines +115 to +118
guard let organizationId = organizationIds.first else {
coordinator.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred))
return
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 What happens with the state of the view in this case? Does the user have any feedback after "An error has occurred" of what to do from there? Should the user stay on this view or go back?

❓ If there are more organizations with the personal ownership policy enabled, is it ok that this always take into consideration the first one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll defer most of the logic and test of the processor to another PR, it is basically a scaffolding as it is now.


state.organizationName = organization?.name ?? ""
} catch {
services.errorReporter.log(error: error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Same case, should it go back if an error is thrown and caught here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Comment on lines 35 to 52
/// The transfer page renders correctly.
@MainActor
func test_snapshot_transferPage() {
assertSnapshots(
of: subject.navStackWrapped,
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5],
)
}

/// The decline confirmation page renders correctly.
@MainActor
func test_snapshot_declineConfirmationPage() {
processor.state.page = .declineConfirmation
assertSnapshots(
of: subject.navStackWrapped,
as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5],
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Rename these tests before merging to disabletest_ so it's consistent with others until we decide to enable all snapshot tests again.


/// A view that prompts the user to accept or decline an item transfer request from an organization.
///
struct MigrateToMyItemsView: View {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:accessibility: Can the user navigate correctly through this views/pages using VoiceOver?

Copy link
Contributor Author

@andrebispo5 andrebispo5 Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with the Accessibility inspector and it looked fine 👍

@andrebispo5 andrebispo5 requested a review from fedemkr December 17, 2025 14:00
@andrebispo5 andrebispo5 merged commit 41406e2 into main Dec 17, 2025
22 checks passed
@andrebispo5 andrebispo5 deleted the pm-28997/vault_takeover_ui branch December 17, 2025 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants