Some registrations don't seem to be registered in Notification Hub - ios

I'm trying to integrate my Android and iOS native apps with Notification Hub.
For example, on iOS, as soon as I receive the deviceToken and if my user is already authenticated, I register directly with NotificationHub:
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
log.debug("Received device token \(deviceToken.hexEncodedString()). Saving device token to user defaults.", "AppDelegate.didRegisterForRemoteNotificationsWithDeviceToken")
let defaults = UserDefaults.standard
defaults.set(deviceToken, forKey: UserNotificationDeviceTokenKey)
defaults.synchronize()
if let authenticatedUser = Session.shared.authenticatedUser {
log.debug("Trying to register device token \(deviceToken.hexEncodedString()) to user \(authenticatedUser.id) on notification hub with hub name \(String(describing: hubName)) and connection string \(String(describing: connectionString)). Notification hub null? \(notificationHub == nil)")
self.notificationHub?.registerNative(withDeviceToken: deviceToken, tags: Set<String>([authenticatedUser.id]), completion: { error in
if (error != nil) {
log.error("Error registering for notifications: \(error.debugDescription)", "AppDelegate.didRegisterForRemoteNotificationsWithDeviceToken");
} else {
log.debug("Successfully registered device token \(deviceToken.hexEncodedString()) for user with ID \(authenticatedUser.id) and email \(authenticatedUser.emailAddress)", "AppDelegate.didRegisterForRemoteNotificationsWithDeviceToken")
}
})
}
}
And whatever happens, I start by saving the device token to the user defaults so that when a user does log in, he can retrieve the deviceToken from user defaults and call the same registerNative method to create a registration associating this deviceToken with a tag that is the user's identifier:
func userDidSignIn(user: User) {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let deviceToken = UserDefaults.standard.data(forKey: UserNotificationDeviceTokenKey){
log.debug("Trying to register device token \(deviceToken.hexEncodedString()) to user \(user.id) on notification hub with hub name \(String(describing: appDelegate.hubName)) and connection string \(String(describing: appDelegate.connectionString)). Notification hub null? \(appDelegate.notificationHub == nil)")
appDelegate.notificationHub?.registerNative(withDeviceToken: deviceToken, tags: Set<String>([user.id]), completion: { error in
if (error != nil) {
log.error("Error registering for notifications: \(error.debugDescription)");
} else {
log.debug("Successfully registered device token \(deviceToken.hexEncodedString()) for user with ID \(user.id) (\(user.emailAddress)) who just logged in", context: "HTRootViewController.userDidSignIn")
}
})
} else {
log.warning("No device token found in settings.", "HTRootViewController.userDidSignIn")
}
}
I have all my logs remotely sent to Bugfender, which lets me check the logs for all the devices using this app installed in TestFlight, so I can see that all the registerNative's are successful:
Trying to register device token
0fea9a4d99ec37dc4f3ac252c35fa4e1617004fd14740973d80a7dfdaeacc857 to
user 77bfb1c6-b05a-440b-a7a0-71ae5a91bbb2 on notification hub with hub
name Optional("[my notification hub name]") and connection string
Optional("[my notification hub connection string]").
Notification hub null? false
Successfully registered device
token 0fea9a4d99ec37dc4f3ac252c35fa4e1617004fd14740973d80a7dfdaeacc857
for user with ID 77bfb1c6-b05a-440b-a7a0-71ae5a91bbb2
([my user's email]) who just logged in
But then, when I load the list of all the registrations using this .NET C# code:
public async Task<List<RegistrationDescription>> GetRegistrations()
{
var hub = NotificationHubClient.CreateClientFromConnectionString([my notification hub connection string], [my notification hub name]);
var result = await hub.GetAllRegistrationsAsync(1000);
return result.ToList();
}
I can see that some registrations are there, but others are nowhere to be found, even though they succeeded according to my mobile logs.
Does anyone know what else could be going on? What else can I check? Why else would registrations not be saved even though the call to registerNative seems to succeed?
EDIT: As a matter of fact, and after checking more precisely what is happening, the registration first appears in the list of registrations returned by Notification Hub, but as soon as we try to send a notification, it's as if Notification Hub could not reach the corresponding device token and deleted the registration altogether. Is there a way to see the logs of Notification Hub to see what is going on and why it decides to delete this registration?

I figured it out. It turns out it was working on my device because I ran the beta in debug mode, so it was getting its device token from the Sandbox APS environment, which matched the Sandbox endpoint configured in Notification Hub, but other users were using the Production APS environment configured in the archive build, so they got device tokens for a different environment and notification hub correctly identified as incorrect device tokens. Once we switched the Notification Hub to the production APNS endpoint, all the TestFlight users were able to receive notifications because their registrations stayed in Notification Hub. Now that does mean that my debug builds trying to use that environments won't receive notifications, but that's OK, that's why we have a different environment and Notification Hub for development purposes. So if anyone sees something similar, don't forget to double check your Notification Hub configuration matches the type of build your users are using, and don't forget that TestFlight builds are now production-type builds, same as App Store builds.

Related

Do i need to "didReceiveRegistrationToken"?

I want to send notifications, but only to authenticated users. This means, if my token updates (potential reasons: app is restored on a new device, user uninstalls/reinstall the app or user clears app data) my user will also not be authenticated.
Thus, whenever my user tries to login i update the token, which i receive via this method, for my backend:
Messaging.messaging().token { token, error in
// ...
}
When the user logs out i delete the token, so why would i implement this method here:
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
}
However, if i do not implement it, i receive this:
9.5.0 - [FirebaseMessaging][I-FCM002023] The object <Name.Delegate: 0x2827c0650> does not respond to -messaging:didReceiveRegistrationToken:. Please implement -messaging:didReceiveRegistrationToken: to be provided with an FCM token.
In this function you must Save your device "Token" or "refresh token" as Local:
"didReceiveRegistrationToken"
When the user logged in you must register the push-token in the backend
When the user logged out you must clear the push-token in the backend
You must after login set a key value in "KeyChain" such as:
"userId" : "x"
"For the users that before login, cleared or uninstalled the app":
In each launch you must check your app user is login or not?
if "NO" (doesn't have the token):
You must clear the push-token with "userId -> x" that you saved on keychain. (request it to the backend)
Albeit the new push-token is different with the previouse push-token.
if "YES" (user had backend token)
you must save the new "push-token" or "refresh-push-token" from below function:
"didReceiveRegistrationToken"

How to get Firebase FCM token outside the Swift AppDelegate

I have a SwiftUI app and inside it's custom AppDelagte I get the FCM token and save it to my database. Which works.
But when the user registers and gets put to the main screen of the applcation, their FCM token isn't uploaded to the database until they restart the app and the AppDelagte messaging function is run again. So basically, when the user registers, the app will not function correctly until they restart the app.
I have tried to get and then send the FCM token as soon as they successfully register via:
Messaging.messaging().fcmToken
which returns an empty string, meaning it is only made after they re run the app delegate.
How do I get around this, and send the FCM token to their database profile as soon as they register?
You could store the fcmToken on a static property of the AppDelegate as soon as you get it and then access it whenever the user registers.
Add a static property to the app delegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
static var fcmToken: String?
// when you get the token set it to the static property
Self.fcmToken = fcmToken
}
Then when you need the token simply access:
AppDelegate.fcmToken
You can use this method at any time to access the token instead of storing it.
Messaging.messaging().token { token, error in
if let error = error {
print("Error fetching FCM registration token: \(error)")
} else if let token = token {
print("FCM registration token: \(token)")
self.fcmRegTokenMessage.text = "Remote FCM registration token: \(token)"
}
}
https://firebase.google.com/docs/cloud-messaging/ios/client

Testing passwordless auth in Firebase test lab for iOS

I am trying to figure out how to perform e2e test via firebase test lab for iOS that allow to check passwordless authentication flow, which essentially should do following
Enters email within my app
Firebase sends auth link to such email
Somehow I need to be logged into such email somewhere in firebases test device, I assume either in mail app, or gmail?
I need to know when new email arrives and open it
Once I opened an email I need to click on auth link
This should bring me back into the app and authenticate
My biggest issue at the moment is figuring out steps that happen outside my app i.e. how can I prepare for this test and log in under my email address (is it better to log into gmail in safari for example or somehow add this acc to apples mail app?).
Testing email
In my experience, testing your own code to see if an email was sent is not straightforward beyond checking if the method call you expect to send the email has happened.
Add on top of that using Firebase, which does not expose its underlying email send code, and that looks like a challenge to me.
In terms of testing, I suggest you assert that your method calls to send email happened or that the relevant code path was reached. In Firebase web, this looks like:
firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings)
.then(function() {
// The link was successfully sent. Inform the user.
// Save the email locally so you don't need to ask the user for it again
// if they open the link on the same device.
window.localStorage.setItem('emailForSignIn', email);
// TODO save email to something accessible in your iOS tests
// TODO In your tests, confirm that email was saved after it was sent
})
.catch(function(error) {
// Some error occurred, you can inspect the code: error.code
});
See: https://firebase.google.com/docs/auth/web/email-link-auth#send_an_authentication_link_to_the_users_email_address
Another option:
You could setup a test user with an email address on a mail server that you manage, and check for incoming mail for that test user with your own custom mail reading code.
I would use Firebase Admin tools for this: https://firebase.google.com/docs/auth/admin/manage-users#create_a_user
I think you should first take a look at firebase docs for iOS on how to create dynamic links that you can use for email auth.
https://firebase.google.com/docs/auth/ios/email-link-auth
https://firebase.google.com/docs/auth/ios/passing-state-in-email-actions#configuring_firebase_dynamic_links
After you're done with those two check out the following code:
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
// [END old_delegate]
if handlePasswordlessSignIn(withURL: url) {
return true
}
}
func handlePasswordlessSignIn(withURL url: URL) -> Bool {
let link = url.absoluteString
// [START is_signin_link]
if Auth.auth().isSignIn(withEmailLink: link) {
// [END is_signin_link]
UserDefaults.standard.set(link, forKey: "Link")
(window?.rootViewController as? UINavigationController)?.popToRootViewController(animated: false)
window?.rootViewController?.children[0].performSegue(withIdentifier: "passwordless", sender: nil)
return true
}
return false
}
This is just an example on how you can handle the deep link in your app after the user taps the link. The delegate method
func application(_ application: UIApplication, open url: URL,
sourceApplication: String?, annotation: Any) -> Bool
in AppDelegate is used for all deep links into an app. You could set up for example your own scheme that your app conforms to. And you can send url type links with your custom scheme into your app from the browser for example.
To do this just Open Xcode, go to Project Settings -> Info, and add inside ‘The URL Types” section a new URL scheme. Add something of the sort of com.myApp in order for it to be as unizue as possible. Then you can just type into a browser com.myApp://main and handle that in the appDelegate.
Edit: It says so in their docs that you can present a prompt inside the app for the user to input the email. Where the user opens his email from isn't really your concern as long as your dynamic link is set up properly.

