Skip to content

Commit 5bf7e24

Browse files
[PM-26063] Move the Flight Recorder's toast banner to BitwardenKit (#2149)
1 parent b5e7033 commit 5bf7e24

File tree

9 files changed

+122
-51
lines changed

9 files changed

+122
-51
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// MARK: - FlightRecorderToastBannerState
2+
3+
/// The state for the flight recorder toast banner that displays when a flight recorder log is active.
4+
///
5+
public struct FlightRecorderToastBannerState: Equatable {
6+
// MARK: Properties
7+
8+
/// The active flight recorder log metadata, or `nil` if the flight recorder isn't active.
9+
public var activeLog: FlightRecorderData.LogMetadata?
10+
11+
// MARK: Computed Properties
12+
13+
/// Whether the flight recorder toast banner is visible.
14+
public var isToastBannerVisible: Bool {
15+
!(activeLog?.isBannerDismissed ?? true)
16+
}
17+
18+
// MARK: Initialization
19+
20+
/// Initialize a `FlightRecorderToastBannerState`.
21+
///
22+
/// - Parameter activeLog: The active flight recorder log metadata, or `nil` if the flight recorder
23+
/// isn't active.
24+
///
25+
public init(activeLog: FlightRecorderData.LogMetadata? = nil) {
26+
self.activeLog = activeLog
27+
}
28+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import XCTest
2+
3+
@testable import BitwardenKit
4+
5+
class FlightRecorderToastBannerStateTests: BitwardenTestCase {
6+
// MARK: Tests
7+
8+
/// `isToastBannerVisible` returns `false` when `activeLog` is `nil`.
9+
func test_isToastBannerVisible_activeLogNil() {
10+
let subject = FlightRecorderToastBannerState(activeLog: nil)
11+
12+
XCTAssertFalse(subject.isToastBannerVisible)
13+
}
14+
15+
/// `isToastBannerVisible` returns `true` when `activeLog` has `isBannerDismissed` as `false`.
16+
func test_isToastBannerVisible_notDismissed() {
17+
let activeLog = FlightRecorderData.LogMetadata(
18+
duration: .eightHours,
19+
startDate: Date(year: 2025, month: 11, day: 13),
20+
)
21+
let subject = FlightRecorderToastBannerState(activeLog: activeLog)
22+
23+
XCTAssertTrue(subject.isToastBannerVisible)
24+
}
25+
26+
/// `isToastBannerVisible` returns `false` when `activeLog` has `isBannerDismissed` as `true`.
27+
func test_isToastBannerVisible_dismissed() {
28+
var activeLog = FlightRecorderData.LogMetadata(
29+
duration: .eightHours,
30+
startDate: Date(year: 2025, month: 11, day: 13),
31+
)
32+
activeLog.isBannerDismissed = true
33+
let subject = FlightRecorderToastBannerState(activeLog: activeLog)
34+
35+
XCTAssertFalse(subject.isToastBannerVisible)
36+
}
37+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import BitwardenResources
2+
import SwiftUI
3+
4+
public extension View {
5+
/// Displays a toast banner indicating that the flight recorder is active.
6+
///
7+
/// - Parameters:
8+
/// - activeLog: The active flight recorder log metadata used to display the end date/time.
9+
/// - additionalBottomPadding: Additional bottom padding to apply to the toast banner.
10+
/// - isVisible: A binding to control the visibility of the toast banner.
11+
/// - goToSettingsAction: The action to perform when the "Go to Settings" button is tapped.
12+
///
13+
/// - Returns: A view with the flight recorder toast banner applied.
14+
///
15+
func flightRecorderToastBanner(
16+
activeLog: FlightRecorderData.LogMetadata?,
17+
additionalBottomPadding: CGFloat = 0,
18+
isVisible: Binding<Bool>,
19+
goToSettingsAction: @escaping () -> Void,
20+
) -> some View {
21+
toastBanner(
22+
title: Localizations.flightRecorderOn,
23+
subtitle: {
24+
guard let activeLog else { return "" }
25+
return Localizations.flightRecorderWillBeActiveUntilDescriptionLong(
26+
activeLog.formattedEndDate,
27+
activeLog.formattedEndTime,
28+
)
29+
}(),
30+
additionalBottomPadding: additionalBottomPadding,
31+
isVisible: isVisible,
32+
) {
33+
Button(Localizations.goToSettings) {
34+
goToSettingsAction()
35+
}
36+
}
37+
}
38+
}

BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessor.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,6 @@ extension VaultListProcessor {
267267
/// Dismisses the flight recorder toast banner for the active user.
268268
///
269269
private func dismissFlightRecorderToastBanner() async {
270-
state.isFlightRecorderToastBannerVisible = false
271270
await services.flightRecorder.setFlightRecorderBannerDismissed()
272271
}
273272

@@ -499,8 +498,7 @@ extension VaultListProcessor {
499498
///
500499
private func streamFlightRecorderLog() async {
501500
for await log in await services.flightRecorder.activeLogPublisher().values {
502-
state.activeFlightRecorderLog = log
503-
state.isFlightRecorderToastBannerVisible = !(log?.isBannerDismissed ?? true)
501+
state.flightRecorderToastBanner.activeLog = log
504502
}
505503
}
506504

BitwardenShared/UI/Vault/Vault/VaultList/VaultListProcessorTests.swift

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -510,11 +510,9 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
510510
@MainActor
511511
func test_perform_dismissFlightRecorderToastBanner() async {
512512
stateService.activeAccount = .fixture()
513-
subject.state.isFlightRecorderToastBannerVisible = true
514513

515514
await subject.perform(.dismissFlightRecorderToastBanner)
516515

517-
XCTAssertFalse(subject.state.isFlightRecorderToastBannerVisible)
518516
XCTAssertTrue(flightRecorder.setFlightRecorderBannerDismissedCalled)
519517
}
520518

@@ -870,31 +868,12 @@ class VaultListProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
870868
defer { task.cancel() }
871869

872870
flightRecorder.activeLogSubject.send(FlightRecorderData.LogMetadata(duration: .eightHours, startDate: .now))
873-
try await waitForAsync { self.subject.state.isFlightRecorderToastBannerVisible }
874-
XCTAssertEqual(subject.state.isFlightRecorderToastBannerVisible, true)
871+
try await waitForAsync { self.subject.state.flightRecorderToastBanner.isToastBannerVisible }
872+
XCTAssertEqual(subject.state.flightRecorderToastBanner.isToastBannerVisible, true)
875873

876874
flightRecorder.activeLogSubject.send(nil)
877-
try await waitForAsync { !self.subject.state.isFlightRecorderToastBannerVisible }
878-
XCTAssertEqual(subject.state.isFlightRecorderToastBannerVisible, false)
879-
}
880-
881-
/// `perform(_:)` with `.streamFlightRecorderLog` streams the flight recorder log but doesn't
882-
/// display the flight recorder banner if the user has dismissed it previously.
883-
@MainActor
884-
func test_perform_streamFlightRecorderLog_userDismissed() async throws {
885-
stateService.activeAccount = .fixture()
886-
887-
let task = Task {
888-
await subject.perform(.streamFlightRecorderLog)
889-
}
890-
defer { task.cancel() }
891-
892-
var log = FlightRecorderData.LogMetadata(duration: .eightHours, startDate: .now)
893-
log.isBannerDismissed = true
894-
flightRecorder.activeLogSubject.send(log)
895-
896-
try await waitForAsync { self.subject.state.activeFlightRecorderLog != nil }
897-
XCTAssertEqual(subject.state.isFlightRecorderToastBannerVisible, false)
875+
try await waitForAsync { !self.subject.state.flightRecorderToastBanner.isToastBannerVisible }
876+
XCTAssertEqual(subject.state.flightRecorderToastBanner.isToastBannerVisible, false)
898877
}
899878

900879
/// `perform(_:)` with `.streamOrganizations` updates the state's organizations whenever it changes.

BitwardenShared/UI/Vault/Vault/VaultList/VaultListState.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import Foundation
99
struct VaultListState: Equatable {
1010
// MARK: Properties
1111

12-
/// The active flight recorder log metadata, or `nil` if the flight recorder isn't active.
13-
var activeFlightRecorderLog: FlightRecorderData.LogMetadata?
14-
1512
/// List of available item type for creation.
1613
var itemTypesUserCanCreate: [CipherType] = CipherType.canCreateCases
1714

1815
/// Whether the vault filter can be shown.
1916
var canShowVaultFilter = true
2017

18+
/// The state for the flight recorder toast banner displayed in the item list.
19+
var flightRecorderToastBanner = FlightRecorderToastBannerState()
20+
2121
/// The base url used to fetch icons.
2222
var iconBaseURL: URL?
2323

@@ -30,9 +30,6 @@ struct VaultListState: Equatable {
3030
/// Whether the user is eligible for an app review prompt.
3131
var isEligibleForAppReview: Bool = false
3232

33-
/// Whether the flight recorder toast banner is visible.
34-
var isFlightRecorderToastBannerVisible = false
35-
3633
/// The loading state of the My Vault screen.
3734
var loadingState: LoadingState<[VaultListSection]> = .loading(nil)
3835

BitwardenShared/UI/Vault/Vault/VaultList/VaultListView+SnapshotTests.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ class VaultListViewTests: BitwardenTestCase {
8080
@MainActor
8181
func disabletest_snapshot_flightRecorderToastBanner() {
8282
processor.state.loadingState = .data([])
83-
processor.state.isFlightRecorderToastBannerVisible = true
84-
processor.state.activeFlightRecorderLog = FlightRecorderData.LogMetadata(
83+
processor.state.flightRecorderToastBanner.activeLog = FlightRecorderData.LogMetadata(
8584
duration: .twentyFourHours,
8685
startDate: Date(year: 2025, month: 4, day: 3),
8786
)

BitwardenShared/UI/Vault/Vault/VaultList/VaultListView+ViewInspectorTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,10 @@ class VaultListViewTests: BitwardenTestCase {
231231
/// `.navigateToFlightRecorderSettings` action.
232232
@MainActor
233233
func test_toastBannerGoToSettings_tap() async throws {
234-
processor.state.isFlightRecorderToastBannerVisible = true
234+
processor.state.flightRecorderToastBanner.activeLog = FlightRecorderData.LogMetadata(
235+
duration: .eightHours,
236+
startDate: Date(year: 2025, month: 4, day: 3),
237+
)
235238
let button = try subject.inspect().find(button: Localizations.goToSettings)
236239
try button.tap()
237240
XCTAssertEqual(processor.dispatchedActions, [.navigateToFlightRecorderSettings])

BitwardenShared/UI/Vault/Vault/VaultList/VaultListView.swift

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,25 +51,17 @@ private struct SearchableVaultListView: View {
5151
),
5252
additionalBottomPadding: FloatingActionButton.bottomOffsetPadding,
5353
)
54-
.toastBanner(
55-
title: Localizations.flightRecorderOn,
56-
subtitle: {
57-
guard let log = store.state.activeFlightRecorderLog else { return "" }
58-
return Localizations.flightRecorderWillBeActiveUntilDescriptionLong(
59-
log.formattedEndDate,
60-
log.formattedEndTime,
61-
)
62-
}(),
54+
.flightRecorderToastBanner(
55+
activeLog: store.state.flightRecorderToastBanner.activeLog,
6356
additionalBottomPadding: FloatingActionButton.bottomOffsetPadding,
6457
isVisible: store.bindingAsync(
65-
get: \.isFlightRecorderToastBannerVisible,
58+
get: \.flightRecorderToastBanner.isToastBannerVisible,
6659
perform: { _ in .dismissFlightRecorderToastBanner },
6760
),
68-
) {
69-
Button(Localizations.goToSettings) {
61+
goToSettingsAction: {
7062
store.send(.navigateToFlightRecorderSettings)
71-
}
72-
}
63+
},
64+
)
7365
.onChange(of: store.state.url) { newValue in
7466
guard let url = newValue else { return }
7567
openURL(url)

0 commit comments

Comments
 (0)