How to deal with device settings in Xamarin.Forms? - xamarin.forms

I have a Xamarin.Forms application supporting Android, iOS, and UWP. I need the application at the starting point to check if location feature is enabled, and if not, suggest the user to do so, and let him open device Settings. But if the user agrees to open the Settings, I would need the application to wait for the results, and then continue based on the newly changed settings. But how can I make the execution wait for the user to finish working with the settings?
Here is the code I have for now:
Task<bool> task = Application.Current?.MainPage?.DisplayAlert("Location service is disabled on this device",
"MyCompany Mobile uses your location to provide you with the correct product mix and other information for your market. Please go into Settings and turn on Location for the device.",
"Settings",
"Maybe Later");
if (task == null)
{
return bu;
}
bool result = await task;
if (result)
{
IDeviceService deviceService = DependencyService.Get<IDeviceService>();
if (deviceService != null)
{
bool openedSuccessfully = await deviceService.OpenDeviceSettingsAsync();
}
}

Related

Firebase gets logged out after long time in Flutter Web

I'm developing a web app and I use Firebase Authentication for the authentication service.
The project seems to store the authentication, since if I refresh the page, or close the browser, the user is still logged in.
However I noticed that if I don't access the app for a long time (more than 1 hour, after the night for example), the authentication gets lost.
I don't know how to debug this and how to solve this.
Following some snippets of code to better understand my implementation:
This is the function I have in my startup view to redirect the user to the right page based on auth status.
bool isUserLoggedIn() {
var user = _firebaseAuth.currentUser;
return user != null;
}
void handleStartupBasedOnAuthStatus() {
Future.delayed(const Duration(milliseconds: 1000), () async {
bool loggedInShared =
await sharedPreferences.getBoolSharedPreferences("loggedIn");
if (isUserLoggedIn() || loggedInShared) {
String ruoloValue =
await sharedPreferences.getSharedPreferences('ruolo');
(ruoloValue == Ruolo.ADMIN)
? navigationService.replaceWith(Routes.admin)
: navigationService.replaceWith(Routes.messages);
} else {
navigationService.replaceWith(Routes.login);
}
});
}
In the following function I call the onAuthStateChange to set sharedpreferences accordingly. I have the check on the timestamp because I noticed that it is triggered more time once the page is refreshed.
void listenToAuthChangesSharedPref() {
FirebaseAuth.instance.authStateChanges().listen((firebaseUser) async {
var datetimeNow = (DateTime.now().millisecondsSinceEpoch);
String oldDatetimeString =
await sharedPreferences.getSharedPreferences('previous_timestamp');
if (oldDatetimeString != null) {
var oldDatetime = (new DateTime.fromMillisecondsSinceEpoch(
int.parse(oldDatetimeString)))
.millisecondsSinceEpoch;
if (datetimeNow - oldDatetime > 1000) {
if (firebaseUser == null) {
await sharedPreferences.setBoolSharedPreferences('loggedIn', false);
} else {
await sharedPreferences.setBoolSharedPreferences('loggedIn', true);
}
await sharedPreferences.setSharedPreferences(
'previous_timestamp', datetimeNow.toString());
}
} else {
if (firebaseUser == null) {
await sharedPreferences.setBoolSharedPreferences('loggedIn', false);
} else {
await sharedPreferences.setBoolSharedPreferences('loggedIn', true);
}
await sharedPreferences.setSharedPreferences(
'previous_timestamp', datetimeNow.toString());
}
});
}
My question is: is possible that after long time currentUser and also the onAuthStateChanges gets called and the user is not logged in?
Persisting authentication state#
The Firebase SDKs for all platforms provide out of the box support for ensuring that your user's authentication state is persisted across app restarts or page reloads.
On native platforms such as Android & iOS, this behaviour is not configurable and the user's authentication state will be persisted on-device between app restarts. The user can clear the apps cached data via the device settings which will wipe any existing state being stored.
On web platforms, the user's authentication state is stored in local storage. If required, you can change this default behaviour to only persist authentication state for the current session, or not at all. To configure these settings, call the setPersistence() method (note; on native platforms an UnimplementedError will be thrown):
// Disable persistence on web platforms
await FirebaseAuth.instance.setPersistence(Persistence.NONE);
for more info:
for more info:

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.

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();
}
}
}

IAP items not showing using the Xamarin IAP nuget package

