iOS Push Notifications with Azure Notification Hub - xamarin.forms

I am having absolutely no luck getting push notifications to work in iOS in a Xamarin Forms project.
In AppDelegate.cs, I am calling the following in the FinishedLaunching override:
MSNotificationHub.Start("Endpoint=sb://[redacted].servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey=[redacted]",
"[redacted]");
After the user logs in further in the app lifecycle, I also register the user with their user tag as follows:
public async Task UpdateTags(string token)
{
await Task.Run(() =>
{
try
{
// No point registering tags until the user has signed in and we have a device token
if (CurrentAccount == null)
{
Console.WriteLine($"UpdateTags cancelled: Account is null");
return;
}
var tag = $"user:{CurrentAccount.UserName}";
Console.WriteLine($"Registering tag: {tag}");
MSNotificationHub.AddTag(tag);
}
catch (Exception e)
{
Console.WriteLine($"Error registering tag: {e.ToString()}");
}
});
}
I have properly configured the Apple (APNS) settings in the notification hub, using the Token authentication mode (verified the four fields several times). The certificate (signing identity) is "iOS Distribution", the identifier bundle matches exactly what I have in the configuration (not using wildcard), the key has Apple Push Notifications service (APNs) enabled, and the provisioning profile has Platform: iOS and Type: App Store.
I pushed the application to TestFlight, as I don't have access to a physical Mac (we use a Cloud mac for development). When I view the device logs from my personal iPhone with the app installed, I see the following when I run it:
<Notice>: Registered for push notifications with token: [redacted]
<Notice>: Registering tag: user:[redacted]
There are no instances of "Error registering tag" or "UpdateTags cancelled" in the logs at all, which tells me that the method calls are succeeding without an exception. However, when I attempt to send a test notification to either a blank/empty tag, or the specific tag for my test user, no notifications are received and the messaging simply shows "Message was successfully sent, but there were no matching targets."
Also, when I pull all of the registrations with var registrations = await hub.GetAllRegistrationsAsync(0);, I only see the FCM (Firebase/Android) registrations from my successful testing on the Android side of things.
I am at a complete loss and have hit a wall, as there are no exceptions being thrown, and seemingly no way to troubleshoot what is going on behind the scenes.
This is also my 2nd attempt - I was using a more complex SBNotificationHub implementation and had the same results - no exceptions and everything looked fine at face value.

Thanks to a comment pointing to another question, I have determined that all I needed to do was to ensure that my tag registration ran on the main UI thread. My updated code below is working:
public async Task UpdateTags(string token)
{
await Task.Run(() =>
{
Device.BeginInvokeOnMainThread(() =>
{
try
{
// No point registering tags until the user has signed in and we have a device token
if (CurrentAccount == null)
{
Console.WriteLine($"UpdateTags cancelled: Account: {Trico.OrbitalApp.App.CurrentAccount};");
return;
}
var tag = $"user:{CurrentAccount.UserName}";
Console.WriteLine($"Registering tag: {tag}");
MSNotificationHub.AddTag(tag);
}
catch (Exception e)
{
Console.WriteLine($"Error registering device: {e.ToString()}");
}
});
});
}

You can try implementing the MSInstallationLifecycleDelegate interface which will allow you to check and see if the installation is being saved on the back end with either success or failure.
// Set a listener for lifecycle management
MSNotificationHub.SetLifecycleDelegate(new InstallationLifecycleDelegate());
// Implementation of the lifecycle listener.
public class InstallationLifecycleDelegate : MSInstallationLifecycleDelegate
{
public InstallationLifecycleDelegate()
{
}
public override void DidFailToSaveInstallation(MSNotificationHub notificationHub, MSInstallation installation, NSError error)
{
Console.WriteLine($"Save installation failed with exception: {error.LocalizedDescription}");
}
public override void DidSaveInstallation(MSNotificationHub notificationHub, MSInstallation installation)
{
Console.WriteLine($"Installation successfully saved with Installation ID: {installation.InstallationId}");
}
}

Related

Xamarin Forms iOS - Saving a user tag in Azure Notification Hubs works in AppDelegate but not in a service