Notification Hub Devices disappearing after Push (APNS)

We have an Azure Notification Hub set up, with APNS Configured in Production Mode, using our Production APNS Certificate.
We register our test devices (using TestFlight / Production build, and certificate) to APNS, and then to ANH with a tag dealer-1. We can send notifications using our production certificate and registered Device ID with success when using APNS directly, however, when we use 'Test Send' we get a 'Successful send' to 1 device (or however many we have registered). The notification is not received. If we then run 'Test Send' again, the are 0 devices to send to.
In the logs, we see 'APNS Errors' per device, per test send. I cannot see any way to view what the errors actually are though so this is an absolutely useless metric.
I have ran through all the troubleshooting steps and confirmed many times that everything is setup in 'Production'.
Having reviewed other questions, the answers have been along the lines of:
.. registering a sandbox certificate and then changing it to production. Unfortunately we created this hub from scratch as Production in an attempt to work around that potential issue.
.. registering sandbox devices (and thus tokens) against the production certificate. Unfortunately I have controlled this closely and ensured that we are only registering TestFlight builds (thus Production) against the ANH.
.. uploading the wrong certificate. I have confirmed with the Push Notification Tester, as above, that the certificate is correct (thumbprint confirmed, re-uploaded, etc) and works to send to the devices via Production APNS endpoint.
The resource name is: eight-technology/react-push-notification-hub
In-app registration process is as follows:
Device registers for push notifications
Registration event is handled in iOS Project (AppDelegate event)..
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
//base.RegisteredForRemoteNotifications(application, deviceToken);
App.ConfigurePushNotifications(deviceToken.ToString());
}
ConfigurePushNotifications is in the XF Shared Project..
public static void ConfigurePushNotifications(string deviceToken)
{
var azureComm = DependencyService.Get<Interop.IAzureCommunication>();
azureComm.RegisterForPushTags(
"sb://eight-technology.servicebus.windows.net/",
".. token ..",
"react-push-notification-hub",
deviceToken,
StateManager.SelectedNodes.Select(m => "dealer-" + m).ToArray());
}
The implementation is pretty much as per the sample code provided (contained in iOS project)
public class AzureCommunication : DealerwebReact.Interop.IAzureCommunication
{
public void RegisterForPushTags(string url, string key, string hubName, string deviceToken, string[] tags)
{
var cs = SBConnectionString.CreateListenAccess(new NSUrl(url), key);
var hub = new SBNotificationHub(cs, hubName);
hub.RegisterNativeAsync(deviceToken, new NSSet(tags), err =>
{
if (err != null)
Console.WriteLine("Error: " + err.Description);
else
Console.WriteLine("Success");
});
}
}
After a frustrating few days, and thanks to the help of Nikita G. and hvaughan3 I finally got to the root cause of my issue. As anticipated it wasn't any of the issues actually outlined, but was to do with the way we handled the cross-plat aspect of the registrations with Xamarin Forms.
That is, we stored our token in a class as a string. The NSData that is received as part of the iOS Device registration in RegisteredForRemoteNotifications has a ToString() method that is incompatible with sending to ANH. Furthermore, RegisterNativeAsync method from the Azure library requires an NSData which I assume Xamarin can morph a string into without warning or error, hence it was unknown that the two were somewhat incompatible.
Basically, to maintain cross platform functionality, we are now simply passing the token around as an object and performing the translation back to the original type in the platform-specific implementation of our push configuration method.
Our registration method now looks like this, note the explicit use of the NSData type so that it remains untouched whilst passing through the Xamarin Forms layer:
public void RegisterForPushTags(string url, string key, string hubName, object deviceToken, string[] tags)
{
var cs = SBConnectionString.CreateListenAccess(new NSUrl(url), key);
var hub = new SBNotificationHub(cs, hubName);
hub.RegisterNativeAsync((NSData)deviceToken, new NSSet(tags), err =>
{
if (err != null)
Console.WriteLine("Error: " + err.Description);
else
Console.WriteLine("Success");
});
}
Is this the guide you used for troubleshooting?
Is there a chance you somehow do any string (or any other type of) processing on the APN handle before you register your device? The 'APNS errors' you're seeing seem to be 'invalid token size'. Since I don't know what's going on in your code, it's hard to suggest what exactly might it be, but maybe would help you.
A similar thing happened to me when the device would register correctly but as soon as a notification was sent, the devices would disappear from the list. It always turned out to be an issue with the APNS certificate that was configured within the Notification Hub was not connected properly to the App ID and/or the app was not being signed with the correct provisioning profile.

