Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BITAU-124] Refresh Item List When Foregrounding the App #178

Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2e5aa95
[BITAU-132] Move to Bitwarden Long Press on Local Items
brant-livefront Oct 24, 2024
5207553
[BITAU-130] Add Option to Manual Key Entry for Adding to Bitwarden
brant-livefront Oct 24, 2024
85c59a8
Updated per PR review: removed BW shortening and renamed hash function
brant-livefront Oct 25, 2024
f664ce3
Merge in latest from brant/BITAU-132-move-to-bitwarden-long-press-on-โ€ฆ
brant-livefront Oct 25, 2024
dfe9ef0
Fixed one missed merge conflict
brant-livefront Oct 25, 2024
016ad2f
Merged in latest from main, fixed conflicts
brant-livefront Oct 25, 2024
7837fe7
[BITAU-131] Add Popup to QR Code Scan to Add Code to Bitwarden
brant-livefront Oct 28, 2024
d060aca
Increase sleep time to allow test to succeed on CI
brant-livefront Oct 28, 2024
46a383f
Respond to PR feedback
brant-livefront Oct 29, 2024
c2acc5c
Removed dead code that Codecov revealed
brant-livefront Oct 29, 2024
bf971bb
Copy changes from the UX team
brant-livefront Oct 29, 2024
73b9869
[BITAU-124] Refersh Item List When Foregrounding the App
brant-livefront Oct 30, 2024
67bae02
Merge branch 'main' into brant/BITAU-130-add-option-to-manual-key-entโ€ฆ
brant-livefront Oct 31, 2024
d50a68b
Merge in latest from main; fix conflicts
brant-livefront Oct 31, 2024
3f01f91
Add tests and handle case where QR code scan is done with the sync noโ€ฆ
brant-livefront Oct 31, 2024
4798034
Merge branch 'brant/BITAU-131-add-popup-to-qr-code-scan-to-add-code-tโ€ฆ
brant-livefront Oct 31, 2024
22c6f77
Added NotificationCenter property and tests to cover NotificationCentโ€ฆ
brant-livefront Oct 31, 2024
2983145
Updated strings to put quotes around default save options
brant-livefront Oct 31, 2024
836e087
Merge branch 'main' into brant/BITAU-130-add-option-to-manual-key-entโ€ฆ
brant-livefront Nov 1, 2024
bcc76d3
Merge branch 'brant/BITAU-130-add-option-to-manual-key-entry-for-addiโ€ฆ
brant-livefront Nov 1, 2024
1be6c0e
Merge branch 'brant/BITAU-131-add-popup-to-qr-code-scan-to-add-code-tโ€ฆ
brant-livefront Nov 1, 2024
d74bbe0
Update with PR suggestions
brant-livefront Nov 6, 2024
d01ffd1
Merged in latest from main
brant-livefront Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

// MARK: - DefaultSaveOption

