mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 17:46:07 -06:00
[PM-26063] Move the Flight Recorder's toast banner to BitwardenKit (#2149)
This commit is contained in:
parent
b5e7033d54
commit
5bf7e24f97
@ -0,0 +1,28 @@
|
||||
// MARK: - FlightRecorderToastBannerState
|
||||
|
||||
/// The state for the flight recorder toast banner that displays when a flight recorder log is active.
|
||||
///
|
||||
public struct FlightRecorderToastBannerState: Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// The active flight recorder log metadata, or `nil` if the flight recorder isn't active.
|
||||
public var activeLog: FlightRecorderData.LogMetadata?
|
||||
|
||||
// MARK: Computed Properties
|
||||
|
||||
/// Whether the flight recorder toast banner is visible.
|
||||
public var isToastBannerVisible: Bool {
|
||||
!(activeLog?.isBannerDismissed ?? true)
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize a `FlightRecorderToastBannerState`.
|
||||
///
|
||||
/// - Parameter activeLog: The active flight recorder log metadata, or `nil` if the flight recorder
|
||||
/// isn't active.
|
||||
///
|
||||
public init(activeLog: FlightRecorderData.LogMetadata? = nil) {
|
||||
self.activeLog = activeLog
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenKit
|
||||
|
||||
class FlightRecorderToastBannerStateTests: BitwardenTestCase {
|
||||
// MARK: Tests
|
||||
|
||||
/// `isToastBannerVisible` returns `false` when `activeLog` is `nil`.
|
||||
func test_isToastBannerVisible_activeLogNil() {
|
||||
let subject = FlightRecorderToastBannerState(activeLog: nil)
|
||||
|
||||
XCTAssertFalse(subject.isToastBannerVisible)
|
||||
}
|
||||
|
||||
/// `isToastBannerVisible` returns `true` when `activeLog` has `isBannerDismissed` as `false`.
|
||||
func test_isToastBannerVisible_notDismissed() {
|
||||
let activeLog = FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: Date(year: 2025, month: 11, day: 13),
|
||||
)
|
||||
let subject = FlightRecorderToastBannerState(activeLog: activeLog)
|
||||
|
||||
XCTAssertTrue(subject.isToastBannerVisible)
|
||||
}
|
||||
|
||||
/// `isToastBannerVisible` returns `false` when `activeLog` has `isBannerDismissed` as `true`.
|
||||
func test_isToastBannerVisible_dismissed() {
|
||||
var activeLog = FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: Date(year: 2025, month: 11, day: 13),
|
||||
)
|
||||
activeLog.isBannerDismissed = true
|
||||
let subject = FlightRecorderToastBannerState(activeLog: activeLog)
|
||||
|
||||
XCTAssertFalse(subject.isToastBannerVisible)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import BitwardenResources
|
||||
import SwiftUI
|
||||
|
||||
public extension View {
|
||||
/// Displays a toast banner indicating that the flight recorder is active.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - activeLog: The active flight recorder log metadata used to display the end date/time.
|
||||
/// - additionalBottomPadding: Additional bottom padding to apply to the toast banner.
|
||||
/// - isVisible: A binding to control the visibility of the toast banner.
|
||||
/// - goToSettingsAction: The action to perform when the "Go to Settings" button is tapped.
|
||||
///
|
||||
/// - Returns: A view with the flight recorder toast banner applied.
|
||||
///
|
||||
func flightRecorderToastBanner(
|
||||
activeLog: FlightRecorderData.LogMetadata?,
|
||||
additionalBottomPadding: CGFloat = 0,
|
||||
isVisible: Binding<Bool>,
|
||||
goToSettingsAction: @escaping () -> Void,
|
||||
) -> some View {
|
||||
toastBanner(
|
||||
title: Localizations.flightRecorderOn,
|
||||
subtitle: {
|
||||
guard let activeLog else { return "" }
|
||||
return Localizations.flightRecorderWillBeActiveUntilDescriptionLong(
|
||||
activeLog.formattedEndDate,
|
||||
activeLog.formattedEndTime,
|
||||
)
|
||||
}(),
|
||||
additionalBottomPadding: additionalBottomPadding,
|
||||
isVisible: isVisible,
|
||||
) {
|
||||
Button(Localizations.goToSettings) {
|
||||
goToSettingsAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -267,7 +267,6 @@ extension VaultListProcessor {
|
||||
/// Dismisses the flight recorder toast banner for the active user.
|
||||
///
|
||||
private func dismissFlightRecorderToastBanner() async {
|
||||
state.isFlightRecorderToastBannerVisible = false
|
||||
await services.flightRecorder.setFlightRecorderBannerDismissed()
|
||||
}
|
||||
|
||||
@ -499,8 +498,7 @@ extension VaultListProcessor {
|
||||
///
|
||||
private func streamFlightRecorderLog() async {
|
||||
for await log in await services.flightRecorder.activeLogPublisher().values {
|
||||
state.activeFlightRecorderLog = log
|
||||
state.isFlightRecorderToastBannerVisible = !(log?.isBannerDismissed ?? true)
|
||||
state.flightRecorderToastBanner.activeLog = log
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -510,11 +510,9 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
|
||||
@MainActor
|
||||
func test_perform_dismissFlightRecorderToastBanner() async {
|
||||
stateService.activeAccount = .fixture()
|
||||
subject.state.isFlightRecorderToastBannerVisible = true
|
||||
|
||||
await subject.perform(.dismissFlightRecorderToastBanner)
|
||||
|
||||
XCTAssertFalse(subject.state.isFlightRecorderToastBannerVisible)
|
||||
XCTAssertTrue(flightRecorder.setFlightRecorderBannerDismissedCalled)
|
||||
}
|
||||
|
||||
@ -870,31 +868,12 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
|
||||
defer { task.cancel() }
|
||||
|
||||
flightRecorder.activeLogSubject.send(FlightRecorderData.LogMetadata(duration: .eightHours, startDate: .now))
|
||||
try await waitForAsync { self.subject.state.isFlightRecorderToastBannerVisible }
|
||||
XCTAssertEqual(subject.state.isFlightRecorderToastBannerVisible, true)
|
||||
try await waitForAsync { self.subject.state.flightRecorderToastBanner.isToastBannerVisible }
|
||||
XCTAssertEqual(subject.state.flightRecorderToastBanner.isToastBannerVisible, true)
|
||||
|
||||
flightRecorder.activeLogSubject.send(nil)
|
||||
try await waitForAsync { !self.subject.state.isFlightRecorderToastBannerVisible }
|
||||
XCTAssertEqual(subject.state.isFlightRecorderToastBannerVisible, false)
|
||||
}
|
||||
|
||||
/// `perform(_:)` with `.streamFlightRecorderLog` streams the flight recorder log but doesn't
|
||||
/// display the flight recorder banner if the user has dismissed it previously.
|
||||
@MainActor
|
||||
func test_perform_streamFlightRecorderLog_userDismissed() async throws {
|
||||
stateService.activeAccount = .fixture()
|
||||
|
||||
let task = Task {
|
||||
await subject.perform(.streamFlightRecorderLog)
|
||||
}
|
||||
defer { task.cancel() }
|
||||
|
||||
var log = FlightRecorderData.LogMetadata(duration: .eightHours, startDate: .now)
|
||||
log.isBannerDismissed = true
|
||||
flightRecorder.activeLogSubject.send(log)
|
||||
|
||||
try await waitForAsync { self.subject.state.activeFlightRecorderLog != nil }
|
||||
XCTAssertEqual(subject.state.isFlightRecorderToastBannerVisible, false)
|
||||
try await waitForAsync { !self.subject.state.flightRecorderToastBanner.isToastBannerVisible }
|
||||
XCTAssertEqual(subject.state.flightRecorderToastBanner.isToastBannerVisible, false)
|
||||
}
|
||||
|
||||
/// `perform(_:)` with `.streamOrganizations` updates the state's organizations whenever it changes.
|
||||
|
||||
@ -9,15 +9,15 @@ import Foundation
|
||||
struct VaultListState: Equatable {
|
||||
// MARK: Properties
|
||||
|
||||
/// The active flight recorder log metadata, or `nil` if the flight recorder isn't active.
|
||||
var activeFlightRecorderLog: FlightRecorderData.LogMetadata?
|
||||
|
||||
/// List of available item type for creation.
|
||||
var itemTypesUserCanCreate: [CipherType] = CipherType.canCreateCases
|
||||
|
||||
/// Whether the vault filter can be shown.
|
||||
var canShowVaultFilter = true
|
||||
|
||||
/// The state for the flight recorder toast banner displayed in the item list.
|
||||
var flightRecorderToastBanner = FlightRecorderToastBannerState()
|
||||
|
||||
/// The base url used to fetch icons.
|
||||
var iconBaseURL: URL?
|
||||
|
||||
@ -30,9 +30,6 @@ struct VaultListState: Equatable {
|
||||
/// Whether the user is eligible for an app review prompt.
|
||||
var isEligibleForAppReview: Bool = false
|
||||
|
||||
/// Whether the flight recorder toast banner is visible.
|
||||
var isFlightRecorderToastBannerVisible = false
|
||||
|
||||
/// The loading state of the My Vault screen.
|
||||
var loadingState: LoadingState<[VaultListSection]> = .loading(nil)
|
||||
|
||||
|
||||
@ -80,8 +80,7 @@ class VaultListViewTests: BitwardenTestCase {
|
||||
@MainActor
|
||||
func disabletest_snapshot_flightRecorderToastBanner() {
|
||||
processor.state.loadingState = .data([])
|
||||
processor.state.isFlightRecorderToastBannerVisible = true
|
||||
processor.state.activeFlightRecorderLog = FlightRecorderData.LogMetadata(
|
||||
processor.state.flightRecorderToastBanner.activeLog = FlightRecorderData.LogMetadata(
|
||||
duration: .twentyFourHours,
|
||||
startDate: Date(year: 2025, month: 4, day: 3),
|
||||
)
|
||||
|
||||
@ -231,7 +231,10 @@ class VaultListViewTests: BitwardenTestCase {
|
||||
/// `.navigateToFlightRecorderSettings` action.
|
||||
@MainActor
|
||||
func test_toastBannerGoToSettings_tap() async throws {
|
||||
processor.state.isFlightRecorderToastBannerVisible = true
|
||||
processor.state.flightRecorderToastBanner.activeLog = FlightRecorderData.LogMetadata(
|
||||
duration: .eightHours,
|
||||
startDate: Date(year: 2025, month: 4, day: 3),
|
||||
)
|
||||
let button = try subject.inspect().find(button: Localizations.goToSettings)
|
||||
try button.tap()
|
||||
XCTAssertEqual(processor.dispatchedActions, [.navigateToFlightRecorderSettings])
|
||||
|
||||
@ -51,25 +51,17 @@ private struct SearchableVaultListView: View {
|
||||
),
|
||||
additionalBottomPadding: FloatingActionButton.bottomOffsetPadding,
|
||||
)
|
||||
.toastBanner(
|
||||
title: Localizations.flightRecorderOn,
|
||||
subtitle: {
|
||||
guard let log = store.state.activeFlightRecorderLog else { return "" }
|
||||
return Localizations.flightRecorderWillBeActiveUntilDescriptionLong(
|
||||
log.formattedEndDate,
|
||||
log.formattedEndTime,
|
||||
)
|
||||
}(),
|
||||
.flightRecorderToastBanner(
|
||||
activeLog: store.state.flightRecorderToastBanner.activeLog,
|
||||
additionalBottomPadding: FloatingActionButton.bottomOffsetPadding,
|
||||
isVisible: store.bindingAsync(
|
||||
get: \.isFlightRecorderToastBannerVisible,
|
||||
get: \.flightRecorderToastBanner.isToastBannerVisible,
|
||||
perform: { _ in .dismissFlightRecorderToastBanner },
|
||||
),
|
||||
) {
|
||||
Button(Localizations.goToSettings) {
|
||||
goToSettingsAction: {
|
||||
store.send(.navigateToFlightRecorderSettings)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.onChange(of: store.state.url) { newValue in
|
||||
guard let url = newValue else { return }
|
||||
openURL(url)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user