Parse cloud push notifications are pushing to all users instead of targeted user

I'm having trouble targeting the push notifications to a single user. Right now my test device is receiving a push notification no matter which user is logged into the device. Even when I send a push to _User that is currently logged out and the device is logged in with a completely different _User it receives the push notification.
I have set up cloud code with parse.com to enable push notifications to be sent to users. I have a "user" pointer in the installation class that points to the cooresponding _User that is to receive the push notification.
Example: _User "Sam" is logged in to the mobile device. I send a push from the iOS simulator in Xcode to _User "steve". The mobile device receives the push even though "Sam" is logged in. It receives the push even if NO user is logged in.
When I check the Push control on parse, it seems like the cloud cloud is correctly querying the "user" pointer in the installation that matches the objectId for the _User class, as seen here:
user advanced operator ($inQuery {"className"=>"_User", "objectId"=>"LJQXaJDzkU"})
SENDING TIME
August 5th, 2015 at 3:01 PM
EXPIRATION
None
FULL TARGET
{
"user": {
"$inQuery": {
"className": "_User",
"objectId": "LJQXaJDzkU"
}
}
}
FULL DATA
This is the cloud code that queries the user and sends the push from the parse cloud. The recipientUserId receives a string of the objectId for the _User whom is receiving the push.
Parse.Cloud.define("sendPush", function(request, response) {
var senderUser = request.user
var recipientUserId = request.params.recipientId;
var senderUserId = request.params.senderId;
var message = senderUserId + " sent a message"
var recipientUser = new Parse.User();
recipientUser.id = recipientUserId;
var pushQuery = new Parse.Query(Parse.Installation);
pushQuery.matchesQuery('user', recipientUser);
Parse.Push.send ({
where: pushQuery,
data: {
aps: {
alert: message,
badge: "Increment",
sound: "cow-moo1.wav"
}
}
}, {
success: function() {
response.success("Success!");
},
error: function(error) {
response.error(error);
}
});
});
The pushes send no problem, and I can open them on the device but I'm really lost as to why the pushes are being sent to the device even when the queried user isn't even logged in to the device. Any ideas as to whats going on would be greatly appreciated. Thanks!!!
Edit your code so that when you log out, you're removing the user from the installation.
Edit - This was mentioned in the comments below, so I want to make sure it was mentioned here in case it was the solution that worked. OP used matchesQuery, which requires a query to be passed in, but was passing in a Parse.User object instead. I suggested he used equalTo instead of matchesQuery.

Resources