I'm currently trying to get push notifications working for my mobile app using Azure Notification Hubs. Android is working fine and the initial iOS set up in AppDelegate works ok with a sample tag.
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
if (deviceToken == null)
{
return;
}
SBNotificationHub hub = new SBNotificationHub(CommonConstants.LISTEN_CONNECTION_STRING, CommonConstants.NOTIFICATION_HUB_NAME);
// update registration with Azure Notification Hub
hub.UnregisterAll(deviceToken, async (error) =>
{
if (error != null)
{
System.Diagnostics.Debug.WriteLine($"Unable to call unregister {error}");
return;
}
string[] tags = new[] { "iostestpush" };
NSSet userTags = new NSSet(tags);
hub.RegisterNative(deviceToken, userTags, (error) =>
{
if (error != null)
{
System.Diagnostics.Debug.WriteLine($"Unable to call register {error}");
return;
}
});
var templateExpiration = DateTime.Now.AddDays(120).ToString(System.Globalization.CultureInfo.CreateSpecificCulture("en-US"));
hub.RegisterTemplate(deviceToken, "defaultTemplate", CommonConstants.APN_TEMPLATE_BODY, templateExpiration, userTags, (errorCallback) =>
{
if (errorCallback != null)
{
System.Diagnostics.Debug.WriteLine($"RegisterTemplateAsync error: {errorCallback}");
}
});
});
}
The issue I'm having is I need to register the UserId after a successful login. So I set up a service with the above code, saved the token to the device as string so it can be retrieved in the service and turned back into an NSData token
NSData deviceToken = new NSData(token, NSDataBase64DecodingOptions.None);
After a successful login I send the token string and the tag array to my service.
string[] userTag = new[] { loginResponse.UserId.ToString() };
await this._azureReg.SendRegistrationToServer(deviceToken, userTag);
Which, other than turning the token back into NSData and the user tag into an NSSet, is the same as above other than the name change. But Azure is claiming there is no registration even though my output shows
Registered for push notifications with token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
I thought it was the string conversion back and forth, so tested that in the AppDelegate and it worked fine.
So, I'm at a loss at how to register the UserId after a successful login and why it works in one place but not the other.
I hope that's clear and thanks for any advice in advance.
You probably ran into the same bug as me and several others.
Basically SBNotificationHub method overloads like UnregisterAll and RegisterTemplate with the callback signature do not work when you use them off the main thread, using the libraries to date. I was also using a Service for the same purpose (to handle push across platforms with different tags, especially for user id) but my implementation involved switching off the main thread for this.
The bug we logged and is now being addressed is here: https://github.com/Azure/azure-notificationhubs-ios/issues/95
The solution, for now, is to ditch SBNotificationHub completely. The Xamarin / Azure documentation is out of date, and SBNOtificationHub is legacy code. The recommended library is MSNotificationHub. https://github.com/azure/azure-notificationhubs-xamarin
As workarounds you can use the SBNotificationHub method overloads that do not involve callbacks (they return an error message instead) or the workaround in the 95 issue above.

How to disable Firebase push notifications in Unity from client side application?

