Skip to content

Commit 34cbcf6

Browse files
[PM-16687] Sync before determining if we need 2FA notice (#1251)
1 parent 02dd8c6 commit 34cbcf6

11 files changed

+104
-48
lines changed

BitwardenShared/Core/Platform/Services/Services.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ typealias Services = HasAPIService
3737
& HasSendRepository
3838
& HasSettingsRepository
3939
& HasStateService
40+
& HasSyncService
4041
& HasSystemDevice
4142
& HasTOTPExpirationManagerFactory
4243
& HasTOTPService
@@ -300,6 +301,13 @@ protocol HasStateService {
300301
var stateService: StateService { get }
301302
}
302303

304+
/// Protocol for an object that has a `SyncService`.
305+
///
306+
protocol HasSyncService {
307+
/// The service used by the application to sync account data.
308+
var syncService: SyncService { get }
309+
}
310+
303311
/// Protocol for an object that provides a `SystemDevice`.
304312
///
305313
protocol HasSystemDevice {

BitwardenShared/UI/Auth/TwoFactorNotice/SetUpTwoFactor/SetUpTwoFactorAction.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,4 @@
55
enum SetUpTwoFactorAction: Equatable, Sendable {
66
/// The url has been opened so clear the value in the state.
77
case clearURL
8-
9-
/// The user tapped the button to turn on two-factor authentication.
10-
case turnOnTwoFactorTapped
11-
12-
/// The user tapped the button to change email.
13-
case changeAccountEmailTapped
148
}

BitwardenShared/UI/Auth/TwoFactorNotice/SetUpTwoFactor/SetUpTwoFactorEffect.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
/// Effects that can be processed by a `SetUpTwoFactorProcessor`.
44
///
55
enum SetUpTwoFactorEffect: Equatable, Sendable {
6+
/// The user tapped the button to change email.
7+
case changeAccountEmailTapped
8+
69
/// The user tapped the "remind me later" button.
710
case remindMeLaterTapped
11+
12+
/// The user tapped the button to turn on two-factor authentication.
13+
case turnOnTwoFactorTapped
814
}

BitwardenShared/UI/Auth/TwoFactorNotice/SetUpTwoFactor/SetUpTwoFactorProcessor.swift

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,39 @@ class SetUpTwoFactorProcessor: StateProcessor<SetUpTwoFactorState, SetUpTwoFacto
4545

4646
override func perform(_ effect: SetUpTwoFactorEffect) async {
4747
switch effect {
48+
case .changeAccountEmailTapped:
49+
await handleChangeAccountEmail()
4850
case .remindMeLaterTapped:
4951
await handleDismiss()
52+
case .turnOnTwoFactorTapped:
53+
await handleTurnOnTwoFactor()
5054
}
5155
}
5256

5357
override func receive(_ action: SetUpTwoFactorAction) {
5458
switch action {
55-
case .changeAccountEmailTapped:
56-
coordinator.showAlert(.changeEmailAlert {
57-
self.state.url = self.services.environmentService.changeEmailURL
58-
})
5959
case .clearURL:
6060
state.url = nil
61-
case .turnOnTwoFactorTapped:
62-
coordinator.showAlert(.turnOnTwoFactorLoginAlert {
63-
self.state.url = self.services.environmentService.setUpTwoFactorURL
64-
})
6561
}
6662
}
6763

6864
// MARK: Private Methods
6965

66+
/// Handles when the user taps the Change Account Email button. This will
67+
/// give the user the ability to go to the website to change their email, and
68+
/// will in the process nil the last sync time so the next time the app launches
69+
/// it will be able to sync immediately.
70+
private func handleChangeAccountEmail() async {
71+
do {
72+
try await services.stateService.setLastSyncTime(nil)
73+
coordinator.showAlert(.changeEmailAlert {
74+
self.state.url = self.services.environmentService.changeEmailURL
75+
})
76+
} catch {
77+
services.errorReporter.log(error: error)
78+
}
79+
}
80+
7081
/// Saves the current time to disk when the user dismisses the notice.
7182
private func handleDismiss() async {
7283
do {
@@ -77,4 +88,19 @@ class SetUpTwoFactorProcessor: StateProcessor<SetUpTwoFactorState, SetUpTwoFacto
7788
services.errorReporter.log(error: error)
7889
}
7990
}
91+
92+
/// Handles when the user taps the Turn On Two-Step button. This will
93+
/// give the user the ability to go to the website to change their email, and
94+
/// will in the process nil the last sync time so the next time the app launches
95+
/// it will be able to sync immediately.
96+
private func handleTurnOnTwoFactor() async {
97+
do {
98+
try await services.stateService.setLastSyncTime(nil)
99+
coordinator.showAlert(.turnOnTwoFactorLoginAlert {
100+
self.state.url = self.services.environmentService.setUpTwoFactorURL
101+
})
102+
} catch {
103+
services.errorReporter.log(error: error)
104+
}
105+
}
80106
}

BitwardenShared/UI/Auth/TwoFactorNotice/SetUpTwoFactor/SetUpTwoFactorProcessorTests.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ class SetUpTwoFactorProcessorTests: BitwardenTestCase {
3737
services: services,
3838
state: SetUpTwoFactorState(allowDelay: true, emailAddress: "person@example.com")
3939
)
40+
41+
let account = Account.fixture()
42+
stateService.activeAccount = account
43+
stateService.lastSyncTimeByUserId["1"] = timeProvider.presentTime
4044
}
4145

