diff --git a/BitwardenKit/UI/Platform/Application/Views/ActionCard.swift b/BitwardenKit/UI/Platform/Application/Views/ActionCard.swift index 140954576..9655e11df 100644 --- a/BitwardenKit/UI/Platform/Application/Views/ActionCard.swift +++ b/BitwardenKit/UI/Platform/Application/Views/ActionCard.swift @@ -180,6 +180,15 @@ public struct ActionCard: View { dismissButtonState: ActionCard.ButtonState(title: "Dismiss") {}, ) + ActionCard( + title: "Title", + message: "Message", + actionButtonState: ActionCard.ButtonState(title: "Tap me!") {}, + dismissButtonState: ActionCard.ButtonState(title: "Dismiss") {}, + ) { + SharedAsset.Icons.warning24.swiftUIImage.foregroundStyle(SharedAsset.Colors.iconSecondary.swiftUIColor) + } + ActionCard( title: "Title", message: "Message", diff --git a/BitwardenResources/Localizations/en.lproj/Localizable.strings b/BitwardenResources/Localizations/en.lproj/Localizable.strings index b1a64b914..a6e4fe601 100644 --- a/BitwardenResources/Localizations/en.lproj/Localizable.strings +++ b/BitwardenResources/Localizations/en.lproj/Localizable.strings @@ -1264,6 +1264,9 @@ "TheNewRecommendedEncryptionSettingsDescriptionLong" = "The new recommended encryption settings will improve your account security. Enter your master password to update now."; "Updating" = "Updating…"; "EncryptionSettingsUpdated" = "Encryption settings updated"; +"IntroducingArchive" = "Introducing Archive"; +"KeepYtemsYouDontNeedRightNowSafeButOutOfSight" = "Keep items you don’t need right now safe but out of sight."; +"GoToArchive" = "Go to archive"; "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."; diff --git a/BitwardenShared/Core/Platform/Services/StateService.swift b/BitwardenShared/Core/Platform/Services/StateService.swift index b3f34bd61..412a6b82a 100644 --- a/BitwardenShared/Core/Platform/Services/StateService.swift +++ b/BitwardenShared/Core/Platform/Services/StateService.swift @@ -146,6 +146,12 @@ protocol StateService: AnyObject { /// func getAppTheme() async -> AppTheme + /// Gets whether the archive onboarding has been shown. + /// + /// - Returns: Whether the archive onboarding has been shown. + /// + func getArchiveOnboardingShown() async -> Bool + /// Gets the clear clipboard value for an account. /// /// - Parameter userId: The user ID associated with the clear clipboard value. Defaults to the active @@ -527,6 +533,12 @@ protocol StateService: AnyObject { /// func setAppTheme(_ appTheme: AppTheme) async + /// Sets whether the archive onboarding has been shown. + /// + /// - Parameter shown: Whether the archive onboarding has been shown. + /// + func setArchiveOnboardingShown(_ shown: Bool) async + /// Sets the clear clipboard value for an account. /// /// - Parameters: @@ -1382,6 +1394,14 @@ extension StateService { func setVaultTimeout(value: SessionTimeoutValue) async throws { try await setVaultTimeout(value: value, userId: nil) } + + /// Whether the user should do the archive onboarding. + /// - Returns: `true` if they should, `false` otherwise. + func shouldDoArchiveOnboarding() async -> Bool { + let hasPremium = await doesActiveAccountHavePremium() + let archiveOnboardingShown = await getArchiveOnboardingShown() + return hasPremium && !archiveOnboardingShown + } } // MARK: - StateServiceError @@ -1639,6 +1659,10 @@ actor DefaultStateService: StateService, ActiveAccountStateProvider, ConfigState AppTheme(appSettingsStore.appTheme) } + func getArchiveOnboardingShown() async -> Bool { + appSettingsStore.archiveOnboardingShown + } + func getClearClipboardValue(userId: String?) async throws -> ClearClipboardValue { let userId = try userId ?? getActiveAccountUserId() return appSettingsStore.clearClipboardValue(userId: userId) @@ -1992,6 +2016,10 @@ actor DefaultStateService: StateService, ActiveAccountStateProvider, ConfigState appThemeSubject.send(appTheme) } + func setArchiveOnboardingShown(_ shown: Bool) async { + appSettingsStore.archiveOnboardingShown = shown + } + func setClearClipboardValue(_ clearClipboardValue: ClearClipboardValue?, userId: String?) async throws { let userId = try userId ?? getActiveAccountUserId() appSettingsStore.setClearClipboardValue(clearClipboardValue, userId: userId) diff --git a/BitwardenShared/Core/Platform/Services/StateServiceTests.swift b/BitwardenShared/Core/Platform/Services/StateServiceTests.swift index fb9481d69..e246e7562 100644 --- a/BitwardenShared/Core/Platform/Services/StateServiceTests.swift +++ b/BitwardenShared/Core/Platform/Services/StateServiceTests.swift @@ -622,12 +622,14 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body } } - /// `getClearClipboardValue()` returns the clear clipboard value for the active account. - func test_getClearClipboardValue() async throws { - await subject.addAccount(.fixture()) - appSettingsStore.clearClipboardValues["1"] = .twoMinutes - let value = try await subject.getClearClipboardValue() - XCTAssertEqual(value, .twoMinutes) + /// `getArchiveOnboardingShown()` returns whether the archive onboarding has been shown. + func test_getArchiveOnboardingShown() async { + var hasShownOnboarding = await subject.getArchiveOnboardingShown() + XCTAssertFalse(hasShownOnboarding) + + appSettingsStore.archiveOnboardingShown = true + hasShownOnboarding = await subject.getArchiveOnboardingShown() + XCTAssertTrue(hasShownOnboarding) } /// `getBiometricAuthenticationEnabled(:)` returns biometric unlock preference of the active user. @@ -647,6 +649,14 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body } } + /// `getClearClipboardValue()` returns the clear clipboard value for the active account. + func test_getClearClipboardValue() async throws { + await subject.addAccount(.fixture()) + appSettingsStore.clearClipboardValues["1"] = .twoMinutes + let value = try await subject.getClearClipboardValue() + XCTAssertEqual(value, .twoMinutes) + } + /// `getConnectToWatch()` returns the connect to watch value for the active account. func test_getConnectToWatch() async throws { await subject.addAccount(.fixture()) @@ -1836,6 +1846,15 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body } } + /// `setArchiveOnboardingShown(_:)` sets whether the archive onboarding has been shown. + func test_setArchiveOnboardingShown() async { + await subject.setArchiveOnboardingShown(true) + XCTAssertTrue(appSettingsStore.archiveOnboardingShown) + + await subject.setArchiveOnboardingShown(false) + XCTAssertFalse(appSettingsStore.archiveOnboardingShown) + } + /// `setBiometricAuthenticationEnabled(isEnabled:)` sets biometric unlock preference for the default user. func test_setBiometricAuthenticationEnabled_default() async throws { await subject.addAccount(.fixture()) @@ -2538,6 +2557,33 @@ class StateServiceTests: BitwardenTestCase { // swiftlint:disable:this type_body XCTAssertEqual(appSettingsStore.usesKeyConnector["1"], true) } + /// `shouldDoArchiveOnboarding()` returns `true` when active account is premium + /// and the archive onboarding has not been shown yet. + func test_shouldDoArchiveOnboarding_true() async { + await subject.addAccount(.fixture(profile: .fixture(hasPremiumPersonally: true))) + appSettingsStore.archiveOnboardingShown = false + let shouldDoArchiveOnboarding = await subject.shouldDoArchiveOnboarding() + XCTAssertTrue(shouldDoArchiveOnboarding) + } + + /// `shouldDoArchiveOnboarding()` returns `false` when active account is premium + /// and the archive onboarding has already been shown. + func test_shouldDoArchiveOnboarding_onboardingAlreadyShown() async { + await subject.addAccount(.fixture(profile: .fixture(hasPremiumPersonally: true))) + appSettingsStore.archiveOnboardingShown = true + let shouldDoArchiveOnboarding = await subject.shouldDoArchiveOnboarding() + XCTAssertFalse(shouldDoArchiveOnboarding) + } + + /// `shouldDoArchiveOnboarding()` returns `false` when active account is not premium + /// and the archive onboarding has not been shown yet. + func test_shouldDoArchiveOnboarding_noPremium() async { + await subject.addAccount(.fixture(profile: .fixture(hasPremiumPersonally: false))) + appSettingsStore.archiveOnboardingShown = false + let shouldDoArchiveOnboarding = await subject.shouldDoArchiveOnboarding() + XCTAssertFalse(shouldDoArchiveOnboarding) + } + /// `syncToAuthenticatorPublisher()` returns a publisher for the user's sync to authenticator settings. func test_syncToAuthenticatorPublisher() async throws { await subject.addAccount(.fixture(profile: .fixture(userId: "1"))) diff --git a/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStore.swift b/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStore.swift index 0d64aed3c..2198c263f 100644 --- a/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStore.swift +++ b/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStore.swift @@ -22,6 +22,9 @@ protocol AppSettingsStore: AnyObject { /// The app's theme. var appTheme: String? { get set } + /// Whether the archive onboarding has been shown. + var archiveOnboardingShown: Bool { get set } + /// The last published active user ID by `activeAccountIdPublisher` in the current process. /// If this is different than the active user ID in the `State`, the active user was likely /// switched in an extension and the main app should update accordingly. @@ -767,6 +770,7 @@ extension DefaultAppSettingsStore: AppSettingsStore, ConfigSettingsStore { case appLocale case appRehydrationState(userId: String) case appTheme + case archiveOnboardingShown case biometricAuthEnabled(userId: String) case clearClipboardValue(userId: String) case connectToWatch(userId: String) @@ -840,6 +844,8 @@ extension DefaultAppSettingsStore: AppSettingsStore, ConfigSettingsStore { "appRehydrationState_\(userId)" case .appTheme: "theme" + case .archiveOnboardingShown: + "archiveOnboardingShown" case let .biometricAuthEnabled(userId): "biometricUnlock_\(userId)" case let .clearClipboardValue(userId): @@ -955,6 +961,11 @@ extension DefaultAppSettingsStore: AppSettingsStore, ConfigSettingsStore { set { store(newValue, for: .appTheme) } } + var archiveOnboardingShown: Bool { + get { fetch(for: .archiveOnboardingShown) } + set { store(newValue, for: .archiveOnboardingShown) } + } + var cachedActiveUserId: String? { activeAccountIdSubject.value } diff --git a/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStoreTests.swift b/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStoreTests.swift index 8a208b8e7..d1fabc57d 100644 --- a/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStoreTests.swift +++ b/BitwardenShared/Core/Platform/Services/Stores/AppSettingsStoreTests.swift @@ -300,6 +300,22 @@ class AppSettingsStoreTests: BitwardenTestCase { // swiftlint:disable:this type_ XCTAssertNil(userDefaults.string(forKey: "bwPreferencesStorage:theme")) } + /// `archiveOnboardingShown` returns `false` if there isn't a previously stored value. + func test_archiveOnboardingShown_isInitiallyFalse() { + XCTAssertFalse(subject.archiveOnboardingShown) + } + + /// `archiveOnboardingShown` can be used to get and set the persisted value in user defaults. + func test_archiveOnboardingShown_withValue() { + subject.archiveOnboardingShown = true + XCTAssertTrue(subject.archiveOnboardingShown) + XCTAssertTrue(userDefaults.bool(forKey: "bwPreferencesStorage:archiveOnboardingShown")) + + subject.archiveOnboardingShown = false + XCTAssertFalse(subject.archiveOnboardingShown) + XCTAssertFalse(userDefaults.bool(forKey: "bwPreferencesStorage:archiveOnboardingShown")) + } + /// `cachedActiveUserId` returns `nil` if there isn't a cached active user. func test_cachedActiveUserId_isInitiallyNil() { XCTAssertNil(subject.cachedActiveUserId) diff --git a/BitwardenShared/Core/Platform/Services/Stores/TestHelpers/MockAppSettingsStore.swift b/BitwardenShared/Core/Platform/Services/Stores/TestHelpers/MockAppSettingsStore.swift index 6a345c08d..9d18d7c62 100644 --- a/BitwardenShared/Core/Platform/Services/Stores/TestHelpers/MockAppSettingsStore.swift +++ b/BitwardenShared/Core/Platform/Services/Stores/TestHelpers/MockAppSettingsStore.swift @@ -19,6 +19,7 @@ class MockAppSettingsStore: AppSettingsStore { // swiftlint:disable:this type_bo var appLocale: String? var appRehydrationState = [String: AppRehydrationState]() var appTheme: String? + var archiveOnboardingShown = false var cachedActiveUserId: String? var disableWebIcons = false var flightRecorderData: FlightRecorderData? diff --git a/BitwardenShared/Core/Platform/Services/TestHelpers/MockStateService.swift b/BitwardenShared/Core/Platform/Services/TestHelpers/MockStateService.swift index bbc75d02e..cd1c9e594 100644 --- a/BitwardenShared/Core/Platform/Services/TestHelpers/MockStateService.swift +++ b/BitwardenShared/Core/Platform/Services/TestHelpers/MockStateService.swift @@ -26,6 +26,7 @@ class MockStateService: StateService, ActiveAccountStateProvider { // swiftlint: var appLanguage: LanguageOption = .default var appRehydrationState = [String: AppRehydrationState]() var appTheme: AppTheme? + var archiveOnboardingShown = false var biometricsEnabled = [String: Bool]() var capturedUserId: String? var clearClipboardValues = [String: ClearClipboardValue]() @@ -218,6 +219,16 @@ class MockStateService: StateService, ActiveAccountStateProvider { // swiftlint: addSitePromptShown } + func getAllowSyncOnRefresh(userId: String?) async throws -> Bool { + let userId = try unwrapUserId(userId) + return allowSyncOnRefresh[userId] ?? false + } + + func getAllowUniversalClipboard(userId: String?) async throws -> Bool { + let userId = try unwrapUserId(userId) + return allowUniversalClipboard[userId] ?? false + } + func getAppRehydrationState(userId: String?) async throws -> BitwardenShared.AppRehydrationState? { let userId = try unwrapUserId(userId) return appRehydrationState[userId] @@ -227,14 +238,8 @@ class MockStateService: StateService, ActiveAccountStateProvider { // swiftlint: appTheme ?? .default } - func getAllowSyncOnRefresh(userId: String?) async throws -> Bool { - let userId = try unwrapUserId(userId) - return allowSyncOnRefresh[userId] ?? false - } - - func getAllowUniversalClipboard(userId: String?) async throws -> Bool { - let userId = try unwrapUserId(userId) - return allowUniversalClipboard[userId] ?? false + func getArchiveOnboardingShown() async -> Bool { + archiveOnboardingShown } func getClearClipboardValue(userId: String?) async throws -> ClearClipboardValue { @@ -540,6 +545,10 @@ class MockStateService: StateService, ActiveAccountStateProvider { // swiftlint: self.appTheme = appTheme } + func setArchiveOnboardingShown(_ shown: Bool) async { + archiveOnboardingShown = shown + } + func setClearClipboardValue(_ clearClipboardValue: ClearClipboardValue?, userId: String?) async throws { try clearClipboardResult.get() let userId = try unwrapUserId(userId) diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListAction.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListAction.swift index f7725df66..7e09cf726 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListAction.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListAction.swift @@ -24,6 +24,9 @@ enum VaultListAction: Equatable { /// The vault list disappeared from the screen. case disappeared + /// The user tapped the button to go to archive. + case goToArchive + /// An item in the vault was pressed. case itemPressed(item: VaultListItem) diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListEffect.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListEffect.swift index 5d3daf134..b048db9d7 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListEffect.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListEffect.swift @@ -8,6 +8,9 @@ enum VaultListEffect: Equatable { /// Check if the user is eligible for an app review prompt. case checkAppReviewEligibility + /// The user tapped the dismiss button on the Archive Onboarding action card. + case dismissArchiveOnboardingActionCard + /// The flight recorder toast banner was dismissed. case dismissFlightRecorderToastBanner diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessor.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessor.swift index e8f0fc922..74a81d73a 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessor.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessor.swift @@ -99,6 +99,9 @@ final class VaultListProcessor: StateProcessor< } else { state.isEligibleForAppReview = false } + case .dismissArchiveOnboardingActionCard: + state.shouldShowArchiveOnboardingActionCard = false + await services.stateService.setArchiveOnboardingShown(true) case .dismissFlightRecorderToastBanner: await dismissFlightRecorderToastBanner() case .dismissImportLoginsActionCard: @@ -145,12 +148,20 @@ final class VaultListProcessor: StateProcessor< coordinator.navigate(to: .addFolder) case let .addItemPressed(type): addItem(type: type) + case .appReviewPromptShown: + state.isEligibleForAppReview = false + Task { + await services.reviewPromptService.setReviewPromptShownVersion() + await services.reviewPromptService.clearUserActions() + } case .clearURL: state.url = nil case .copyTOTPCode: break case .disappeared: reviewPromptTask?.cancel() + case .goToArchive: + coordinator.navigate(to: .group(.archive, filter: state.vaultFilterType)) case let .itemPressed(item): handleItemTapped(item) case .navigateToFlightRecorderSettings: @@ -172,12 +183,6 @@ final class VaultListProcessor: StateProcessor< state.searchText = newValue case let .searchVaultFilterChanged(newValue): state.searchVaultFilterType = newValue - case .appReviewPromptShown: - state.isEligibleForAppReview = false - Task { - await services.reviewPromptService.setReviewPromptShownVersion() - await services.reviewPromptService.clearUserActions() - } case .showImportLogins: coordinator.navigate(to: .importLogins) case let .toastShown(newValue): @@ -216,6 +221,7 @@ extension VaultListProcessor { await checkPendingLoginRequests() await checkPersonalOwnershipPolicy() await loadItemTypesUserCanCreate() + state.shouldShowArchiveOnboardingActionCard = await services.stateService.shouldDoArchiveOnboarding() } /// Checks if the user needs to update their KDF settings. diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift index fe4b1f463..bfe759331 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift @@ -537,6 +537,30 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertEqual(stateService.notificationsLastRegistrationDates["1"], timeProvider.presentTime) } + /// `perform(_:)` with `.appeared` updates whether to show the archive onboarding card. + @MainActor + func test_perform_appeared_loadArchiveOnboarding() async { + stateService.doesActiveAccountHavePremiumResult = true + stateService.archiveOnboardingShown = false + + await subject.perform(.appeared) + + XCTAssertTrue(subject.state.shouldShowArchiveOnboardingActionCard) + } + + /// `perform(_:)` with `.dismissArchiveOnboardingActionCard` dismisses the archive onboarding card + /// and sets the archive onboarding shown property to true. + @MainActor + func test_perform_dismissArchiveOnboardingActionCard() async { + subject.state.shouldShowArchiveOnboardingActionCard = true + XCTAssertFalse(stateService.archiveOnboardingShown) + + await subject.perform(.dismissArchiveOnboardingActionCard) + + XCTAssertFalse(subject.state.shouldShowArchiveOnboardingActionCard) + XCTAssertTrue(stateService.archiveOnboardingShown) + } + /// `perform(_:)` with `.dismissFlightRecorderToastBanner` hides the flight recorder toast banner. @MainActor func test_perform_dismissFlightRecorderToastBanner() async { @@ -1815,6 +1839,13 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ XCTAssertTrue(subject.reviewPromptTask!.isCancelled) } + /// `receive(_:)` with `.goToArchive` navigates to archive group. + @MainActor + func test_receive_goToArchive() { + subject.receive(.goToArchive) + XCTAssertEqual(coordinator.routes.last, .group(.archive, filter: .allVaults)) + } + /// `receive(_:)` with `.itemPressed` navigates to the `.viewItem` route for a cipher. @MainActor func test_receive_itemPressed_cipher() async throws { diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListState.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListState.swift index 32942ae39..2cf6cf435 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListState.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListState.swift @@ -81,6 +81,9 @@ struct VaultListState: Equatable { ) } + /// Whether the Archive Onboarding action card should be shown. + var shouldShowArchiveOnboardingActionCard: Bool = false + /// Whether the import logins action card should be shown. var shouldShowImportLoginsActionCard: Bool { importLoginsSetupProgress == .incomplete diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView+SnapshotTests.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView+SnapshotTests.swift index a4207edc4..8611b8ecc 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView+SnapshotTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView+SnapshotTests.swift @@ -13,6 +13,56 @@ import XCTest // MARK: - VaultListViewTests class VaultListViewTests: BitwardenTestCase { + // MARK: Static properties + + /// An array of vault list sections with default data to fill the vault. + static var defaultVaultData: [VaultListSection] { + [ + VaultListSection( + id: "", + items: [ + .fixture(cipherListView: .fixture( + login: .fixture(username: "email@example.com"), + name: "Example", + subtitle: "email@example.com", + )), + .fixture(cipherListView: .fixture(id: "12", name: "Example", type: .secureNote)), + .fixture(cipherListView: .fixture( + id: "13", + organizationId: "1", + login: .fixture(username: "user@bitwarden.com"), + name: "Bitwarden", + subtitle: "user@bitwarden.com", + attachments: 1, + )), + ], + name: "Favorites", + ), + VaultListSection( + id: "2", + items: [ + VaultListItem( + id: "21", + itemType: .group(.login, 123), + ), + VaultListItem( + id: "22", + itemType: .group(.card, 25), + ), + VaultListItem( + id: "23", + itemType: .group(.identity, 1), + ), + VaultListItem( + id: "24", + itemType: .group(.secureNote, 0), + ), + ], + name: "Types", + ), + ] + } + // MARK: Properties var processor: MockProcessor! @@ -96,50 +146,17 @@ class VaultListViewTests: BitwardenTestCase { @MainActor func disabletest_snapshot_myVault() { - processor.state.loadingState = .data([ - VaultListSection( - id: "", - items: [ - .fixture(cipherListView: .fixture( - login: .fixture(username: "email@example.com"), - name: "Example", - subtitle: "email@example.com", - )), - .fixture(cipherListView: .fixture(id: "12", name: "Example", type: .secureNote)), - .fixture(cipherListView: .fixture( - id: "13", - organizationId: "1", - login: .fixture(username: "user@bitwarden.com"), - name: "Bitwarden", - subtitle: "user@bitwarden.com", - attachments: 1, - )), - ], - name: "Favorites", - ), - VaultListSection( - id: "2", - items: [ - VaultListItem( - id: "21", - itemType: .group(.login, 123), - ), - VaultListItem( - id: "22", - itemType: .group(.card, 25), - ), - VaultListItem( - id: "23", - itemType: .group(.identity, 1), - ), - VaultListItem( - id: "24", - itemType: .group(.secureNote, 0), - ), - ], - name: "Types", - ), - ]) + processor.state.loadingState = .data(VaultListViewTests.defaultVaultData) + assertSnapshots( + of: subject, + as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5], + ) + } + + @MainActor + func disabletest_snapshot_myVaultArchiveOnboarding() { + processor.state.shouldShowArchiveOnboardingActionCard = true + processor.state.loadingState = .data(VaultListViewTests.defaultVaultData) assertSnapshots( of: subject, as: [.defaultPortrait, .defaultPortraitDark, .defaultPortraitAX5], diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift index 84aee2d0a..79a8b8531 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift @@ -75,6 +75,24 @@ private struct SearchableVaultListView: View { // MARK: Private Properties + /// The action card for importing login items. + @ViewBuilder private var archiveOnboardingActionCard: some View { + if store.state.shouldShowArchiveOnboardingActionCard { + ActionCard( + title: Localizations.introducingArchive, + message: Localizations.keepYtemsYouDontNeedRightNowSafeButOutOfSight, + actionButtonState: ActionCard.ButtonState(title: Localizations.goToArchive) { + store.send(.goToArchive) + }, + dismissButtonState: ActionCard.ButtonState(title: Localizations.dismiss) { + await store.perform(.dismissArchiveOnboardingActionCard) + }, + ) { + SharedAsset.Icons.archive24.swiftUIImage.foregroundStyle(SharedAsset.Colors.iconSecondary.swiftUIColor) + } + } + } + /// A view that displays the empty vault interface. @ViewBuilder private var emptyVault: some View { VStack(spacing: 24) { @@ -250,6 +268,8 @@ private struct SearchableVaultListView: View { @ViewBuilder private func vaultContents(with sections: [VaultListSection]) -> some View { VStack(spacing: 20) { + archiveOnboardingActionCard + vaultFilterRow ForEach(sections) { section in