I have integrated Firebase into Unity project and it all works. I would like to integrate disabling of push notifications if user wants to disable them in his app. I haven't found solution in code for this, also there is one unanswered question same as mine, so I am posting new one just in case someone has come with a solution for this.
I managed to do this by using Firebase Messaging Topic and calling SubscribeAsync() and UnsubscribeAsync().
private void Start()
{
if ( notificationsAreOn )
InitFirebaseMessaging();
else
DisableFirebase();
}
private void DisableFirebase()
{
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith( task =>
{
var dependencyStatus = task.Result;
if ( dependencyStatus == Firebase.DependencyStatus.Available )
{
FirebaseAnalytics.SetAnalyticsCollectionEnabled( false );
Firebase.Messaging.FirebaseMessaging.UnsubscribeAsync("VeryCoolTopic");
Firebase.Messaging.FirebaseMessaging.TokenReceived += null;
Firebase.Messaging.FirebaseMessaging.MessageReceived += null;
}
else
{
UnityEngine.Debug.LogError(
System.String.Format("Could not resolve all Firebase dependencies: {0}", dependencyStatus)
);
}
});
}
private void InitFirebaseMessaging()
{
Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith( task =>
{
var dependencyStatus = task.Result;
if ( dependencyStatus == Firebase.DependencyStatus.Available )
{
FirebaseAnalytics.SetAnalyticsCollectionEnabled( true );
Firebase.Messaging.FirebaseMessaging.SubscribeAsync("VeryCoolTopic");
Firebase.Messaging.FirebaseMessaging.TokenReceived += OnTokenReceived;
Firebase.Messaging.FirebaseMessaging.MessageReceived += OnMessageReceived;
}
else
{
UnityEngine.Debug.LogError(
System.String.Format("Could not resolve all Firebase dependencies: {0}", dependencyStatus)
);
}
});
}
Then in the Firebase console when creating a message, use Topic as a target instead of User Segment.
You could also use disable whole Firebase by deleting the Firebase token using DeleteTokenAsync() but I haven't tested this because the the method with using subscribe and unsubscribe worked for me.
There are two types of messages that you can send with FCM: notifications messages, and data messages.
If your app is active, notification messages are delivered to your application code, which can decide what to do with it. When the app is not active, notification messages are automatically displayed by the system. There is not way to suppress this behavior.
Data messages are always delivered to your application code, which can decide what to do with them.
So if you want to allow the user to suppress the display of messages, you'll want to only send data messages, and then display them from within your application code.
Note that alternatively, you can find a way to not deliver messages to a user who has disabled notifications. How exactly to do this depends on your implementation. For example: if you're sending directly to FCM Instance ID tokens, you can skip the tokens of users who have disabled push notifications. And if you're using topic subscription to send messages, you can create a topic that users subscribe to to disable notifications, and then create conditions to exclude delivery to that topic.

Weird login behaviour using Azure AD B2C with Xamarin iOS

We have a Xamarin Forms app that uses Azure AD B2C for login. I have followed this example code https://github.com/Azure-Samples/active-directory-b2c-xamarin-native
The Android version works perfectly. The workflow is seamless and works as expected.
On the iOS version we are getting some weird behaviour though.
Here is the flow on the iOS version of the app.
User launches the app and is presented with the Azure AD B2C login screen (correct)
User enters their credentials (correct)
The app redirects the user back to the login screen (with the username and password fields blank) (wrong)
After a few seconds the app then logs them in and navigates them to the main screen
After entering their credentials the user should be immediately navigated to the app main screen, NOT back to the login screen.
Once they are logged in, subsequently launching the app correctly logs them in. The problem is only on first login.
I can't seem to find any help on this anywhere. Is it the Safari browser, the token cache, something else? The code and workflow are correct as the Android version works perfectly, so it's something specific to the iOS app.
UPDATE
After some investigation it seems that this method may be causing the issue.
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(url);
return true;
}
According to the docs
this logic is meant to ensure that once the interactive portion of the
authentication flow is concluded, the flow goes back to MSAL
But it doesn't. It redirects the user back to the OnAppearing() method of the login page where it attempts (again) to acquire a token (which it does).
Is there any way to fix the behaviour of this method?
AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(url)
UPDATE2
Here is the login code (simplified for clarity)
protected override async void OnAppearing()
{
base.OnAppearing();
// Check to see if we have a User in the cache already.
try
{
IEnumerable<IAccount> accounts = await AuthenticationService.PCA().GetAccountsAsync();
var account = this.GetAccountByPolicy(accounts, ApplicationConstants.SignUpSignInPolicy);
AuthenticationResult ar = await AuthenticationService.PCA().AcquireTokenSilentAsync(ApplicationConstants.Scopes, account, ApplicationConstants.Authority, false);
}
catch (Exception)
{
// Doesn't matter, we go in interactive mode
this.OnSignIn();
}
}
private async void OnSignIn()
{
try
{
IEnumerable<IAccount> accounts = await AuthenticationService.PCA().GetAccountsAsync();
var account = this.GetAccountByPolicy(accounts, ApplicationConstants.SignUpSignInPolicy);
AuthenticationResult ar = await AuthenticationService.PCA().AcquireTokenAsync(ApplicationConstants.Scopes, account, App.UiParent);
}
catch (Exception ex)
{
InstanceManager.LoggingHelper().TrackError(ex);
// Checking the exception message
// should ONLY be done for B2C
// reset and not any other error.
if (ex.Message.Contains("AADB2C90118"))
{
this.OnPasswordReset();
}
else
{
await DisplayAlert(StringConstants.ExceptionText, StringConstants.AuthenticationError, StringConstants.CloseDialog);
this.OnSignIn();
}
}
}