/// The default location for saving newly added keys via QR code scan and Manual entry.
///
enum DefaultSaveOption: String, Equatable, Menuable {
/// Ask where to save a code each time a QR code is scanned.
case none

/// Save the code locally without showing any prompt.
case saveLocally

/// Take the user to the Bitwarden PM app to save the code without prompt.
case saveToBitwarden

/// All of the cases to show in the menu, in order.
public static let allCases: [Self] = [
.saveToBitwarden,
.saveLocally,
.none,
]

/// The name of the value to display in the menu.
var localizedName: String {
switch self {
case .none:
Localizations.none
case .saveLocally:
Localizations.saveLocally
case .saveToBitwarden:
Localizations.saveToBitwarden
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import XCTest

@testable import AuthenticatorShared

class DefaultSaveOptionTests: AuthenticatorTestCase {
// MARK: Tests

/// `allCases` returns all of the cases in the correct order.
func test_allCases() {
XCTAssertEqual(
DefaultSaveOption.allCases,
[
.saveToBitwarden,
.saveLocally,
.none,
]
)
}

/// `localizedName` returns the correct values.
func test_localizedName() {
XCTAssertEqual(DefaultSaveOption.none.localizedName, Localizations.none)
XCTAssertEqual(DefaultSaveOption.saveLocally.localizedName, Localizations.saveLocally)
XCTAssertEqual(DefaultSaveOption.saveToBitwarden.localizedName, Localizations.saveToBitwarden)
}

/// `rawValue` returns the correct values.
func test_rawValues() {
XCTAssertEqual(DefaultSaveOption.none.rawValue, "none")
XCTAssertEqual(DefaultSaveOption.saveLocally.rawValue, "saveLocally")
XCTAssertEqual(DefaultSaveOption.saveToBitwarden.rawValue, "saveToBitwarden")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Combine
import UIKit

// MARK: - NotificationCenterService

/// A protocol for a `NotificationCenterService` which accesses the app's notification center.
///
protocol NotificationCenterService: AnyObject {
/// A publisher for when the app enters the background.
///
func didEnterBackgroundPublisher() -> AnyPublisher<Void, Never>
victor-livefront marked this conversation as resolved.
Show resolved Hide resolved

/// A publisher for when the app enters the foreground.
///
func willEnterForegroundPublisher() -> AnyPublisher<Void, Never>
}

// MARK: - DefaultNotificationCenterService

/// A default implementation of the `NotificationCenterService` which accesses the app's notification center.
///
class DefaultNotificationCenterService: NotificationCenterService {
// MARK: Properties

/// The NotificationCenter to use in subscribing to notifications.
let notificationCenter: NotificationCenter

// MARK: Initialization

/// Initialize a `DefaultNotificationCenterService`.
///
/// - Parameter notificationCenter: The NotificationCenter to use in subscribing to notifications.
///
init(notificationCenter: NotificationCenter = NotificationCenter.default) {
self.notificationCenter = notificationCenter
}

// MARK: Methods

func didEnterBackgroundPublisher() -> AnyPublisher<Void, Never> {
notificationCenter
.publisher(for: UIApplication.didEnterBackgroundNotification)
.map { _ in }
.eraseToAnyPublisher()
}

func willEnterForegroundPublisher() -> AnyPublisher<Void, Never> {
notificationCenter
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in }
.eraseToAnyPublisher()
brant-livefront marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import XCTest

@testable import AuthenticatorShared

final class NotificationCenterServiceTests: AuthenticatorTestCase {
// MARK: Properties

var notificationCenter: NotificationCenter!
var subject: DefaultNotificationCenterService!

// MARK: Setup & Teardown

override func setUp() {
notificationCenter = NotificationCenter()
subject = DefaultNotificationCenterService(notificationCenter: notificationCenter)
}

override func tearDown() {
notificationCenter = nil
subject = nil
}

// MARK: Tests

/// `didEnterBackgroundPublisher` publishes a notification when the app enters the background.
func testDidEnterBackgroundPublisher() {
let expectation = XCTestExpectation(description: "Application entered background")
let cancellable = subject.didEnterBackgroundPublisher()
.sink { _ in
expectation.fulfill()
}

notificationCenter.post(
name: UIApplication.didEnterBackgroundNotification,
object: nil
)

wait(for: [expectation], timeout: 1)
cancellable.cancel()
}

/// `willEnterForegroundPublisher` publishes a notification when the app will enter the foreground.
func testWillEnterForegroundPublisher() {
let expectation = XCTestExpectation(description: "Application will enter foreground")
let cancellable = subject.willEnterForegroundPublisher()
.sink { _ in
expectation.fulfill()
}

notificationCenter.post(
name: UIApplication.willEnterForegroundNotification,
object: nil
)

wait(for: [expectation], timeout: 1)
cancellable.cancel()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
/// The service used to perform app data migrations.
let migrationService: MigrationService

/// The service used to receive foreground and background notifications.
let notificationCenterService: NotificationCenterService

/// The service used by the application for sharing data with other apps.
let pasteboardService: PasteboardService

Expand Down Expand Up @@ -86,6 +89,7 @@
/// - exportItemsService: The service to export items.
/// - importItemsService: The service to import items.
/// - migrationService: The service to do data migrations
/// - notificationCenterService: The service used to receive foreground and background notifications.
/// - pasteboardService: The service used by the application for sharing data with other apps.
/// - stateService: The service for managing account state.
/// - timeProvider: Provides the present time for TOTP Code Calculation.
Expand All @@ -105,6 +109,7 @@
exportItemsService: ExportItemsService,
importItemsService: ImportItemsService,
migrationService: MigrationService,
notificationCenterService: NotificationCenterService,
pasteboardService: PasteboardService,
stateService: StateService,
timeProvider: TimeProvider,
Expand All @@ -123,6 +128,7 @@
self.exportItemsService = exportItemsService
self.importItemsService = importItemsService
self.migrationService = migrationService
self.notificationCenterService = notificationCenterService
self.pasteboardService = pasteboardService
self.timeProvider = timeProvider
self.stateService = stateService
Expand Down Expand Up @@ -189,6 +195,8 @@
keychainRepository: keychainRepository
)

let notificationCenterService = DefaultNotificationCenterService()

Check warning on line 199 in AuthenticatorShared/Core/Platform/Services/ServiceContainer.swift

View check run for this annotation

Codecov / codecov/patch

AuthenticatorShared/Core/Platform/Services/ServiceContainer.swift#L198-L199

Added lines #L198 - L199 were not covered by tests
let totpService = DefaultTOTPService(
clientVault: clientService.clientVault(),
errorReporter: errorReporter,
Expand Down Expand Up @@ -259,6 +267,7 @@
exportItemsService: exportItemsService,
importItemsService: importItemsService,
migrationService: migrationService,
notificationCenterService: notificationCenterService,

Check warning on line 270 in AuthenticatorShared/Core/Platform/Services/ServiceContainer.swift

View check run for this annotation

Codecov / codecov/patch

AuthenticatorShared/Core/Platform/Services/ServiceContainer.swift#L270

Added line #L270 was not covered by tests
pasteboardService: pasteboardService,
stateService: stateService,
timeProvider: timeProvider,
Expand Down
8 changes: 8 additions & 0 deletions AuthenticatorShared/Core/Platform/Services/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ typealias Services = HasAppSettingsStore
& HasErrorReporter
& HasExportItemsService
& HasImportItemsService
& HasNotificationCenterService
& HasPasteboardService
& HasStateService
& HasTOTPService
Expand Down Expand Up @@ -85,6 +86,13 @@ protocol HasImportItemsService {
var importItemsService: ImportItemsService { get }
}

/// Protocol for an object that provides a `NotificationCenterService`.
///
protocol HasNotificationCenterService {
/// The service used to receive foreground and background notifications.
var notificationCenterService: NotificationCenterService { get }
}

/// Protocol for an object that provides a `PasteboardService`.
///
protocol HasPasteboardService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ protocol AppSettingsStore: AnyObject {
/// Whether to disable the website icons.
var disableWebIcons: Bool { get set }

/// The default save location for new keys.
var defaultSaveOption: DefaultSaveOption { get set }

/// Whether the user has seen the default save options prompt.
var hasSeenDefaultSaveOptionPrompt: Bool { get }

/// Whether the user has seen the welcome tutorial.
var hasSeenWelcomeTutorial: Bool { get set }

Expand Down Expand Up @@ -296,6 +302,7 @@ extension DefaultAppSettingsStore: AppSettingsStore {
case cardClosedState(card: ItemListCard)
case clearClipboardValue(userId: String)
case debugFeatureFlag(name: String)
case defaultSaveOption
case disableWebIcons
case hasSeenWelcomeTutorial
case hasSyncedAccount(name: String)
Expand Down Expand Up @@ -324,6 +331,8 @@ extension DefaultAppSettingsStore: AppSettingsStore {
key = "clearClipboard_\(userId)"
case let .debugFeatureFlag(name):
key = "debugFeatureFlag_\(name)"
case .defaultSaveOption:
key = "defaultSaveOption"
case .disableWebIcons:
key = "disableFavicon"
case .hasSeenWelcomeTutorial:
Expand Down Expand Up @@ -363,6 +372,21 @@ extension DefaultAppSettingsStore: AppSettingsStore {
set { store(newValue, for: .disableWebIcons) }
}

var defaultSaveOption: DefaultSaveOption {
get {
guard let rawValue: String = fetch(for: .defaultSaveOption),
let value = DefaultSaveOption(rawValue: rawValue)
else { return .none }

return value
}
set { store(newValue.rawValue, for: .defaultSaveOption) }
}

var hasSeenDefaultSaveOptionPrompt: Bool {
fetch(for: .defaultSaveOption) != nil
}

var hasSeenWelcomeTutorial: Bool {
get { fetch(for: .hasSeenWelcomeTutorial) }
set { store(newValue, for: .hasSeenWelcomeTutorial) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,39 @@ class AppSettingsStoreTests: AuthenticatorTestCase {
XCTAssertEqual(userDefaults.integer(forKey: "bwaPreferencesStorage:clearClipboard_2"), -1)
}

/// `defaultSaveOption` returns `.none` if there isn't a previously stored value or if a previously
/// stored value is not a valid option
func test_defaultSaveOption_isInitiallyNone() {
XCTAssertEqual(subject.defaultSaveOption, .none)

userDefaults.set("An invalid value", forKey: "bwaPreferencesStorage:defaultSaveOption")
XCTAssertEqual(subject.defaultSaveOption, .none)
}

/// `defaultSaveOption` can be used to get and set the default save option..
func test_defaultSaveOption_withValue() {
subject.defaultSaveOption = .saveToBitwarden
XCTAssertEqual(subject.defaultSaveOption, .saveToBitwarden)
XCTAssertEqual(userDefaults.string(forKey: "bwaPreferencesStorage:defaultSaveOption"), "saveToBitwarden")

subject.defaultSaveOption = .saveLocally
XCTAssertEqual(subject.defaultSaveOption, .saveLocally)
XCTAssertEqual(userDefaults.string(forKey: "bwaPreferencesStorage:defaultSaveOption"), "saveLocally")

subject.defaultSaveOption = .none
XCTAssertEqual(subject.defaultSaveOption, .none)
XCTAssertEqual(userDefaults.string(forKey: "bwaPreferencesStorage:defaultSaveOption"), "none")
}

/// `hasSeenDefaultSaveOptionPrompt` returns `false` if there isn't a 'defaultSaveOption` value stored, and `true`
/// when there is a value stored.
func test_hasSeenDefaultSaveOptionPrompt() {
XCTAssertFalse(subject.hasSeenDefaultSaveOptionPrompt)

subject.defaultSaveOption = .none
XCTAssertTrue(subject.hasSeenDefaultSaveOptionPrompt)
}

/// `disableWebIcons` returns `false` if there isn't a previously stored value.
func test_disableWebIcons_isInitiallyFalse() {
XCTAssertFalse(subject.disableWebIcons)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class MockAppSettingsStore: AppSettingsStore {
var appLocale: String?
var appTheme: String?
var disableWebIcons = false
var defaultSaveOption: DefaultSaveOption = .none
var hasSeenDefaultSaveOptionPrompt = false
var hasSeenWelcomeTutorial = false
var lastUserShouldConnectToWatch = false
var localUserId: String = "localtest"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Combine
import Foundation

@testable import AuthenticatorShared

class MockNotificationCenterService: NotificationCenterService {
var didEnterBackgroundSubject = PassthroughSubject<Void, Never>()
var willEnterForegroundSubject = PassthroughSubject<Void, Never>()

func didEnterBackgroundPublisher() -> AnyPublisher<Void, Never> {
didEnterBackgroundSubject.eraseToAnyPublisher()
}

func willEnterForegroundPublisher() -> AnyPublisher<Void, Never> {
willEnterForegroundSubject.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ extension ServiceContainer {
exportItemsService: ExportItemsService = MockExportItemsService(),
importItemsService: ImportItemsService = MockImportItemsService(),
migrationService: MigrationService = MockMigrationService(),
notificationCenterService: NotificationCenterService = MockNotificationCenterService(),
pasteboardService: PasteboardService = MockPasteboardService(),
stateService: StateService = MockStateService(),
timeProvider: TimeProvider = MockTimeProvider(.currentTime),
Expand All @@ -37,6 +38,7 @@ extension ServiceContainer {
exportItemsService: exportItemsService,
importItemsService: importItemsService,
migrationService: migrationService,
notificationCenterService: notificationCenterService,
pasteboardService: pasteboardService,
stateService: stateService,
timeProvider: timeProvider,
Expand Down
Loading
Loading