I have two IAPs set up on for my iOS app on iTunes Connect. All of the information is in there and they marry up to what I have set for Android. On iTunes, they are set as non-renewing subscriptions (mainly as this is what the review said they should be). There are no errors shown on the IAP screen and have been submitted for review (this makes no difference, the ones I had there previously didn't show either)
I'm using the Xamarin In App Purchase nuget package to get this working. When I run the app on Android, the packages show correctly. When I run the same code on iOS, nothing is showing.
I have a user set on the sandbox and have followed the instructions on the iTunes website on logging out of the store, but I'm seeing nothing - it's not even asking me to log into the store.
My code for interrogating the store is this
async Task<List<InAppBillingProduct>> GetItems()
{
var billing = CrossInAppBilling.Current;
try
{
var productIds = new string[] { "monthly_renewals", "yearly_subscriptions" };
//You must connect
var connected = await billing.ConnectAsync();
if (!connected)
{
//Couldn't connect
return new List<InAppBillingProduct>();
}
//check purchases
var items = await billing.GetProductInfoAsync(ItemType.Subscription, productIds);
return items.ToList();
}
catch (InAppBillingPurchaseException)
{
return new List<InAppBillingProduct>();
}
catch (Exception)
{
return new List<InAppBillingProduct>();
}
finally
{
await billing.DisconnectAsync();
}
}
It makes no difference if I set the ItemType to Subscription or InAppPurchase. All of the licences on iTunesConnect are correct, IAP is set to work in the provisioning profile too.
Given the store isn't asking for a login for the sandbox user, I'm wondering if something else needs to be done - I just don't know what.

SharedPreferences empty after restarting service / device

As I am working with Google Firebase for Push Notifications, I want to save the Instance Token to the SharedPreferences. Unfortunately, whenever the token gets refreshed and want to check the previous one from SharedPreferences, they are empty...
Is it because I am using a Service here?
public class MyFirebaseIIDService : FirebaseInstanceIdService
{
public override void OnTokenRefresh()
{
var sharedPreferences = PreferenceManager.GetDefaultSharedPreferences(this);
var sharedPreferencesEditor = sharedPreferences.Edit();
// Get Firebase Instance Token
var refreshedToken = FirebaseInstanceId.Instance.Token;
// Check if a Firebase Instance Token has been registered before and unregister it
var oldToken = sharedPreferences.GetString("FirebaseInstanceToken", null);
if (oldToken != null) // is ALWAYS null :(
{
// Unregister old token...
}
// Save the Firebase Instance Token locally
sharedPreferencesEditor.PutString("FirebaseInstanceToken", refreshedToken);
sharedPreferencesEditor.Apply();
// At this point, the SharedPreferences have to token saved.
// Next time, the app reaches this point, it is gone...
}
}
Sorry for the syntax confusion, I use Xamarin, so this is C# but it should not make any difference.
I don't know how it work in xamarin but in native android getSharedPreferences from service context maybe wrong. You should use only applicationContext or MODE_MULTI_PROCESS when open shared preferences.
You can see similar question here.
Xamarin and C# also have this mode when your open some file, so i think exactly the same with preferences. Try some like this instead of using GetDefaultSharedPreferences:
ISharedPreferences prefs = Application.Context.GetSharedPreferences ("PREF_NAME", FileCreationMode.MultiProcess);
Did you uninstall your app to refresh you token? If yes I think you can not get the old token because the SharedPreferences is cleared when you uninstall your app.
I have tried java code to save the token :
public void onTokenRefresh() {
// Get updated InstanceID token.
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
Log.d("Mike", "Refreshed token: " + refreshedToken);
SharedPreferences mSharedPreferences = getSharedPreferences("hello",0);
String oldToken = mSharedPreferences.getString("Token",null);
if(oldToken == null)
{
Log.d("Mike", "oldToken: " + null);
}
SharedPreferences.Editor mEditor = mSharedPreferences.edit();
mEditor.putString("Token", refreshedToken);
mEditor.commit();
// TODO: Implement this method to send any registration to your app's servers.
//sendRegistrationToServer(refreshedToken);
}
At first time you install your app you will get the token and save it in the SharedPreferences, next time you open your app and show the old token in the textview then you can find your token has been saved , And do not uninstall your app:
TextView tv1 = (TextView)findViewById(R.id.tv1);
SharedPreferences mSharedPreferences = getSharedPreferences("hello",0);
String oldToken = mSharedPreferences.getString("Token",null);
tv1.setText(oldToken);
It works. But when you uninstall your app the textview shows null.
I solved the problem and it turned out, that my code was working correctly but behaved strange on my test device.
After re-building the application, SharedPreferences have been cleared, although I checked the Preserve application data/cache on device between deploys option in Visual Studio. That was because my physical testing device was rooted and did not accept this option.
When trying on an unrooted device, everything worked as expected.

Resources