-
Notifications
You must be signed in to change notification settings - Fork 16
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
feat: [FC-0047] FCM #461
feat: [FC-0047] FCM #461
Conversation
Thanks for the pull request, @volodymyr-chekyrta! Please note that it may take us up to several weeks or months to complete a review and merge your PR. Feel free to add as much of the following information to the ticket as you can:
All technical communication about the code itself will be done via the GitHub pull request interface. As a reminder, our process documentation is here. Please let us know once your PR is ready for our review and all tests are green. |
OpenEdX/AppDelegate.swift
Outdated
let pushManager = Container.shared.resolve(PushNotificationsManager.self) | ||
Messaging.messaging().delegate = pushManager | ||
UNUserNotificationCenter.current().delegate = pushManager | ||
UIApplication.shared.registerForRemoteNotifications() |
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.
@volodymyr-chekyrta This way didRegisterForRemoteNotificationsWithDeviceToken
delegate method will fired only if cloudMessagingEnabled==true
. If FCM is disabled then all other providers will unable to register device token.
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.
Makes sense 👍
I've updated it for any provider ✅
let pushManager = Container.shared.resolve(PushNotificationsManager.self)
if config.firebase.enabled {
FirebaseApp.configure()
if config.firebase.cloudMessagingEnabled {
Messaging.messaging().delegate = pushManager
UNUserNotificationCenter.current().delegate = pushManager
}
}
if pushManager?.hasProviders == true {
UIApplication.shared.registerForRemoteNotifications()
}
4dc2a95
to
768726b
Compare
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.
LGTM
private var providers: [PushNotificationsProvider] = [] | ||
private var listeners: [PushNotificationsListener] = [] | ||
|
||
public var hasProviders: Bool { | ||
return providers.count > 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.
'return' isn't necessary in this case.
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. Done ✅
768726b
to
ca1d729
Compare
@saeedbashir, Kindly review the changes made in this pull request. |
Will review it tomorrow. |
@volodymyr-chekyrta Do I need to test this PR against the sandbox because the API |
analytics.userLogin(method: authMethod) | ||
isShowProgress = false | ||
router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) | ||
NotificationCenter.default.post(name: .userAuthorized, object: nil) |
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 piece of code (register success) is redendant, how about moving it to a function, thoughts?
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 don't understand why it is redundant.
It looks reasonable for me to be placed here.
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.
The following code is same in both the success methods loginOrRegister
and registerUser
. Anyhow we can live with it because it's not changed under the scope of this PR.
let user = try await interactor.login(externalToken: response.token, backend: backend)
analytics.identify(id: "\(user.id)", username: user.username, email: user.email)
analytics.userLogin(method: authMethod)
isShowProgress = false
router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen)
NotificationCenter.default.post(name: .userAuthorized, object: nil)
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.
Sure, we can address this later in some refactoring
NotificationCenter.default.post( | ||
name: .userLoggedOut, | ||
object: nil, | ||
userInfo: [Notification.UserInfoKey.isForced: false] |
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 guess this is the forced logout case, if so then the value of isForced
should be true.
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.
Right. Fixed ✅
@@ -176,7 +172,25 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { | |||
} | |||
} | |||
|
|||
public func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse { | |||
private func renameThreadUser(data: Data) async throws -> DataLayer.ThreadList { |
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 we doing this to format data for parsing in mobile?
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, it enables easier parsing of user avatar.
OpenEdX/AppDelegate.swift
Outdated
@@ -120,21 +141,34 @@ class AppDelegate: UIResponder, UIApplicationDelegate { | |||
) | |||
} | |||
|
|||
@objc private func forceLogoutUser() { | |||
@objc private func didUserAuthorize() { | |||
guard Container.shared.resolve(ConfigProtocol.self)?.firebase.cloudMessagingEnabled == true else { return } |
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.
Regardless of the provider, if any provider available the synchronizeToken
should be called and then the provider will decide what to do with the call.
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.
Makes sense 👍
✅
OpenEdX/AppDelegate.swift
Outdated
if Container.shared.resolve(ConfigProtocol.self)?.firebase.cloudMessagingEnabled == true { | ||
UNUserNotificationCenter.current().removeAllDeliveredNotifications() |
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.
Regardless of the provider, if any provider available the refreshToken()
should be called and then the provider will decide what to do with the call.
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.
Makes sense 👍
✅
func refreshToken() { | ||
Messaging.messaging().deleteToken { error 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.
Any reason to delete the old token on logout, and create a new one? Will the user be still able to receive pushes while not login?
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.
User should not receive push notifications after logging out.
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 about the requirements but in this case user may miss important notifications. Like if somehow user is logout, and an assignment due notification is sent from the server, the user won't be receiving the notification. The other case is, moderator replied to the user query/response in the discussion threads, the user won't be receiving the response.
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.
By the current flow, if user is logging out, they are no longer interested in receiving notifications.
Users can still rely on email or calendar notifications.
We can change this flow in the future if we add some vital push notifications.
let userInfo = response.notification.request.content.userInfo | ||
|
||
// With swizzling disabled you must let Messaging know about the message, for Analytics | ||
Messaging.messaging().appDidReceiveMessage(userInfo) |
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 guess the right place for this is FCMListener
.
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.
It requires extra abstraction for PushNotificationsProvider; it can create overhead.
For me, it seems ok to leave just 1 line here.
wdyt?
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.
It won't required any abstraction as the abstraction is already there. The FCMListener: PushNotificationsListener
is implementing the PushNotificationsListener
protocol, you just need to implement optional method didReceiveRemoteNotification
and thats it.
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.
@saeedbashir, but if we override didReceiveRemoteNotification
in FCMListener
, the extension with default implementation (navigation) will stop working 🤷♂️.
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.
We need one more function in PushNotificationsListener
for this or refactor didReceiveRemoteNotification
and not use it for navigation.
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.
Strange, I guess it got missed while writing the abstraction layer. Ideally, every PushNotificationsListener
should implement this piece of code. So first, the listener will check whether the push is for that listener (Braze, FCM etc) or not, and then do the required work before starting the navigation.
The other reason for giving each listener its own implementation is that maybe data will be received in a different format (hierarchy), and a parsing may be required to generate a PushLink from the data.
Thoughts?
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.
Sure, I'll update it now, I think it makes sense
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.
@saeedbashir I've updated implementation of the PushNotificationsManager
, please check.
willPresent notification: UNNotification | ||
) async -> UNNotificationPresentationOptions { | ||
// With swizzling disabled you must let Messaging know about the message, for Analytics | ||
Messaging.messaging().appDidReceiveMessage(notification.request.content.userInfo) |
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 guess the right place for this is FCMListner.
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.
It requires extra abstraction for PushNotificationsProvider; it can create overhead.
For me, it seems ok to leave just 1 line here.
wdyt?
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.
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.
@@ -380,6 +380,10 @@ public class Router: AuthorizationRouter, | |||
lastVisitedBlockID: lastVisitedBlockID | |||
) | |||
navigationController.pushViewController(controller, animated: true) | |||
|
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { | |||
Container.shared.resolve(PushNotificationsManager.self)?.performRegistration() |
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.
The push notification registration is happening from the course home screen, is that intended behavior?
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, it's a UX improvement.
Users automatically reject a push notification request if it appears when the app is launched.
7480208
to
06f8387
Compare
06f8387
to
61ab13d
Compare
You cannot test this endpoint; it has not been merged yet. |
Hi @saeedbashir, thank you for the prompt feedback! |
private lazy var deepLinkManager: DeepLinkManager? = { | ||
Container.shared.resolve(DeepLinkManager.self) | ||
}() |
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.
How about passing this as a parameter, thoughts?
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.
Sure, why not
Done ✅
|
||
func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { | ||
// With swizzling disabled you must let Messaging know about the message, for Analytics | ||
Messaging.messaging().appDidReceiveMessage(userInfo) |
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 guess it should move below the guard statement as the notification can be from the braze.
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.
Yeap ✅
f0866f5
to
c77f727
Compare
@saeedbashir, PR is ready for the next pass 🙌 |
@@ -6,11 +6,26 @@ | |||
// | |||
|
|||
import Foundation | |||
import Swinject |
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.
Please remove this unused import.
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.
👍
@@ -6,8 +6,30 @@ | |||
// | |||
|
|||
import Foundation | |||
import Swinject |
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.
Please remove this unused import and after that we are good to go.
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.
👍
@@ -9,56 +9,58 @@ import Foundation | |||
import Core | |||
import UIKit | |||
import Swinject |
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 guess we are safe to remove this import
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.
👍
private var storage = Container.shared.resolve(CoreStorage.self)! | ||
private let api = Container.shared.resolve(API.self)! |
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 fine with this, but as we changed the dependency injection in the FCMListner, do we need to change it 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.
Yes, it makes sense to provide them as dependencies on the init lever 👍
@volodymyr-chekyrta I've left few comments, please take a look. After that we are good to go with this PR. |
@saeedbashir, thanks for the feedback; PR is ready for final pass 🚀 |
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.
Remove the unused import import Swinject
in FCMProvider.swift
and you are good to go 👍
b9ebbae
to
6ccd026
Compare
Sure, done ✅ |
@rnr @saeedbashir approval was dismissed after the fix, could you please provide the approval and we'll merge it |
@volodymyr-chekyrta 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
This pull request introduces Firebase Cloud Messaging (FCM) to the project, enabling the functionality for receiving push notifications from Firebase.
Testing:
• Verified push notifications are received when the app is in the foreground, background, and terminated states.
• Confirmed the app correctly handles notification taps and displays relevant content.
• Ensured token generation and handling work as expected, including handling token refreshes.
Please use the Firebase console for testing; here is a set of all the required fields for navigation:
Notifications:
notification_type:
enroll
course_id: Your course ID (example: course-v1:future+f1+2024)
notification_type:
unenroll
notification_type:
add_beta_tester
course_id: Your course ID (example: course-v1:future+f1+2024)
notification_type:
remove_beta_tester
course_id: Your course ID (example: course-v1:future+f1+2024)
notification_type:
forum_response
topic_id: Your Topic ID (example: course)
course_id: Your course ID (example: course-v1:future+f1+2024)
thread_id: Your thread ID (example: 66584e96f2195c001ad219c7)
comment_id: Your comment ID (example: 66585c9df2195c001ad219ce)
notification_type:
forum_comment
topic_id: Your Topic ID (example: course)
course_id: Your course ID (example: course-v1:future+f1+2024)
thread_id: Your thread ID (example: 66583d13f2195c001a56a182)
parent_id: Your response ID (example: 66586579f2195c001ad219d8)
comment_id: Your comment ID (example: 6659ac92f2195c001bf37f77)