-
Notifications
You must be signed in to change notification settings - Fork 430
@W-18766706 Fix bug of mixed global and local state, just staying with Global state #3878
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
Conversation
@@ -473,4 +475,14 @@ private extension PushNotificationManager { | |||
SFSDKCoreLogger.i(Self.self, message: "App entering foreground, re-registering push") | |||
_ = registerSalesforceNotifications(completionBlock: nil, failBlock: nil) | |||
} | |||
|
|||
func refreshDependencies() { |
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.
Are these values being stored to make it more testable vs calling something like RestClient.restClient(for: user)
directly in the registration method?
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.
yeah, it was because I was trying to do some dependency injection for unit tests.
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.
I'm not sure if we can always assume the current user if we expose a user parameter in public func registerForSalesforceNotifications(user: UserAccount, completionBlock:@escaping (Result<Bool, PushNotificationManagerError>)->())
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.
Ah, I think I miss read this last comment. Yeah, I think I see what you are saying here. Is it that we have a local state and we are still using the gloabl state instead of the global state in the method?
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.
I cleaned up the parameter interface and global state dependence a bit. Since we kind of depend on the global state for determining our login flow for notification and things like that, I just moved back to using global state as it's the original pattern. Let me know if you think this looks better?
I updated the unit tests as well to mock the global state as well to be more accurate to our original design for Push Notification Manager.
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.
I think there's a bug even in the old code where registerSalesforceNotificationsWithCompletionBlock:(SFUserAccount *)user
would use the RestClient for the current user instead of the RestClient for the user in the parameter. It looks like the global state here would have the same issue if the current user and the user in the parameter were different. Is there a way to keep the testability improvements you made and fix that?
cc @wmathurin if you have a different take
If we kept the global state approach I think user switching would also need to be observed
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.
@bbirman You are right, the code has the issue before this PR.
The issue existed also in the original Objective-C code.
We were sending the request using the default rest client [SFRestAPI sharedInstance]
(which is bound to the current user [SFRestAPI sharedInstanceWithUser:[SFUserAccountManager sharedInstance].currentUser]
).
Instead we should have used the rest client for the user being passed through.
Maybe we shouldn't have a restClient
member in the class and instead get the right one in each method that needs one using the user passed in?
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.
OK, in this update I introduced a protocol-based approach for remote notification registration to keep testability — making it easier to inject mocks in unit tests.
What’s the Fix?
We now fetch the correct RestClient for the user being passed in (instead of relying on the global currentUserAccount). To do this, I updated the protocol to:
func clientForRemoteNotification(for user: UserAccount?) -> RestClient?
This fixes the bug from the original implementation and keeps the code testable and clean.
Clang Static Analysis Issues
Generated by 🚫 Danger |
@@ -670,7 +670,7 @@ - (void)logoutUser:(SFUserAccount *)user reason:(SFLogoutReason)reason { | |||
|
|||
// Before starting actual logout (which will tear down SFRestAPI), first unregister from push notifications if needed | |||
__weak typeof(self) weakSelf = self; | |||
[[SFPushNotificationManager sharedInstance] unregisterSalesforceNotificationsWithCompletionBlock:user completionBlock:^void() { |
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.
Objc and Swift overlook here. Looks like the original method was this as well.
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.
Can this go back to passing the user parameter?
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.
yes
Codecov ReportAttention: Patch coverage is
❌ Your patch status has failed because the patch coverage (75.00%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## dev #3878 +/- ##
==========================================
- Coverage 62.78% 62.40% -0.39%
==========================================
Files 242 242
Lines 22198 22208 +10
==========================================
- Hits 13938 13859 -79
- Misses 8260 8349 +89
🚀 New features to boost your workflow:
|
/// - Returns: `true` if unregistration started successfully, otherwise `false`. | ||
@discardableResult | ||
@objc(unregisterSalesforceNotificationsWithCompletionBlock:completionBlock:) | ||
public func unregisterSalesforceNotifications(for user: UserAccount, |
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.
Making this private looks like it would be a breaking change from 13.0.x
Line 100 in 096b4e5
- (BOOL)unregisterSalesforceNotificationsWithCompletionBlock:(SFUserAccount*)user completionBlock:(nullable void (^)(void))completionBlock NS_REFINED_FOR_SWIFT; |
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.
Same comment on the latest, I only see a private method for unregistering for a user?
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.
Ah, you’re right — I think I was looking at the Objective-C header and got tripped up by the naming. It does look like I goofed that… seeing completionBlock twice in there threw me off. 🫨
I'm running into the same error on login with the latest: "Cannot register for notifications with Salesforce: no restClient" |
@bbirman I ran it locally again after making the additional changes and was able to see a notification after login. Did a couple of times as well. |
@@ -224,6 +347,8 @@ public class PushNotificationManager: NSObject { | |||
return false | |||
} | |||
|
|||
let restClient = notificationRegister.clientForRemoteNotification(for: user) |
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.
@wmathurin @bbirman I’ve updated the implementation to use a protocol for interfacing with the rest client. I removed the restClient property from the class and now inject a dependency that conforms to the protocol. This allows us to provide either a mock rest client in tests or fetch the appropriate client from the UserAccountManager.shared (as was done previously), but now in a cleaner and testable way.
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.
nice!
_ = registerSalesforceNotifications(completionBlock: nil, failBlock: nil) | ||
} | ||
|
||
private func registerSalesforceNotifications(for user: UserAccount, |
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.
Like unregistering, registering notifications for a given user was a public API in 13.0
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.
Yeah, Kept thinking those were typo errors by me. with completionBlock
in teh Objc header naming.
0b80b16
to
5720434
Compare
restClient: RestClient.shared, | ||
currentUser: UserAccountManager.shared.currentUserAccount, | ||
preferences: SFPreferences.sharedPreferences(for: .user, user: UserAccountManager.shared.currentUserAccount)) | ||
preferences: SFPreferences.currentUserLevel()) |
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.
I think the user preferences could get out of sync on user logout / login / switch if it's set at init, is it possible to not store it at the class level like you did with the user account and rest client?
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.
Yes. I'll do a similar pattern for preferences
@@ -224,6 +202,8 @@ public class PushNotificationManager: NSObject { | |||
return false | |||
} | |||
|
|||
let restClient = notificationRegister.client(for: user) |
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.
sorry last one, could you also add a guard or something for the rest client and log an error if it's nil for some reason? I ran into a similar case recently and just want to make sure it can't fail silently. same for the similar block in the unregister method
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.
Yes. I'll update that this morning.
We need to move back to global state dependence on the original design.