4246
override func tearDown() {
@@ -56,8 +60,6 @@ class SetUpTwoFactorProcessorTests: BitwardenTestCase {
5660
/// then dismisses
5761
@MainActor
5862
func test_perform_remindMeLaterTapped() async {
59-
let account = Account.fixture()
60-
stateService.activeAccount = account
6163
await subject.perform(.remindMeLaterTapped)
6264
XCTAssertEqual(
6365
stateService.twoFactorNoticeDisplayState["1"],
@@ -69,8 +71,6 @@ class SetUpTwoFactorProcessorTests: BitwardenTestCase {
6971
/// `.perform(_:)` with `.remindMeLaterTapped` handles errors
7072
@MainActor
7173
func test_perform_remindMeLaterTapped_error() async {
72-
let account = Account.fixture()
73-
stateService.activeAccount = account
7474
stateService.setTwoFactorNoticeDisplayStateError = BitwardenTestError.example
7575
await subject.perform(.remindMeLaterTapped)
7676
XCTAssertEqual(
@@ -85,10 +85,12 @@ class SetUpTwoFactorProcessorTests: BitwardenTestCase {
8585
func test_receive_changeAccountEmailTapped() async throws {
8686
let url = URL("https://www.example.com")!
8787
environmentService.changeEmailURL = url
88-
subject.receive(.changeAccountEmailTapped)
88+
await subject.perform(.changeAccountEmailTapped)
89+
waitFor(!coordinator.alertShown.isEmpty)
8990
let alert = try XCTUnwrap(coordinator.alertShown.last)
9091
try await alert.tapAction(title: Localizations.continue)
9192
XCTAssertEqual(subject.state.url, url)
93+
XCTAssertNil(stateService.lastSyncTimeByUserId["1"])
9294
}
9395

9496
/// `receive(_:)` with `.clearURL` clears the URL in the state.
@@ -105,9 +107,11 @@ class SetUpTwoFactorProcessorTests: BitwardenTestCase {
105107
func test_receive_turnOnTwoFactorTapped() async throws {
106108
let url = URL("https://www.example.com")!
107109
environmentService.setUpTwoFactorURL = url
108-
subject.receive(.turnOnTwoFactorTapped)
110+
await subject.perform(.turnOnTwoFactorTapped)
111+
waitFor(!coordinator.alertShown.isEmpty)
109112
let alert = try XCTUnwrap(coordinator.alertShown.last)
110113
try await alert.tapAction(title: Localizations.continue)
111114
XCTAssertEqual(subject.state.url, url)
115+
XCTAssertNil(stateService.lastSyncTimeByUserId["1"])
112116
}
113117
}

BitwardenShared/UI/Auth/TwoFactorNotice/SetUpTwoFactor/SetUpTwoFactorView.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ struct SetUpTwoFactorView: View {
2727
.padding(.top, 16)
2828

2929
VStack(spacing: 12) {
30-
Button {
31-
store.send(.turnOnTwoFactorTapped)
30+
AsyncButton {
31+
await store.perform(.turnOnTwoFactorTapped)
3232
} label: {
3333
Label {
3434
Text(Localizations.turnOnTwoStepLogin)
@@ -38,8 +38,8 @@ struct SetUpTwoFactorView: View {
3838
}
3939
.buttonStyle(.primary())
4040

41-
Button {
42-
store.send(.changeAccountEmailTapped)
41+
AsyncButton {
42+
await store.perform(.changeAccountEmailTapped)
4343
} label: {
4444
Label {
4545
Text(Localizations.changeAccountEmail)

BitwardenShared/UI/Auth/TwoFactorNotice/SetUpTwoFactor/SetUpTwoFactorViewTests.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ class SetUpTwoFactorViewTests: BitwardenTestCase {
3333

3434
/// Tapping the change email button sends `.changeAccountEmailTapped`
3535
@MainActor
36-
func test_changeEmail_tap() throws {
37-
let button = try subject.inspect().find(button: Localizations.changeAccountEmail)
38-
try button.tap()
36+
func test_changeEmail_tap() async throws {
37+
let button = try subject.inspect().find(asyncButton: Localizations.changeAccountEmail)
38+
try await button.tap()
3939

40-
XCTAssertEqual(processor.dispatchedActions.last, .changeAccountEmailTapped)
40+
XCTAssertEqual(processor.effects.last, .changeAccountEmailTapped)
4141
}
4242

4343
/// Tapping the remind me later button sends `.remindMeLater`
@@ -51,11 +51,11 @@ class SetUpTwoFactorViewTests: BitwardenTestCase {
5151

5252
/// Tapping the turn on two-step login button sends `.turnOnTwoFactorTapped`
5353
@MainActor
54-
func test_turnOnTwoStep_tap() throws {
55-
let button = try subject.inspect().find(button: Localizations.turnOnTwoStepLogin)
56-
try button.tap()
54+
func test_turnOnTwoStep_tap() async throws {
55+
let button = try subject.inspect().find(asyncButton: Localizations.turnOnTwoStepLogin)
56+
try await button.tap()
5757

58-
XCTAssertEqual(processor.dispatchedActions.last, .turnOnTwoFactorTapped)
58+
XCTAssertEqual(processor.effects.last, .turnOnTwoFactorTapped)
5959
}
6060

6161
// MARK: Previews
@@ -70,7 +70,7 @@ class SetUpTwoFactorViewTests: BitwardenTestCase {
7070
)
7171
}
7272

73-
/// The set up two factor view renders correctly when delay is allowed
73+
/// The set up two factor view renders correctly when delay is not allowed
7474
@MainActor
7575
func test_snapshot_setUpTwoFactorView_allowDelay_false() {
7676
processor.state.allowDelay = false

BitwardenShared/UI/Auth/TwoFactorNotice/TwoFactorNoticeHelper.swift

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class DefaultTwoFactorNoticeHelper: TwoFactorNoticeHelper {
4242
& HasErrorReporter
4343
& HasPolicyService
4444
& HasStateService
45+
& HasSyncService
4546
& HasTimeProvider
4647

4748
// MARK: Private Properties
@@ -74,20 +75,23 @@ class DefaultTwoFactorNoticeHelper: TwoFactorNoticeHelper {
7475
/// and displays the notice if necessary
7576
///
7677
func maybeShowTwoFactorNotice() async {
77-
let temporary = await services.configService.getFeatureFlag(
78-
.newDeviceVerificationTemporaryDismiss,
79-
defaultValue: false
80-
)
81-
let permanent = await services.configService.getFeatureFlag(
82-
.newDeviceVerificationPermanentDismiss,
83-
defaultValue: false
84-
)
85-
let ignoreEnvironmentCheck = await services.configService.getFeatureFlag(
86-
.ignore2FANoticeEnvironmentCheck,
87-
defaultValue: false
88-
)
89-
guard temporary || permanent else { return }
9078
do {
79+
try await services.syncService.fetchSync(forceSync: false)
80+
81+
let temporary = await services.configService.getFeatureFlag(
82+
.newDeviceVerificationTemporaryDismiss,
83+
defaultValue: false
84+
)
85+
let permanent = await services.configService.getFeatureFlag(
86+
.newDeviceVerificationPermanentDismiss,
87+
defaultValue: false
88+
)
89+
let ignoreEnvironmentCheck = await services.configService.getFeatureFlag(
90+
.ignore2FANoticeEnvironmentCheck,
91+
defaultValue: false
92+
)
93+
guard temporary || permanent else { return }
94+
9195
guard services.environmentService.region != .selfHosted || ignoreEnvironmentCheck else {
9296
return
9397
}

BitwardenShared/UI/Auth/TwoFactorNotice/TwoFactorNoticeHelperTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
1414
var policyService: MockPolicyService!
1515
var stateService: MockStateService!
1616
var subject: DefaultTwoFactorNoticeHelper!
17+
var syncService: MockSyncService!
1718
var timeProvider: MockTimeProvider!
1819

1920
// MARK: Setup & Teardown
@@ -27,6 +28,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
2728
errorReporter = MockErrorReporter()
2829
policyService = MockPolicyService()
2930
stateService = MockStateService()
31+
syncService = MockSyncService()
3032
timeProvider = MockTimeProvider(.mockTime(Date(year: 2024, month: 6, day: 15, hour: 12, minute: 0)))
3133

3234
let services = ServiceContainer.withMocks(
@@ -35,6 +37,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
3537
errorReporter: errorReporter,
3638
policyService: policyService,
3739
stateService: stateService,
40+
syncService: syncService,
3841
timeProvider: timeProvider
3942
)
4043

@@ -70,6 +73,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
7073
policyService = nil
7174
stateService = nil
7275
subject = nil
76+
syncService = nil
7377
timeProvider = nil
7478
}
7579

@@ -86,6 +90,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
8690
func test_maybeShow() async {
8791
await subject.maybeShowTwoFactorNotice()
8892

93+
XCTAssertTrue(syncService.didFetchSync)
8994
XCTAssertEqual(coordinator.routes, [.twoFactorNotice(allowDelay: false, emailAddress: "user@bitwarden.com")])
9095
}
9196

@@ -98,6 +103,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
98103

99104
await subject.maybeShowTwoFactorNotice()
100105

106+
XCTAssertTrue(syncService.didFetchSync)
101107
XCTAssertEqual(coordinator.routes, [.twoFactorNotice(allowDelay: false, emailAddress: "user@bitwarden.com")])
102108
}
103109

@@ -109,6 +115,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
109115

110116
await subject.maybeShowTwoFactorNotice()
111117

118+
XCTAssertTrue(syncService.didFetchSync)
112119
XCTAssertEqual(coordinator.routes, [])
113120
}
114121

@@ -120,6 +127,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
120127

121128
await subject.maybeShowTwoFactorNotice()
122129

130+
XCTAssertTrue(syncService.didFetchSync)
123131
XCTAssertEqual(coordinator.routes, [])
124132
}
125133

@@ -131,6 +139,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
131139

132140
await subject.maybeShowTwoFactorNotice()
133141

142+
XCTAssertTrue(syncService.didFetchSync)
134143
XCTAssertEqual(coordinator.routes, [])
135144
}
136145

@@ -143,6 +152,7 @@ class TwoFactorNoticeHelperTests: BitwardenTestCase {
143152

144153
await subject.maybeShowTwoFactorNotice()
145154

155+
XCTAssertTrue(syncService.didFetchSync)
146156
XCTAssertEqual(coordinator.routes, [.twoFactorNotice(allowDelay: false, emailAddress: "user@bitwarden.com")])
147157
}
148158

BitwardenShared/UI/Vault/Vault/VaultCoordinator.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public protocol VaultCoordinatorDelegate: AnyObject {
5454

5555
/// A coordinator that manages navigation in the vault tab.
5656
///
57-
final class VaultCoordinator: Coordinator, HasStackNavigator {
57+
final class VaultCoordinator: Coordinator, HasStackNavigator { // swiftlint:disable: this type_body_length
5858
// MARK: Types
5959

6060
typealias Module = GeneratorModule
@@ -79,6 +79,7 @@ final class VaultCoordinator: Coordinator, HasStackNavigator {
7979
& HasReviewPromptService
8080
& HasSettingsRepository
8181
& HasStateService
82+
& HasSyncService
8283
& HasTOTPExpirationManagerFactory
8384
& HasTextAutofillHelperFactory
8485
& HasTimeProvider

BitwardenShared/UI/Vault/Vault/VaultCoordinatorTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,10 @@ class VaultCoordinatorTests: BitwardenTestCase {
313313
let action = try XCTUnwrap(stackNavigator.actions.last)
314314
XCTAssertEqual(action.type, .presented)
315315
XCTAssertTrue(module.twoFactorNoticeCoordinator.isStarted)
316-
XCTAssertEqual(module.twoFactorNoticeCoordinator.routes.last, .emailAccess(allowDelay: true, emailAddress: "person@example.com"))
316+
XCTAssertEqual(
317+
module.twoFactorNoticeCoordinator.routes.last,
318+
.emailAccess(allowDelay: true, emailAddress: "person@example.com")
319+
)
317320
}
318321

319322
/// `.navigate(to:)` with `.vaultItemSelection` presents the vault item selection screen.

0 commit comments

Comments
 (0)