generated from bitwarden/template
-
Notifications
You must be signed in to change notification settings - Fork 27
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
PM-11974: Fix login with device notification for inactive account not switching to that account #1157
Open
matt-livefront
wants to merge
1
commit into
main
Choose a base branch
from
matt/PM-11974-login-with-device-notification
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+84
โ62
Open
PM-11974: Fix login with device notification for inactive account not switching to that account #1157
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,7 @@ protocol NotificationService { | |
|
||
/// The delegate to handle login request actions originating from notifications. | ||
/// | ||
@MainActor | ||
protocol NotificationServiceDelegate: AnyObject { | ||
/// Users are logged out, route to landing page. | ||
/// | ||
|
@@ -66,10 +67,9 @@ protocol NotificationServiceDelegate: AnyObject { | |
/// | ||
/// - Parameters: | ||
/// - account: The account associated with the login request. | ||
/// - loginRequest: The login request to show. | ||
/// - showAlert: Whether to show the alert or simply switch the account. | ||
/// | ||
func switchAccounts(to account: Account, for loginRequest: LoginRequest, showAlert: Bool) | ||
func switchAccountsForLoginRequest(to account: Account, showAlert: Bool) async | ||
} | ||
|
||
// MARK: - DefaultNotificationService | ||
|
@@ -279,14 +279,13 @@ class DefaultNotificationService: NotificationService { | |
await stateService.setLoginRequest(data) | ||
|
||
// Get the email of the account that the login request is coming from. | ||
let loginSourceAccount = try await stateService.getAccounts() | ||
.first(where: { $0.profile.userId == data.userId }) | ||
let loginSourceEmail = loginSourceAccount?.profile.email ?? "" | ||
let loginSourceAccount = try await stateService.getAccount(userId: data.userId) | ||
let loginSourceEmail = loginSourceAccount.profile.email | ||
|
||
// Assemble the data to add to the in-app banner notification. | ||
let loginRequestData = try? JSONEncoder().encode(LoginRequestPushNotification( | ||
timeoutInMinutes: Constants.loginRequestTimeoutMinutes, | ||
userEmail: loginSourceEmail | ||
userId: loginSourceAccount.profile.userId | ||
)) | ||
|
||
// Create an in-app banner notification to tell the user about the login request. | ||
|
@@ -308,14 +307,14 @@ class DefaultNotificationService: NotificationService { | |
let request = UNNotificationRequest(identifier: data.id, content: content, trigger: nil) | ||
try await UNUserNotificationCenter.current().add(request) | ||
|
||
// If the request is for the existing account, show the login request view automatically. | ||
guard let loginRequest = try await authService.getPendingLoginRequest(withId: data.id).first | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This API request was being made if the login request was for an active or inactive user. And it fails for an inactive user since it has the wrong access token. This instead needs to happen after switching accounts if the request is for an inactive user. |
||
else { return } | ||
if data.userId == userId { | ||
delegate?.showLoginRequest(loginRequest) | ||
} else if let loginSourceAccount { | ||
// If the request is for the existing account, show the login request view automatically. | ||
guard let loginRequest = try await authService.getPendingLoginRequest(withId: data.id).first | ||
else { return } | ||
await delegate?.showLoginRequest(loginRequest) | ||
} else { | ||
// Otherwise, show an alert asking the user if they want to switch accounts. | ||
delegate?.switchAccounts(to: loginSourceAccount, for: loginRequest, showAlert: true) | ||
await delegate?.switchAccountsForLoginRequest(to: loginSourceAccount, showAlert: true) | ||
} | ||
} | ||
|
||
|
@@ -361,20 +360,15 @@ class DefaultNotificationService: NotificationService { | |
private func handleNotificationTapped(_ loginRequestData: LoginRequestPushNotification) async { | ||
do { | ||
// Get the user id of the source of the login request. | ||
guard let loginSourceAccount = try await stateService.getAccounts() | ||
.first(where: { $0.profile.email == loginRequestData.userEmail }) | ||
else { return } | ||
let loginSourceAccount = try await stateService.getAccount(userId: loginRequestData.userId) | ||
|
||
// Get the active account for comparison. | ||
let activeAccount = try await stateService.getActiveAccount() | ||
|
||
// If the notification banner was tapped but it's for a different account, switch | ||
// to that account automatically. | ||
if activeAccount.profile.userId != loginSourceAccount.profile.userId, | ||
let loginRequestData = await stateService.getLoginRequest(), | ||
let loginRequest = try await authService.getPendingLoginRequest(withId: loginRequestData.id).first { | ||
try await stateService.setActiveAccount(userId: loginSourceAccount.profile.userId) | ||
delegate?.switchAccounts(to: loginSourceAccount, for: loginRequest, showAlert: false) | ||
if activeAccount.profile.userId != loginSourceAccount.profile.userId { | ||
await delegate?.switchAccountsForLoginRequest(to: loginSourceAccount, showAlert: false) | ||
} | ||
} catch { | ||
errorReporter.log(error: error) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -371,6 +371,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
|
||
/// `messageReceived(_:notificationDismissed:notificationTapped:)` tells | ||
/// the delegate to show the switch account alert if it's a login request for a non-active account. | ||
@MainActor | ||
func test_messageReceived_loginRequest_differentAccount() async throws { | ||
// Set up the mock data. | ||
stateService.setIsAuthenticated() | ||
|
@@ -379,7 +380,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
authService.getPendingLoginRequestResult = .success([.fixture()]) | ||
let loginRequestNotification = LoginRequestNotification(id: "requestId", userId: "differentUser") | ||
let notificationData = try JSONEncoder().encode(loginRequestNotification) | ||
let message: [AnyHashable: Any] = [ | ||
nonisolated(unsafe) let message: [AnyHashable: Any] = [ | ||
"data": [ | ||
"type": NotificationType.authRequest.rawValue, | ||
"payload": String(data: notificationData, encoding: .utf8) ?? "", | ||
|
@@ -392,12 +393,12 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
// Confirm the results. | ||
XCTAssertEqual(stateService.loginRequest, loginRequestNotification) | ||
XCTAssertEqual(delegate.switchAccountsAccount, .fixture(profile: .fixture(userId: "differentUser"))) | ||
XCTAssertEqual(delegate.switchAccountsLoginRequest, .fixture()) | ||
XCTAssertEqual(delegate.switchAccountsShowAlert, true) | ||
} | ||
|
||
/// `messageReceived(_:notificationDismissed:notificationTapped:)` tells | ||
/// the delegate to show the login request if it's a login request for the active account. | ||
@MainActor | ||
func test_messageReceived_loginRequest_sameAccount() async throws { | ||
// Set up the mock data. | ||
stateService.setIsAuthenticated() | ||
|
@@ -406,7 +407,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
authService.getPendingLoginRequestResult = .success([.fixture()]) | ||
let loginRequestNotification = LoginRequestNotification(id: "requestId", userId: "1") | ||
let notificationData = try JSONEncoder().encode(loginRequestNotification) | ||
let message: [AnyHashable: Any] = [ | ||
nonisolated(unsafe) let message: [AnyHashable: Any] = [ | ||
"data": [ | ||
"type": NotificationType.authRequest.rawValue, | ||
"payload": String(data: notificationData, encoding: .utf8) ?? "", | ||
|
@@ -423,14 +424,15 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
|
||
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles logout requests and will not route | ||
/// to the landing screen if the logged-out account was not the currently active account. | ||
@MainActor | ||
func test_messageReceived_logout_nonActiveUser() async throws { | ||
// Set up the mock data. | ||
stateService.setIsAuthenticated() | ||
let activeAccount: Account = .fixture() | ||
let nonActiveAccount: Account = .fixture(profile: .fixture(userId: "b245a33f")) | ||
stateService.accounts = [activeAccount, nonActiveAccount] | ||
|
||
let message: [AnyHashable: Any] = [ | ||
nonisolated(unsafe) let message: [AnyHashable: Any] = [ | ||
"data": [ | ||
"type": NotificationType.logOut.rawValue, | ||
"payload": "{\"UserId\":\"\(nonActiveAccount.profile.userId)\"}", | ||
|
@@ -445,14 +447,15 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
|
||
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles logout requests and will route | ||
/// to the landing screen if the logged-out account was the currently active account. | ||
@MainActor | ||
func test_messageReceived_logout_activeUser() async throws { | ||
// Set up the mock data. | ||
stateService.setIsAuthenticated() | ||
let activeAccount: Account = .fixture() | ||
let nonActiveAccount: Account = .fixture(profile: .fixture(userId: "b245a33f")) | ||
stateService.accounts = [activeAccount, nonActiveAccount] | ||
|
||
let message: [AnyHashable: Any] = [ | ||
nonisolated(unsafe) let message: [AnyHashable: Any] = [ | ||
"data": [ | ||
"type": NotificationType.logOut.rawValue, | ||
"payload": "{\"UserId\":\"\(activeAccount.profile.userId)\"}", | ||
|
@@ -466,15 +469,16 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
} | ||
|
||
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles notifications being dismissed. | ||
@MainActor | ||
func test_messageReceived_notificationDismissed() async throws { | ||
// Set up the mock data. | ||
stateService.loginRequest = LoginRequestNotification(id: "1", userId: "2") | ||
let loginRequest = LoginRequestPushNotification( | ||
timeoutInMinutes: 15, | ||
userEmail: "[email protected]" | ||
userId: "2" | ||
) | ||
let testData = try JSONEncoder().encode(loginRequest) | ||
let message: [AnyHashable: Any] = [ | ||
nonisolated(unsafe) let message: [AnyHashable: Any] = [ | ||
"notificationData": String(data: testData, encoding: .utf8) ?? "", | ||
] | ||
|
||
|
@@ -486,6 +490,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
} | ||
|
||
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles notifications being tapped. | ||
@MainActor | ||
func test_messageReceived_notificationTapped() async throws { | ||
// Set up the mock data. | ||
stateService.accounts = [.fixture()] | ||
|
@@ -494,10 +499,10 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
authService.getPendingLoginRequestResult = .success([.fixture(id: "requestId")]) | ||
let loginRequest = LoginRequestPushNotification( | ||
timeoutInMinutes: 15, | ||
userEmail: Account.fixture().profile.email | ||
userId: Account.fixture().profile.userId | ||
) | ||
let testData = try JSONEncoder().encode(loginRequest) | ||
let message: [AnyHashable: Any] = [ | ||
nonisolated(unsafe) let message: [AnyHashable: Any] = [ | ||
"notificationData": String(data: testData, encoding: .utf8) ?? "", | ||
] | ||
|
||
|
@@ -506,31 +511,27 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty | |
|
||
// Confirm the results. | ||
XCTAssertEqual(delegate.switchAccountsAccount, .fixture()) | ||
XCTAssertEqual(delegate.switchAccountsLoginRequest, .fixture(id: "requestId")) | ||
XCTAssertEqual(delegate.switchAccountsShowAlert, false) | ||
} | ||
|
||
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles errors. | ||
@MainActor | ||
func test_messageReceived_notificationTapped_error() async throws { | ||
// Set up the mock data. | ||
stateService.accounts = [.fixture()] | ||
stateService.activeAccount = .fixtureAccountLogin() | ||
stateService.loginRequest = LoginRequestNotification(id: "requestId", userId: "1") | ||
authService.getPendingLoginRequestResult = .failure(BitwardenTestError.example) | ||
let loginRequest = LoginRequestPushNotification( | ||
timeoutInMinutes: 15, | ||
userEmail: Account.fixture().profile.email | ||
userId: Account.fixture().profile.userId | ||
) | ||
let testData = try JSONEncoder().encode(loginRequest) | ||
let message: [AnyHashable: Any] = [ | ||
nonisolated(unsafe) let message: [AnyHashable: Any] = [ | ||
"notificationData": String(data: testData, encoding: .utf8) ?? "", | ||
] | ||
|
||
// Test. | ||
await subject.messageReceived(message, notificationDismissed: nil, notificationTapped: true) | ||
|
||
// Confirm the results. | ||
XCTAssertEqual(errorReporter.errors.last as? BitwardenTestError, .example) | ||
XCTAssertEqual(errorReporter.errors.last as? StateServiceError, .noAccounts) | ||
} | ||
} | ||
|
||
|
@@ -542,7 +543,6 @@ class MockNotificationServiceDelegate: NotificationServiceDelegate { | |
var showLoginRequestRequest: LoginRequest? | ||
|
||
var switchAccountsAccount: Account? | ||
var switchAccountsLoginRequest: LoginRequest? | ||
var switchAccountsShowAlert: Bool? | ||
|
||
func routeToLanding() async { | ||
|
@@ -553,9 +553,8 @@ class MockNotificationServiceDelegate: NotificationServiceDelegate { | |
showLoginRequestRequest = loginRequest | ||
} | ||
|
||
func switchAccounts(to account: Account, for loginRequest: LoginRequest, showAlert: Bool) { | ||
func switchAccountsForLoginRequest(to account: Account, showAlert: Bool) { | ||
switchAccountsAccount = account | ||
switchAccountsLoginRequest = loginRequest | ||
switchAccountsShowAlert = showAlert | ||
} | ||
} // swiftlint:disable:this file_length |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This previously stored the email of the account that the request was for. Which would be problematic if there were multiple accounts with the same email. So I switched to the use the user ID.