mirror of
https://github.com/bitwarden/ios.git
synced 2026-02-04 02:14:09 -06:00
[PM-29634] feat: Implement archive onboarding (#2277)
Co-authored-by: Katherine Bertelsen <kbertelsen@bitwarden.com>
This commit is contained in:
parent
9db668f0e2
commit
8cb3e7d59c
@ -180,6 +180,15 @@ public struct ActionCard<LeadingContent: View>: 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",
|
||||
|
||||
@ -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.";
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")))
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<VaultListState, VaultListAction, VaultListEffect>!
|
||||
@ -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],
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user