Once the user submits the login info, and presses login button the following method is called;
public Page OnLogInButtonClicked (string email, string password)
{
var client = new RestClient("http://babyAPI.com");
var request =
new RestRequest("api/ApiKey?email=" + email + "&password=" + password, Method.GET);
var queryResult = client.Execute(request);
if (queryResult.StatusCode == HttpStatusCode.OK)
{
var deserial = new JsonDeserializer();
var x = deserial.Deserialize<ApiKey>(queryResult);
return;
}
else
{
return;
}
}
Is this the correct way to do it? And if the user is authenticated, I need to navigate to a new page, else show authentication failed. How can that be done?
Your question is quite broad. What are you looking to solve?
Here's what I think can be improved in this code. Assuming this is a ViewModel class - if it isn't read up on MVVM in Xamarin. I'd suggest also reading up on Separation of Concerns (also see the links on the bottom about DRY and Single Responsibility)
// make the method async so your UI doesn't lock up
public async Task AuthenticateAndNavigate(string email, string pass){
// your MVVM framework may have IsBusy property, otherwise - define it
// it should be bindable so you can use it to bind activity indicators' IsVisible and buttons' IsEnabled
IsBusy = true;
try{
// split out the code that talks to the server in a separate class - don't mix UI, ViewModel and server interactivity (separation of concerns principle)
// the assumption here is that a null is returned if auth fails
var apiKey = BabyApi.GetApiKey(email, pass);
// the Navigation property below exists in some MVVM frameworks, otherwise it comes from a Page instance that calls this code
if (apiKey!=null)
await Navigation.PushModalAsync(new HomePage());
else
await Navigation.PushAsync(new FailedToAuthenticatePage()); // or just do nothing
} catch {
await Navigation.PushAsync(new FailedToAuthenticatePage { Error: x.Message });
} finally {
IsBusy = false;
}
}
Related
I am implementing an application in Xamarin with a login functionality. I am using Xamarin SecureStorage to store my username and password in Keychain. I am using Prism to segue between my pages.
I have an AuthenticationService that handles all saving and storing keychain data with SecureStorage. In my App.Xaml, I have a PubSubEvent below that handles when the user is authenticated or not.
var result = await NavigationService.NavigateAsync("/MainPage?title=Welcome");
if (!result.Success)
{
Console.WriteLine("Error {0}", result.Exception);
logger.Report(result.Exception, new Dictionary<string, string> { { "navigationPath", "/MainPage" } });
//MainPage = result.CreateErrorPage();
System.Diagnostics.Debugger.Break();
}
In my LoginPageViewModel I have the following binding based on my login button.
private async Task OnLoginUserCommandExecutedAsync()
{
try
{
MyPrivateSession response = new MyPrivateSession();
//Username/Password text is valid
response = await _authenticationService.LoginAsync(UserLogin);
if (!response.IsValidUser)
{
await PageDialogService.DisplayAlertAsync("", "Login Unsuccessful - Invalid User Id or Password", "Ok");
}
}
catch (AuthenticationException authEx)
{
await PageDialogService.DisplayAlertAsync("Error", authEx.Message, "Ok");
}
catch (Exception e)
{
Debugger.Break();
Logger.Log(e.Message, Category.Exception, Priority.Medium);
await PageDialogService.DisplayAlertAsync("Fatal Error", e.Message, "Ok");
}
}
Also in my LoginPageViewModel, I have the following code in onNavigatedTo to get saved login from SecureStorage
public override async void OnNavigatedTo(INavigationParameters parameters)
{
// Determine if there is a preferred account to use in cases where the user is logged out and sent directly to the login
// in this case the AssumedLogin isn't defined.
if (_authenticationService.AssumedLogin is null)
{
await _authenticationService.AssumeUserPriorToLogin();
if (!(_authenticationService.AssumedLogin is null))
{
UserLogin.Username = _authenticationService.AssumedLogin.Username;
UserLogin.Password = _authenticationService.AssumedLogin.Properties["password"];
//Segues to VIN Utilities
await OnLoginUserCommandExecutedAsync();
}
}
}
In my AuthenticationService, I have this at the end of my logic after accessing username and password from SecureStorage
var user = GetCurrentUser();
_container.UseInstance(user);
//Publish User Authenticated Event
_eventAggregator.GetEvent<UserAuthenticatedEvent>().Publish(user);
My problem is when a user manually logs in with the login button, everything works perfectly. However, when I'm adding logic on the LoginPageViewModel's OnNavigatedTo to automatically authenticate user when the user have a saved username and password. It kicks me out of MainPage and back to login page.
One my doubts is that, when I do it automatic login through navigation page, the application is not finished loading yet. The application would log OnNavigatedTo from MainPage first before "Loaded My App" in the AppDelegate. (see below) How can I ensure the iOS initializers finish first before I start segueing to the main page?
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init();
Rg.Plugins.Popup.Popup.Init();
SlideOverKit.iOS.SlideOverKit.Init();
SfCheckBoxRenderer.Init();
Syncfusion.SfCarousel.XForms.iOS.SfCarouselRenderer.Init();
Console.WriteLine("Loading My App");
LoadApplication(new App(new IOSInitializer()));
Console.WriteLine("Loaded My App");
return base.FinishedLaunching(app, options);
}
public class IOSInitializer : IPlatformInitializer
{
public void RegisterTypes(IContainerRegistry containerRegistry)
{
}
}
private void OnUserAuthenticated(IUser user)
{
var result = await NavigationService.NavigateAsync("/MainPage");
if (!result.Success)
{
Console.WriteLine("Error {0}", result.Exception);
//MainPage = result.CreateErrorPage();
System.Diagnostics.Debugger.Break();
}
}
i have a log in page where user enters username and password and then the user is redirected to his account page, but when i press the back button it takes me back to the log in page. How do i prevent that from happening.
Code Snippet:
public Login ()
{
if (App.IsUserLoggedIn == false)
{
InitializeComponent();
}
else
{
Navigation.PushAsync(new NavigationPage(new LoginIndexPage()));
}
}
private async void LoginButton_Clicked(object sender, EventArgs e)
{
var user = new Xcore.Users.csUser
{
RefNo = referansnoEntry.Text,
Username = usernameEntry.Text,
Password = passwordEntry.Text
};
var isValid = IsCorrectLogin(user);
if (isValid)
{
App.IsUserLoggedIn = true;
await Navigation.PushAsync(new NavigationPage(new LoginIndexPage()));
}
else
{
showError.Text = "*Hatalı giriş";
passwordEntry.Text = string.Empty;
usernameEntry.Text = string.Empty;
referansnoEntry.Text = string.Empty;
}
}
private bool IsCorrectLogin(Xcore.Users.csUser user)
{
return user.RefNo == Xcore.Users.RegInfo.RefNo
&& user.Username == Xcore.Users.RegInfo.Username
&& user.Password == Xcore.Users.RegInfo.Password;
}
public LoginIndexPage()
{
if (App.IsUserLoggedIn == true)
{
InitializeComponent();
}
else
{
Navigation.PushAsync(new NavigationPage(new Login()));
}
}
protected override void OnAppearing()
{
base.OnAppearing();
showuserName.Text = Xcore.Users.RegInfo.Username;
}
When i login, its going on LoginIndexPage.
Similar to what App Pack mentioned, you should not have LoginPage in the navigation stack. Instead of trying to remove it, just set the next page to the root page.
Instead of pushing the next page onto the stack, set it :
Application.Current.MainPage = <your next page>
Also, I wouldn't navigate to a page in the constructor of a page. That's not really a good practice.
Usually in this situation I would remove the login page from the Navigation Stack.
I believe there is a method on Navigation such as Navigation.RemovePage(Page page) as you are navigating away from the login page. That way your back-stack and functionality stays in place but will not go back to the login page as it has been removed from the stack.
What you must remember however, is that if the login was the root page, there does need to still be a page in the stack when you call pop().
try use this code in your LoginIndexPage.
protected override bool OnBackButtonPressed()
{
return true; // true prevent navigation back and false to allow
}
When a user authenticates correctly, it will be directed to the HomeViewModel. I want to remove the possibility that it can return to the login screen so I have created a Custom Presenter to remove all the screens that are below the new screen.
The implementation is as follows:
public class CustomPresenter: MvxFormsIosPagePresenter
{
public CustomPresenter(UIWindow window, MvxFormsApplication mvxFormsApp)
: base(window, mvxFormsApp)
{
}
public override void Show(MvxViewModelRequest request)
{
if (request.PresentationValues?["NavigationCommand"] == "StackClear")
{
var navigation = FormsApplication.MainPage.Navigation;
Debug.WriteLine("Navigation Back Stack Count -> " + navigation.NavigationStack.Count());
navigation.PopToRootAsync();
Debug.WriteLine("Navigation Back Stack Count After PopToRootAsync -> " + navigation.NavigationStack.Count());
return;
}
base.Show(request);
}
}
When the authentication process finishes correctly, I navigate to the home screen by passing a bundle with this special command:
LoginWithFacebookCommand.Subscribe(token => {
Debug.WriteLine("JWT Token -> " + token);
_userDialogs.ShowSuccess(AppResources.Login_Success);
var mvxBundle = new MvxBundle(new Dictionary<string, string> { { "NavigationCommand", "StackClear" } });
ShowViewModel<HomeViewModel>(presentationBundle: mvxBundle);
});
The problem is that it does not change the screen, it stays in the current one. What would be the way to do it correctly ?.
I am using MvvmCross 5.1.1 and MvvmCross.Forms 5.1.1
Thank you very much in advance.
As I understand it, PopToRootAsync() pops everything off the stack to the root. Which means you should then push your view that you wish to navigate to, onto your stack after that method is called i.e. use PushViewController(yourViewController) afterwards. Also, you should be using the new IMvxNavigationService by MvvmCross. You can give this a try:
var navigationService = Mvx.Resolve<IMvxNavigationService>();
LoginWithFacebookCommand.Subscribe(async (token) => {
Debug.WriteLine("JWT Token -> " + token);
_userDialogs.ShowSuccess(AppResources.Login_Success);
await navigationService.Navigate<HomeViewModel>();
});
To clear the backstack you basically need to override the Show method in the presenter and check whether your viewmodel is being called. If it is then set a new array of viewControllers. (Credit to #pnavk!!)
public class CustomPresenter : MvxIosViewPresenter
{
public override void Show(IMvxIosView view, MvxViewModelRequest request)
{
if (MasterNavigationController != null && view.ViewModel.GetType() == typeof(HomeViewModel))
{
var viewController = view as UIViewController;
MasterNavigationController.SetViewControllers(new UIViewController[] { viewController }, true);
}
else
base.Show(view, request);
}
}
Try this:
navigation.SetViewControllers(new UIViewController[] { vc }, true);
vc is the ViewController you want to set as the root of the navigation stack. You will need to get a reference to it which you can using the ViewControllers property on the NavigationController.
true - means you want to animate.
On the App.xaml.cs I have the following code
private async void OnCommandsRequested(SettingsPane settingsPane, SettingsPaneCommandsRequestedEventArgs e)
{
var loader = ResourceLoader.GetForCurrentView();
var generalCommand = new SettingsCommand("General Settings", "General Settings", handler =>
{
var generalSettings = new GeneralSettingsFlyout();
generalSettings.Show();
});
e.Request.ApplicationCommands.Add(generalCommand);
object data;
IAuthService _authService = new AuthService();
if (Global.UserId == 0)
data = await _authService.GetSettingValueBySettingName(DatabaseType.GeneralDb, ApplicationConstants.GeneralDbSettingNames.ShowSupportInfo);
else
data = await _authService.GetSettingValueBySettingName(DatabaseType.UserDb, ApplicationConstants.UserDbSettingNames.ShowSupportInfo);
if (data != null && data.ToString().Equals("1"))
{
var supportCommand = new SettingsCommand("Support", "Support", handler =>
{
var supportPane = new SupportFlyout();
supportPane.Show();
});
e.Request.ApplicationCommands.Add(supportCommand);
}
var aboutCommand = new SettingsCommand("About", loader.GetString("Settings_OptionLabels_About"), handler =>
{
var aboutPane = new About();
aboutPane.Show();
});
e.Request.ApplicationCommands.Add(aboutCommand);
}
This code adds the setting "General Settings" but neither "Support" or "About" commands. Can anyone advice what's wrong with this code?
Instead of querying the commands from your service when they are requested you'll need to query them ahead of time and then add the already known commands.
You cannot use await in OnCommandsRequested.
A method returns when it gets to the first await, so only commands added to the request before the await will be used.
Since the SettingsPaneCommandsRequestedEventArgs doesn't provide a deferral there is no way to tell the requester to wait for internal async calls to complete.
Note also that SettingsPane is deprecated and not recommended for new app development for Windows 10.
Anyone explain replyHandler and InvokeOnMainThread works in this code enter code here. I have copied this code form a sample project I need to implement this thing in my project
partial void UIButton7_TouchUpInside (UIButton sender)
{
var context = new LAContext ();
var error = new NSError ();
if (context.CanEvaluatePolicy (LAPolicy.DeviceOwnerAuthenticationWithBiometrics,out error)) {
var replyHandler = new LAContextReplyHandler((success, err) => {
this.InvokeOnMainThread(() => {
if(success){
Console.WriteLine("You Logged in");
} else {
var errorAlertView = new UIAlertView("Login Error", err.LocalizedDescription, null, "Close");
errorAlertView.Show();
}
});
});
context.EvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, "You need to login", replyHandler);
}
}
The reply handler is basically a callback to manage feedback when get the result from the touch id.
InvokeOnMainThread is to allow show an ui change when gets this result back. It forces to be on the ui thread to be able to reflect an ui change.