diff --git a/BitwardenKit/UI/Platform/Application/Extensions/View+Toolbar.swift b/BitwardenKit/UI/Platform/Application/Extensions/View+Toolbar.swift index 0ed10b0b8a..de751c6c65 100644 --- a/BitwardenKit/UI/Platform/Application/Extensions/View+Toolbar.swift +++ b/BitwardenKit/UI/Platform/Application/Extensions/View+Toolbar.swift @@ -19,6 +19,34 @@ public extension View { .accessibilityIdentifier("AddItemButton") } + /// Returns a toolbar button configured for navigating back within a view flow. + /// + /// - Parameters: + /// - hidden: Whether to hide the toolbar item. + /// - action: The action to perform when the button is tapped. + /// - Returns: A `Button` configured for navigating back. + /// + func backToolbarButton(hidden: Bool = false, action: @escaping () -> Void) -> some View { + if #available(iOS 26, *) { + return Button(action: action) { + Label(Localizations.back, systemImage: "chevron.backward") + } + .hidden(hidden) + .accessibilityIdentifier("BackButton") + .accessibilityLabel(Localizations.back) + } + return Button(action: action) { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + .font(.body.weight(.semibold)) + Text(Localizations.back) + } + } + .buttonStyle(.toolbar) + .hidden(hidden) + .accessibilityIdentifier("BackButton") + } + /// Returns a toolbar button configured for cancelling an operation in a view. /// /// - Parameters: diff --git a/BitwardenResources/Localizations/en.lproj/Localizable.strings b/BitwardenResources/Localizations/en.lproj/Localizable.strings index c6774e52a9..4faddc51ab 100644 --- a/BitwardenResources/Localizations/en.lproj/Localizable.strings +++ b/BitwardenResources/Localizations/en.lproj/Localizable.strings @@ -1273,6 +1273,17 @@ "TheNewRecommendedEncryptionSettingsDescriptionLong" = "The new recommended encryption settings will improve your account security. Enter your master password to update now."; "Updating" = "Updating…"; "EncryptionSettingsUpdated" = "Encryption settings updated"; +"ItemTransfer" = "Item transfer"; +"TransferItemsToX" = "Transfer items to %1$@"; +"XIsRequiringAllItemsToBeOwnedByTheOrganizationDescriptionLong" = "%1$@ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items."; +"DeclineAndLeave" = "Decline and leave"; +"WhyAmISeeingThis" = "Why am I seeing this?"; +"AreYouSureYouWantToLeave" = "Are you sure you want to leave?"; +"ByDecliningYourPersonalItemsWillStayInYourAccountDescriptionLong" = "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."; +"ContactYourAdminToRegainAccess" = "Contact your admin to regain access."; +"LeaveX" = "Leave %1$@"; +"HowToManageMyVault" = "How to manage My vault"; +"YouLeftTheOrganization" = "You left the organization"; "ThisSettingIsManagedByYourOrganization" = "This setting is managed by your organization."; "YourOrganizationHasSetTheDefaultSessionTimeoutToX" = "Your organization has set the default session timeout to %1$@."; "YourOrganizationHasSetTheMaximumSessionTimeoutToX" = "Your organization has set the maximum session timeout to %1$@."; diff --git a/BitwardenShared/Core/Platform/Utilities/ExternalLinksConstants.swift b/BitwardenShared/Core/Platform/Utilities/ExternalLinksConstants.swift index 175b23cdaa..e4a0a05dce 100644 --- a/BitwardenShared/Core/Platform/Utilities/ExternalLinksConstants.swift +++ b/BitwardenShared/Core/Platform/Utilities/ExternalLinksConstants.swift @@ -43,6 +43,9 @@ extension ExternalLinksConstants { /// A markdown link to Bitwarden's terms of service. static let termsOfService = URL(string: "https://bitwarden.com/terms/")! + /// A link to Bitwarden's help page for My vault items migration. . + static let transferOwnership = URL(string: "https://bitwarden.com/help/transfer-ownership/")! + /// A markdown link to Bitwarden's marketing email preferences. static let unsubscribeFromMarketingEmails = URL(string: "https://bitwarden.com/email-preferences/")! diff --git a/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer-warning.imageset/Contents.json b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer-warning.imageset/Contents.json new file mode 100644 index 0000000000..e8c0934bc5 --- /dev/null +++ b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer-warning.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "item-transfer-warning.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer-warning.imageset/item-transfer-warning.pdf b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer-warning.imageset/item-transfer-warning.pdf new file mode 100644 index 0000000000..359faacd91 Binary files /dev/null and b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer-warning.imageset/item-transfer-warning.pdf differ diff --git a/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer.imageset/Contents.json b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer.imageset/Contents.json new file mode 100644 index 0000000000..8ce9a8361a --- /dev/null +++ b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "item-transfer.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer.imageset/item-transfer.pdf b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer.imageset/item-transfer.pdf new file mode 100644 index 0000000000..03c306cda4 Binary files /dev/null and b/BitwardenShared/UI/Platform/Application/Support/Images.xcassets/Images/Illustrations/item-transfer.imageset/item-transfer.pdf differ diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsAction.swift b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsAction.swift new file mode 100644 index 0000000000..55c9d08031 --- /dev/null +++ b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsAction.swift @@ -0,0 +1,11 @@ +// MARK: - MigrateToMyItemsAction + +/// Actions that can be processed by a `MigrateToMyItemsProcessor`. +/// +enum MigrateToMyItemsAction: Equatable, Sendable { + /// The user tapped the back button on the decline confirmation screen. + case backTapped + + /// The user tapped the "Decline and leave" button. + case declineAndLeaveTapped +} diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsEffect.swift b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsEffect.swift new file mode 100644 index 0000000000..f157b7f32d --- /dev/null +++ b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsEffect.swift @@ -0,0 +1,14 @@ +// MARK: - MigrateToMyItemsEffect + +/// Effects that can be processed by a `MigrateToMyItemsProcessor`. +/// +enum MigrateToMyItemsEffect: Equatable, Sendable { + /// The user tapped the "Accept transfer" button. + case acceptTransferTapped + + /// The view appeared on screen. + case appeared + + /// The user tapped the "Leave {organization}" button to confirm leaving. + case leaveOrganizationTapped +} diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsProcessor.swift b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsProcessor.swift new file mode 100644 index 0000000000..d7f89519f3 --- /dev/null +++ b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsProcessor.swift @@ -0,0 +1,127 @@ +import BitwardenKit +import BitwardenResources +import Foundation + +// MARK: - MigrateToMyItemsProcessorDelegate + +/// A delegate for the `MigrateToMyItemsProcessor` to communicate events back to the coordinator. +/// +@MainActor +protocol MigrateToMyItemsProcessorDelegate: AnyObject { + /// Called when the user has left the organization. + /// + func didLeaveOrganization() +} + +// MARK: - MigrateToMyItemsProcessor + +/// The processor used to manage state and handle actions for the migrate to my items screen. +/// +final class MigrateToMyItemsProcessor: StateProcessor< + MigrateToMyItemsState, + MigrateToMyItemsAction, + MigrateToMyItemsEffect, +> { + // MARK: Types + + typealias Services = HasErrorReporter + & HasPolicyService + & HasVaultRepository + + // MARK: Private Properties + + /// The coordinator that handles navigation. + private let coordinator: AnyCoordinator + + /// The delegate to notify of events. + private weak var delegate: MigrateToMyItemsProcessorDelegate? + + /// The services used by this processor. + private let services: Services + + // MARK: Initialization + + /// Creates a new `MigrateToMyItemsProcessor`. + /// + /// - Parameters: + /// - coordinator: The coordinator that handles navigation. + /// - delegate: The delegate to notify of events. + /// - services: The services required by this processor. + /// - state: The initial state of the processor. + /// + init( + coordinator: AnyCoordinator, + delegate: MigrateToMyItemsProcessorDelegate?, + services: Services, + state: MigrateToMyItemsState, + ) { + self.coordinator = coordinator + self.delegate = delegate + self.services = services + super.init(state: state) + } + + // MARK: Methods + + override func perform(_ effect: MigrateToMyItemsEffect) async { + switch effect { + case .acceptTransferTapped: + await acceptTransfer() + case .appeared: + await loadOrganizationName() + case .leaveOrganizationTapped: + await leaveOrganization() + } + } + + override func receive(_ action: MigrateToMyItemsAction) { + switch action { + case .backTapped: + state.page = .transfer + case .declineAndLeaveTapped: + state.page = .declineConfirmation + } + } + + // MARK: Private Methods + + /// Accepts the item transfer. + /// + private func acceptTransfer() async { + coordinator.showLoadingOverlay(LoadingOverlayState(title: Localizations.loading)) + + // TODO: PM-29709 Implement accept transfer API call + + defer { coordinator.hideLoadingOverlay() } + coordinator.navigate(to: .dismiss()) + } + + /// Leaves the organization after declining the item transfer. + /// + private func leaveOrganization() async { + coordinator.showLoadingOverlay(LoadingOverlayState(title: Localizations.loading)) + + // TODO: PM-29710 Implement leave organization API call + + defer { coordinator.hideLoadingOverlay() } + delegate?.didLeaveOrganization() + } + + /// Loads the organization name from the policy service and vault repository. + /// + private func loadOrganizationName() async { + do { + let organizationIds = await services.policyService.organizationsApplyingPolicyToUser(.personalOwnership) + guard let organizationId = organizationIds.first else { + coordinator.showAlert(.defaultAlert(title: Localizations.anErrorHasOccurred)) + return + } + let organization = try await services.vaultRepository.fetchOrganization(withId: organizationId) + // TODO: PM-29113 Validate if user must do vault migration and error handling + + state.organizationName = organization?.name ?? "" + } catch { + services.errorReporter.log(error: error) + } + } +} diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsState.swift b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsState.swift new file mode 100644 index 0000000000..fb621dccb9 --- /dev/null +++ b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsState.swift @@ -0,0 +1,27 @@ +import Foundation + +// MARK: - MigrateToMyItemsState + +/// An object that defines the current state of a `MigrateToMyItemsView`. +/// +struct MigrateToMyItemsState: Equatable, Sendable { + // MARK: Types + + /// An enumeration of the pages in the migrate to my items flow. + /// + enum Page: Equatable, Sendable { + /// The main screen prompting the user to accept or decline the transfer. + case transfer + + /// The confirmation screen shown when the user chooses to decline. + case declineConfirmation + } + + // MARK: Properties + + /// The name of the organization requesting the item transfer. + var organizationName: String = "" + + /// The current page being displayed. + var page: Page = .transfer +} diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView+SnapshotTests.swift b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView+SnapshotTests.swift new file mode 100644 index 0000000000..196c9fb0fb --- /dev/null +++ b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView+SnapshotTests.swift @@ -0,0 +1,53 @@ +// swiftlint:disable:this file_name +import BitwardenKit +import BitwardenKitMocks +import SnapshotTesting +import XCTest + +@testable import BitwardenShared + +class MigrateToMyItemsViewSnapshotTests: BitwardenTestCase { + // MARK: Properties + + var processor: MockProcessor! + var subject: MigrateToMyItemsView! + + // MARK: Setup & Teardown + + override func setUp() { + super.setUp() + + processor = MockProcessor(state: MigrateToMyItemsState(organizationName: "Acme Corporation")) + let store = Store(processor: processor) + + subject = MigrateToMyItemsView(store: store) + } + + override func tearDown() { + super.tearDown() + + processor = nil + subject = nil + } + + // MARK: Snapshots + + /// The transfer page renders correctly. + @MainActor + func disabletest_snapshot_transferPage() { + assertSnapshots( + of: subject.navStackWrapped, + as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5], + ) + } + + /// The decline confirmation page renders correctly. + @MainActor + func disabletest_snapshot_declineConfirmationPage() { + processor.state.page = .declineConfirmation + assertSnapshots( + of: subject.navStackWrapped, + as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5], + ) + } +} diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView+ViewInspectorTests.swift b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView+ViewInspectorTests.swift new file mode 100644 index 0000000000..6bbc1bcf92 --- /dev/null +++ b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView+ViewInspectorTests.swift @@ -0,0 +1,72 @@ +// swiftlint:disable:this file_name +import BitwardenKit +import BitwardenKitMocks +import BitwardenResources +import XCTest + +@testable import BitwardenShared + +class MigrateToMyItemsViewTests: BitwardenTestCase { + // MARK: Properties + + var processor: MockProcessor! + var subject: MigrateToMyItemsView! + + // MARK: Setup & Teardown + + override func setUp() { + super.setUp() + + processor = MockProcessor(state: MigrateToMyItemsState(organizationName: "Acme Corporation")) + let store = Store(processor: processor) + + subject = MigrateToMyItemsView(store: store) + } + + override func tearDown() { + super.tearDown() + + processor = nil + subject = nil + } + + // MARK: Tests - Transfer Page + + /// Tapping the accept button dispatches the `.acceptTransferTapped` effect. + @MainActor + func test_acceptButton_tap() async throws { + let button = try subject.inspect().find(asyncButton: Localizations.accept) + try await button.tap() + XCTAssertEqual(processor.effects.last, .acceptTransferTapped) + } + + /// Tapping the decline and leave button dispatches the `.declineAndLeaveTapped` action. + @MainActor + func test_declineAndLeaveButton_tap() throws { + let button = try subject.inspect().find(button: Localizations.declineAndLeave) + try button.tap() + XCTAssertEqual(processor.dispatchedActions.last, .declineAndLeaveTapped) + } + + // MARK: Tests - Decline Confirmation Page + + /// Tapping the back button dispatches the `.backTapped` action. + @MainActor + func test_backButton_tap() throws { + processor.state.page = .declineConfirmation + let button = try subject.inspect().find(viewWithAccessibilityIdentifier: "BackButton").button() + try button.tap() + XCTAssertEqual(processor.dispatchedActions.last, .backTapped) + } + + /// Tapping the leave organization button dispatches the `.leaveOrganizationTapped` effect. + @MainActor + func test_leaveOrganizationButton_tap() async throws { + processor.state.page = .declineConfirmation + let button = try subject.inspect().find( + asyncButton: Localizations.leaveX(processor.state.organizationName), + ) + try await button.tap() + XCTAssertEqual(processor.effects.last, .leaveOrganizationTapped) + } +} diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView.swift b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView.swift new file mode 100644 index 0000000000..c5b8d8c77e --- /dev/null +++ b/BitwardenShared/UI/Vault/MigrateToMyItems/MigrateToMyItemsView.swift @@ -0,0 +1,149 @@ +import BitwardenKit +import BitwardenResources +import SwiftUI + +// MARK: - MigrateToMyItemsView + +/// A view that prompts the user to accept or decline an item transfer request from an organization. +/// +struct MigrateToMyItemsView: View { + // MARK: Properties + + /// An object used to open URLs from this view. + @Environment(\.openURL) private var openURL + + /// The `Store` for this view. + @ObservedObject var store: Store + + // MARK: View + + var body: some View { + Group { + switch store.state.page { + case .transfer: + transferPage + case .declineConfirmation: + declineConfirmationPage + } + } + .transition(.opacity) + .animation(.easeInOut, value: store.state.page) + .navigationBar(title: Localizations.itemTransfer, titleDisplayMode: .inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if store.state.page == .declineConfirmation { + backToolbarButton { + store.send(.backTapped) + } + } + } + } + .interactiveDismissDisabled() + .task { + await store.perform(.appeared) + } + } + + // MARK: Private Views + + /// The main transfer page prompting the user to accept or decline. + private var transferPage: some View { + VStack(spacing: 24) { + IllustratedMessageView( + image: Asset.Images.Illustrations.itemTransfer, + style: .mediumImage, + title: Localizations.transferItemsToX(store.state.organizationName), + message: Localizations.xIsRequiringAllItemsToBeOwnedByTheOrganizationDescriptionLong( + store.state.organizationName, + ), + ) + + VStack(spacing: 12) { + AsyncButton(Localizations.accept) { + await store.perform(.acceptTransferTapped) + } + .buttonStyle(.primary()) + + Button(Localizations.declineAndLeave) { + store.send(.declineAndLeaveTapped) + } + .buttonStyle(.secondary()) + } + + Button { + openURL(ExternalLinksConstants.transferOwnership) + } label: { + Text(Localizations.whyAmISeeingThis) + } + .buttonStyle(.bitwardenBorderless) + } + .padding(.horizontal, 16) + .padding(.top, 12) + .frame(maxWidth: .infinity) + .scrollView() + } + + /// The decline confirmation page warning the user about leaving the organization. + private var declineConfirmationPage: some View { + VStack(spacing: 24) { + IllustratedMessageView( + image: Asset.Images.Illustrations.itemTransferWarning, + style: .mediumImage, + title: Localizations.areYouSureYouWantToLeave, + message: Localizations.byDecliningYourPersonalItemsWillStayInYourAccountDescriptionLong, + ) { + Text(Localizations.contactYourAdminToRegainAccess) + .styleGuide(.body) + .foregroundStyle(SharedAsset.Colors.textPrimary.swiftUIColor) + .padding(.top, 8) + } + + VStack(spacing: 12) { + AsyncButton(Localizations.leaveX(store.state.organizationName)) { + await store.perform(.leaveOrganizationTapped) + } + .buttonStyle(.primary(isDestructive: true)) + } + + Button { + openURL(ExternalLinksConstants.transferOwnership) + } label: { + Text(Localizations.howToManageMyVault) + } + .buttonStyle(.bitwardenBorderless) + } + .padding(.horizontal, 16) + .padding(.top, 12) + .frame(maxWidth: .infinity) + .scrollView() + } +} + +// MARK: - Previews + +#if DEBUG +#Preview("Transfer") { + MigrateToMyItemsView( + store: Store( + processor: StateProcessor( + state: MigrateToMyItemsState(organizationName: "Acme"), + ), + ), + ) + .navStackWrapped +} + +#Preview("Decline Confirmation") { + MigrateToMyItemsView( + store: Store( + processor: StateProcessor( + state: MigrateToMyItemsState( + organizationName: "Acme", + page: .declineConfirmation, + ), + ), + ), + ) + .navStackWrapped +} +#endif diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.1.png b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.1.png new file mode 100644 index 0000000000..d62e21bd39 Binary files /dev/null and b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.1.png differ diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.2.png b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.2.png new file mode 100644 index 0000000000..1302214d50 Binary files /dev/null and b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.2.png differ diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.3.png b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.3.png new file mode 100644 index 0000000000..65d048b327 Binary files /dev/null and b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_declineConfirmationPage.3.png differ diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.1.png b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.1.png new file mode 100644 index 0000000000..d69adc3161 Binary files /dev/null and b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.1.png differ diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.2.png b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.2.png new file mode 100644 index 0000000000..9140fb54bb Binary files /dev/null and b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.2.png differ diff --git a/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.3.png b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.3.png new file mode 100644 index 0000000000..87c1b4a56d Binary files /dev/null and b/BitwardenShared/UI/Vault/MigrateToMyItems/__Snapshots__/MigrateToMyItemsView+SnapshotTests/test_snapshot_transferPage.3.png differ