Xamarin android: Async await calls not working when app open by clicking push notification

When I open app by tapping on FCM push notification, The API service calls I am making by using await keyword those are not working. Entire app not returning data.
Code for API calling
var result = await objHomework.GetHomeWorksForStudentPagesAsync(studentId.ToString());
result returning null. if app already open, everything working fine. See the Image below screenshot of app
Notification messages are delivered to OnMessageReceived callback only when the app is in the foreground.
Override the HandleIntent Method of the FirebaseMessageService to work for background as well
public override void HandleIntent(Intent intent)
{
try
{
if (intent.Extras != null)
{
var builder = new RemoteMessage.Builder("MyFirebaseMessagingService");
foreach (string key in intent.Extras.KeySet())
{
builder.AddData(key, intent.Extras.Get(key).ToString());
}
this.OnMessageReceived(builder.Build());
}
else
{
base.HandleIntent(intent);
}
}
catch (Exception)
{
base.HandleIntent(intent);
}
}
Actually, I was missing some keys which is necessary for service call authentication in my project. I am getting those keys in MaiActivity but notification click even starting app from somewhere else therefore keys values was null and service calls was not happening.

Using SignalR to send message to client from Azure Worker Role

I'm working an ASP.net MVC cloud service project running two roles, a web role and a worker role. One of the pages in the web role initiate a request to build an APK file, building an APK file on the server can take anywhere from 1-5 minutes. So we came up with the following flow:
The user initiate the APK building process on the page.
The request is routed to our mvc action, creating a new message on an Azure Storage Queue.
The Worker role is always polling from the queue and starts the APK building process. Now that the APK is ready we want ideally to notify the user by:
(a) sending an email, which is working now. and (b) notifying the user on the page using SignalR.
Our problem is now in the SignalR part, how can we notify the user on the page that the APK is ready and he can download it.
EDIT - Copying contents of the first comment for the sake of completeness -
I've looked the question again and I understand that you are using a worker role to poll the queue. In this case, you can make your work role a .Net SignalR client that connects to the APK signalR hub on the web role. The signlaR hub on the web role can simple forward any message it receives from the .Net client to the javascript client (browser).
I would recommend going through the below links
Hubs API Guide - Server
Hubs API Guide - Javascript Client
before going through rest of the answer.
As can be understood from the above two links, SignalR enables the server to 'push' data to the client. In order for this to happen, you require two things -
A signalR hub - this is the 'hub' to which clients can subscribe to in order to receive messages.
A client connected to the hub
Your signalR hub on the server can look something like this -
public class APKHub : Hub
{
public async Task JoinGroup(string groupName)
{
await Groups.Add(Context.ConnectionId, groupName);
Clients.Group(groupName).sendMessage(Context.User.Identity.Name + " joined.");
}
public Task LeaveGroup(string groupName)
{
return Groups.Remove(Context.ConnectionId, groupName);
}
public void NotifyUser(string userId)
{
this.Clients.Group(userId).notify();
}
}
On the client, your code might look something like this -
var notificationHandler = function () {
var url;
var user;
var init = function (notificationUrl, userId) {
url = notificationUrl;
user = userId;
connectToAPKHub();
}
var connectToAPKHub = function () {
$.connection.hub.url = url;
var apk= $.connection.apkHub;
apk.client.notifyUser = function (user) {
console.log(user);
}
apk.client.addMessage = function (message) {
console.log(message);
}
$.connection.hub.start().done(function () {
console.log('connected to apkhub');
apk.server.joinGroup(user);
})
}
return {
init: init
}
}();
The notificationUrl is the URL that the signalR server is listening to.
This sets up your basic hub on the server and you should now be able to connect your client to the signalR hub. When the APK is built, you can use the following code (place it anywhere - for ex - in a controller action) to actually push a message to the concerned client -
var apkHub = GlobalHost.ConnectionManager.GetHubContext<APKHub>();
apkHub.Clients.Group(groupName).notifyUser(groupName);
The groupName can be an identifier that uniquely identifies a user.
Hope this helps.

Resources