2023-04-19 12:14:14 +02:00
using System ;
2022-08-30 15:42:25 +02:00
using System.Collections.Generic ;
2021-05-13 20:03:07 +02:00
using System.ComponentModel ;
2022-08-30 15:42:25 +02:00
using System.Linq ;
2021-05-13 20:03:07 +02:00
using System.Threading.Tasks ;
2022-08-30 15:42:25 +02:00
using System.Windows.Input ;
2021-05-13 20:03:07 +02:00
using Plugin.Connectivity ;
2022-08-30 15:42:25 +02:00
using Serilog ;
2021-05-13 20:03:07 +02:00
using TINK.Model ;
2022-08-30 15:42:25 +02:00
using TINK.Model.User ;
using TINK.Model.User.Account ;
2021-05-13 20:03:07 +02:00
using TINK.MultilingualResources ;
2022-08-30 15:42:25 +02:00
using TINK.Repository.Exception ;
using TINK.View ;
using TINK.ViewModel.Map ;
using Xamarin.Forms ;
2021-05-13 20:03:07 +02:00
namespace TINK.ViewModel
{
2022-09-06 16:08:19 +02:00
public class LoginPageViewModel : INotifyPropertyChanged
{
/// <summary>
/// Reference on view service to show modal notifications and to perform navigation.
/// </summary>
private readonly IViewService m_oViewService ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Reference on the tink app instance. </summary>
private ITinkApp TinkApp { get ; }
/// <summary>
/// Holds the mail address candidate being entered by user.
/// </summary>
private string m_strMailAddress ;
/// <summary>
/// Holds the password candidate being entered by user.
/// </summary>
private string m_strPassword ;
private bool m_bMailAndPasswordCandidatesOk = false ;
/// <summary> Holds a reference to the external trigger service. </summary>
private Action < string > OpenUrlInExternalBrowser { get ; }
/// <summary>
/// Event which notifies clients about changed properties.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged ;
2023-07-19 10:10:36 +02:00
/// <summary> Used to block more than on copri requests at a given time.</summary>
private bool isIdle = true ;
/// <summary>
/// True if any action can be performed (login etc.)
/// </summary>
public virtual bool IsIdle
{
get = > isIdle ;
set
{
if ( value = = isIdle )
return ;
Log . ForContext < LoginPageViewModel > ( ) . Debug ( $"Switch value of {nameof(IsIdle)} to {value}." ) ;
isIdle = value ;
PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( nameof ( IsIdle ) ) ) ;
PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( nameof ( IsProcessWithRunningProcessView ) ) ) ;
}
}
public bool IsProcessWithRunningProcessView = > ! isIdle ;
/// <summary> Holds info about current action. </summary>
private string statusInfoText ;
/// <summary> Holds info about current action. </summary>
public string StatusInfoText
{
get = > statusInfoText ;
set
{
if ( value = = statusInfoText )
return ;
Log . ForContext < LoginPageViewModel > ( ) . Debug ( $"Switch value of {nameof(StatusInfoText)} to {value}." ) ;
statusInfoText = value ;
PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( nameof ( StatusInfoText ) ) ) ;
}
}
2022-09-06 16:08:19 +02:00
/// <summary>
///
/// </summary>
/// <param name="p_oSetCredentials">Delegate to set new mail address and password to model.</param>
/// <param name="p_oUser">Mail address of user if login succeeded.</param>
/// <param name="openUrlInExternalBrowser">Action to open an external browser.</param>
2023-04-19 12:14:14 +02:00
/// <param name="p_oViewService">Interface to actuate methods on GUI.</param>
2022-09-06 16:08:19 +02:00
/// <param name="p_oNavigation">Interface to navigate.</param>
public LoginPageViewModel (
ITinkApp tinkApp ,
Action < string > openUrlInExternalBrowser ,
IViewService p_oViewService )
{
TinkApp = tinkApp
? ? throw new ArgumentException ( "Can not instantiate map page view model- object. No tink app object available." ) ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
OpenUrlInExternalBrowser = openUrlInExternalBrowser
? ? throw new ArgumentException ( "Can not instantiate login page view model- object. No user external browse service available." ) ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
m_oViewService = p_oViewService
? ? throw new ArgumentException ( "Can not instantiate login page view model- object. No view available." ) ;
2021-05-13 20:03:07 +02:00
2022-09-06 16:08:19 +02:00
m_strMailAddress = tinkApp . ActiveUser . Mail ;
m_strPassword = tinkApp . ActiveUser . Password ;
IsRegisterTargetsInfoVisible = new List < string > { Model . Services . CopriApi . ServerUris . CopriServerUriList . TINK_LIVE , Model . Services . CopriApi . ServerUris . CopriServerUriList . TINK_DEVEL } . Contains ( tinkApp . Uris . ActiveUri . AbsoluteUri ) ;
tinkApp . ActiveUser . StateChanged + = OnStateChanged ;
}
/// <summary>
/// Login state changed.
/// </summary>
/// <param name="p_oSender"></param>
/// <param name="p_oEventArgs"></param>
private void OnStateChanged ( object p_oSender , EventArgs p_oEventArgs )
{
var l_oPropertyChanged = PropertyChanged ;
if ( l_oPropertyChanged ! = null )
{
l_oPropertyChanged ( this , new PropertyChangedEventArgs ( nameof ( IsLoggedOut ) ) ) ;
l_oPropertyChanged ( this , new PropertyChangedEventArgs ( nameof ( IsLoginRequestAllowed ) ) ) ;
2023-09-28 15:37:44 +02:00
l_oPropertyChanged ( this , new PropertyChangedEventArgs ( nameof ( MailAddress ) ) ) ;
l_oPropertyChanged ( this , new PropertyChangedEventArgs ( nameof ( Password ) ) ) ;
2022-09-06 16:08:19 +02:00
}
}
/// <summary> Gets a value indicating whether user is logged out or not.</summary>
public bool IsLoggedOut { get { return ! TinkApp . ActiveUser . IsLoggedIn ; } }
/// <summary> Gets a value indicating whether user can try to login.</summary>
public bool IsLoginRequestAllowed
{
get
{
return ! TinkApp . ActiveUser . IsLoggedIn
& & m_bMailAndPasswordCandidatesOk ;
}
}
/// <summary> Gets mail address of user.</summary>
public string MailAddress
{
get
{
return m_strMailAddress ;
}
set
{
m_strMailAddress = value ;
UpdateAndFireIfRequiredOnEnteringMailOrPwd ( ) ;
}
}
/// <summary> Gets password user.</summary>
public string Password
{
get
{
return m_strPassword ;
}
set
{
m_strPassword = value ;
UpdateAndFireIfRequiredOnEnteringMailOrPwd ( ) ;
}
}
/// <summary>Update on password or mailaddress set. </summary>
private void UpdateAndFireIfRequiredOnEnteringMailOrPwd ( )
{
var l_bLastMailAndPasswordCandidatesOk = m_bMailAndPasswordCandidatesOk ;
m_bMailAndPasswordCandidatesOk = Validator . ValidateMailAndPasswordDelegate ( MailAddress , Password ) . ValidElement = = Elements . Account ;
if ( m_bMailAndPasswordCandidatesOk ! = l_bLastMailAndPasswordCandidatesOk )
{
PropertyChanged ? . Invoke ( this , new PropertyChangedEventArgs ( nameof ( IsLoginRequestAllowed ) ) ) ;
}
2023-07-19 10:10:36 +02:00
}
2022-09-06 16:08:19 +02:00
/// <summary>
/// Command object to bind login button to view model.
/// </summary>
public ICommand OnLoginRequest
{
get
{
return new Command ( async ( ) = > await Login ( ) ) ;
}
}
/// <summary> Processes request to register a new user. </summary>
public ICommand OnRegisterRequest = > new Command ( async ( ) = >
{
if ( CrossConnectivity . Current . IsConnected )
{
await m_oViewService . PushAsync ( ViewTypes . RegisterPage ) ;
}
else
{
await m_oViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . MessageHintTitle ,
AppResources . ErrorNoWeb ,
2022-09-06 16:08:19 +02:00
AppResources . MessageAnswerOk ) ;
}
} ) ;
/// <summary> Processes request to recover password. </summary>
public ICommand OnPasswordForgottonRequest = > new Command ( async ( ) = >
{
if ( CrossConnectivity . Current . IsConnected )
{
await m_oViewService . PushAsync ( ViewTypes . PasswordForgottenPage ) ;
}
else
{
await m_oViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . MessageHintTitle ,
AppResources . ErrorNoWeb ,
2022-09-06 16:08:19 +02:00
AppResources . MessageAnswerOk ) ;
}
} ) ;
/// <summary>
/// User request to log in.
/// </summary>
public async Task Login ( )
{
2023-09-22 11:38:42 +02:00
if ( ! IsLoginRequestAllowed )
2022-09-06 16:08:19 +02:00
{
2023-09-22 11:38:42 +02:00
if ( TinkApp . ActiveUser . IsLoggedIn )
2022-09-06 16:08:19 +02:00
{
await m_oViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . ErrorLoginTitle ,
2023-09-22 11:38:42 +02:00
string . Format ( AppResources . ErrorLoginAlreadyLoggedIn , TinkApp . ActiveUser . Mail ) ,
2022-09-06 16:08:19 +02:00
AppResources . MessageAnswerOk ) ;
}
2023-09-22 11:38:42 +02:00
else if ( ! m_bMailAndPasswordCandidatesOk )
{
2022-09-06 16:08:19 +02:00
await m_oViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . ErrorLoginTitle ,
2023-09-22 11:38:42 +02:00
AppResources . ErrorLoginInvalidMailOrPasswortInput ,
2022-09-06 16:08:19 +02:00
AppResources . MessageAnswerOk ) ;
}
2023-09-22 11:38:42 +02:00
return ;
}
else
{
IsIdle = false ;
StatusInfoText = AppResources . ActivityTextOneMomentPlease ;
IAccount account = new EmptyAccount ( ) ;
try
2022-09-06 16:08:19 +02:00
{
2023-09-22 11:38:42 +02:00
Log . ForContext < LoginPageViewModel > ( ) . Information ( "User taped login button." ) ;
try
2022-09-06 16:08:19 +02:00
{
2023-09-22 11:38:42 +02:00
TinkApp . ActiveUser . CheckIsPasswordValid ( MailAddress , Password ) ;
2022-09-06 16:08:19 +02:00
2023-09-22 11:38:42 +02:00
// Do login.
account = await TinkApp . GetConnector ( CrossConnectivity . Current . IsConnected ) . Command . DoLogin ( MailAddress , Password , TinkApp . ActiveUser . DeviceId ) ;
await TinkApp . ActiveUser . Login ( account ) ;
// Update map page filter because user might be of both groups TINK an Konrad.
TinkApp . GroupFilterMapPage =
GroupFilterMapPageHelper . CreateUpdated (
TinkApp . GroupFilterMapPage ,
TinkApp . ActiveUser . DoFilter ( TinkApp . FilterGroupSetting . DoFilter ( ) ) ) ;
// Update settings page filter because user might be of both groups TINK an Konrad.
TinkApp . FilterGroupSetting . DoFilter ( TinkApp . ActiveUser . Group ) ;
// Persist new settings.
TinkApp . Save ( ) ;
TinkApp . UpdateConnector ( ) ;
2022-09-06 16:08:19 +02:00
}
2023-09-22 11:38:42 +02:00
catch ( InvalidAuthorizationResponseException l_oException )
2022-09-06 16:08:19 +02:00
{
2023-09-22 11:38:42 +02:00
// Copri response is invalid.
Log . ForContext < LoginPageViewModel > ( ) . Error ( "Login failed (invalid. auth. response). {@l_oException}." , l_oException ) ;
2022-09-06 16:08:19 +02:00
await m_oViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . ErrorLoginTitle ,
2023-09-22 11:38:42 +02:00
AppResources . ErrorLoginInvalidAuthorization ,
AppResources . MessageAnswerOk ) ;
IsIdle = true ;
StatusInfoText = string . Empty ;
return ;
2022-09-06 16:08:19 +02:00
}
2023-09-22 11:38:42 +02:00
catch ( UnsupportedCopriVersionDetectedException )
2022-09-06 16:08:19 +02:00
{
await m_oViewService . DisplayAlert (
2023-08-31 12:20:06 +02:00
AppResources . ErrorLoginTitle ,
2023-09-22 11:38:42 +02:00
string . Format (
AppResources . MessageAppVersionIsOutdated ,
TinkApp . Flavor . GetDisplayName ( ) ) ,
2022-09-06 16:08:19 +02:00
AppResources . MessageAnswerOk ) ;
2023-09-22 11:38:42 +02:00
IsIdle = true ;
StatusInfoText = string . Empty ;
return ;
2022-09-06 16:08:19 +02:00
}
2023-09-22 11:38:42 +02:00
catch ( Exception l_oException )
{
// Copri server is not reachable.
if ( l_oException is WebConnectFailureException )
{
Log . ForContext < LoginPageViewModel > ( ) . Information ( "Login failed (web communication exception). {@l_oException}." , l_oException ) ;
await m_oViewService . DisplayAlert (
AppResources . ErrorNoConnectionTitle ,
AppResources . ErrorNoWeb ,
AppResources . MessageAnswerOk ) ;
}
else if ( l_oException is UsernamePasswordInvalidException )
{
// Cookie is empty.
Log . ForContext < LoginPageViewModel > ( ) . Error ( "Login failed (empty cookie). {@l_oException}." , l_oException ) ;
await m_oViewService . DisplayAlert (
AppResources . ErrorLoginTitle ,
string . Format ( AppResources . ErrorLoginNoCookie , l_oException . Message ) ,
AppResources . MessageAnswerOk ) ;
}
else
{
Log . ForContext < LoginPageViewModel > ( ) . Error ( "Login failed. {@l_oException}." , l_oException ) ;
await m_oViewService . DisplayAlert (
AppResources . ErrorLoginTitle ,
l_oException . Message ,
AppResources . MessageAnswerOk ) ;
}
IsIdle = true ;
StatusInfoText = string . Empty ;
return ;
}
// Display information that login succeeded.
Log . ForContext < LoginPageViewModel > ( ) . Information ( "Login succeeded. {@tinkApp.ActiveUser}." , TinkApp . ActiveUser ) ;
var title = TinkApp . ActiveUser . Group . Intersect ( new List < string > { Model . Connector . FilterHelper . CARGOBIKE , Model . Connector . FilterHelper . CITYBIKE } ) . Any ( )
? string . Format ( AppResources . MessageLoginWelcomeTitleGroup , TinkApp . ActiveUser . GetUserGroupDisplayName ( ) )
: string . Format ( AppResources . MessageLoginWelcomeTitle ) ;
await m_oViewService . DisplayAlert (
title ,
string . Format ( AppResources . MessageLoginWelcome , TinkApp . ActiveUser . Mail ) ,
AppResources . MessageAnswerOk ) ;
2023-09-28 15:37:44 +02:00
//clear MailAdress and Password user input
MailAddress = string . Empty ;
Password = string . Empty ;
2023-09-22 11:38:42 +02:00
}
catch ( Exception p_oException )
{
Log . ForContext < LoginPageViewModel > ( ) . Error ( "An unexpected error occurred displaying log out page. {@Exception}" , p_oException ) ;
2022-09-06 16:08:19 +02:00
2023-07-19 10:10:36 +02:00
IsIdle = true ;
StatusInfoText = string . Empty ;
2022-09-06 16:08:19 +02:00
return ;
}
2023-09-22 11:38:42 +02:00
try
2022-09-06 16:08:19 +02:00
{
2023-09-22 11:38:42 +02:00
if ( ! TinkApp . ActiveUser . Group . Contains ( Model . Connector . FilterHelper . CARGOBIKE ) )
{
// No need to show "Anleitung TINK Räder" because user can not use tink.
await m_oViewService . ShowPage ( "//MapPage" ) ;
IsIdle = true ;
StatusInfoText = string . Empty ;
return ;
}
2021-05-13 20:03:07 +02:00
2023-09-22 11:38:42 +02:00
// Switch to map page
await m_oViewService . ShowPage ( "//MapPage" ) ;
}
catch ( Exception p_oException )
{
Log . ForContext < LoginPageViewModel > ( ) . Error ( "Ein unerwarteter Fehler ist auf der Seite Anleitung TINK Räder (nach Anmeldeseite) aufgetreten. {@Exception}" , p_oException ) ;
2023-07-19 10:10:36 +02:00
2023-09-22 11:38:42 +02:00
IsIdle = true ;
StatusInfoText = string . Empty ;
return ;
}
2021-05-13 20:03:07 +02:00
2023-09-22 11:38:42 +02:00
IsIdle = true ;
StatusInfoText = string . Empty ;
}
2022-09-06 16:08:19 +02:00
}
/// <summary> Holds whether TINK/ Copri info is shown.</summary>
public bool IsRegisterTargetsInfoVisible { get ; private set ; }
/// <summary> Text providing info about TINK/ konrad registration. </summary>
public FormattedString RegisterTargetsInfo
{
get
{
var l_oHint = new FormattedString ( ) ;
l_oHint . Spans . Add ( new Span { Text = AppResources . MarkingLoginInstructionsTinkKonradTitle } ) ;
l_oHint . Spans . Add ( new Span { Text = AppResources . MarkingLoginInstructionsTinkKonradMessage } ) ;
return l_oHint ;
}
}
/// <summary> Opens login page.</summary>
public void RegisterRequest ( string url )
{
try
{
OpenUrlInExternalBrowser ( url ) ;
}
catch ( Exception p_oException )
{
Log . Error ( "Ein unerwarteter Fehler ist auf der Login Seite beim Öffnen eines Browsers, Seite {url}, aufgetreten. {@Exception}" , url , p_oException ) ;
return ;
}
}
}
2021-05-13 20:03:07 +02:00
}