1using System.ComponentModel;
2using System.Security.Cryptography;
5using CommunityToolkit.Mvvm.ComponentModel;
6using CommunityToolkit.Mvvm.Input;
15using System.Globalization;
29 this.SupportEmail =
"neuro-access@trustanchorgroup.com";
33 private string supportEmail;
36 private async Task ContactSupport()
38 string email = this.SupportEmail;
41 string mailtoUri = $
"mailto:{email}?subject={Uri.EscapeDataString(subject)}";
45 if(!await Launcher.OpenAsync(
new Uri(mailtoUri)))
61 protected override async Task OnInitialize()
63 await base.OnInitialize();
65 LocalizationManager.Current.PropertyChanged += this.LocalizationManagerEventHandler;
69 this.CountDownTimer =
App.
Current.Dispatcher.CreateTimer();
70 this.CountDownTimer.Interval = TimeSpan.FromMilliseconds(1000);
71 this.CountDownTimer.Tick += this.CountDownEventHandler;
78 new KeyValuePair<string, string>(
"Accept",
"application/json"));
80 if ((Result is Dictionary<string, object> Response) &&
81 Response.TryGetValue(
"CountryCode", out
object? cc) &&
82 (cc is
string CountryCode))
85 this.SelectedCountry = Country;
94 public void LocalizationManagerEventHandler(
object? sender, PropertyChangedEventArgs e)
96 this.OnPropertyChanged(nameof(this.LocalizedSendCodeText));
99 protected override void OnPropertyChanged(PropertyChangedEventArgs e)
101 base.OnPropertyChanged(e);
103 if (e.PropertyName == nameof(this.IsBusy))
104 this.SendCommand.NotifyCanExecuteChanged();
109 [NotifyPropertyChangedFor(nameof(CanSend))]
110 [NotifyPropertyChangedFor(nameof(EmailValidationError))]
111 [NotifyCanExecuteChangedFor(nameof(this.SendCommand))]
112 private bool emailIsValid;
115 [NotifyPropertyChangedFor(nameof(CanSend))]
116 [NotifyPropertyChangedFor(nameof(PhoneValidationError))]
117 [NotifyCanExecuteChangedFor(nameof(this.SendCommand))]
118 private bool phoneIsValid;
121 [NotifyPropertyChangedFor(nameof(CanSend))]
122 [NotifyCanExecuteChangedFor(nameof(this.SendCommand))]
123 private string emailText =
string.Empty;
126 [NotifyPropertyChangedFor(nameof(CanSend))]
127 [NotifyCanExecuteChangedFor(nameof(this.SendCommand))]
128 private string phoneText =
string.Empty;
130 public string EmailValidationError => !this.EmailIsValid ?
ServiceRef.
Localizer[nameof(AppResources.EmailValidationFormat)] :
string.Empty;
132 public string PhoneValidationError => !this.PhoneIsValid ?
ServiceRef.
Localizer[nameof(AppResources.PhoneValidationDigits)] :
string.Empty;
135 [NotifyPropertyChangedFor(nameof(LocalizedSendCodeText))]
136 [NotifyCanExecuteChangedFor(nameof(SendCommand))]
137 [NotifyCanExecuteChangedFor(nameof(ResendCodeCommand))]
138 private int countDownSeconds;
141 private IDispatcherTimer? countDownTimer;
143 public bool CanSend => this.EmailIsValid && this.PhoneIsValid &&
145 (this.CountDownSeconds <= 0) &&
146 !
string.IsNullOrEmpty(this.EmailText) && !
string.IsNullOrEmpty(this.PhoneText);
153 private string? localizedPhoneValidationError;
154 public string LocalizedSendCodeText
158 if (this.CountDownSeconds > 0)
168 private async Task SelectPhoneCode()
171 await MopupService.Instance.PushAsync(Page);
175 if (Result is not
null)
176 this.SelectedCountry = Result;
180 public bool CanResendCode => this.CountDownSeconds <= 0;
182 [RelayCommand(CanExecute = nameof(this.CanSend))]
183 private async Task Send()
196 string fullPhoneNumber = $
"+{SelectedCountry.DialCode}{PhoneText}";
198 if (SelectedCountry.DialCode ==
"46")
199 fullPhoneNumber = $
"+{SelectedCountry.DialCode}{PhoneText.TrimStart('0')}";
204 new Dictionary<string, object>
206 {
"Nr", fullPhoneNumber },
207 {
"AppName", Constants.Application.Name },
208 {
"Language", CultureInfo.CurrentCulture.TwoLetterISOLanguageName }
209 },
new KeyValuePair<string, string>(
"Accept",
"application/json"));
212 bool phoneSent = phoneSendResult is Dictionary<string, object> phoneResponse &&
213 phoneResponse.TryGetValue(
"Status", out var phoneObj) &&
214 phoneObj is
bool phoneStatus && phoneStatus;
221 if (!await this.VerifyCodeAsync(fullPhoneNumber, isEmail:
false))
244 [RelayCommand(CanExecute = nameof(CanResendCode))]
245 private async Task ResendCode()
250 private async Task<bool> VerifyCodeAsync(
string identifier,
bool isEmail)
252 VerifyCodeNavigationArgs navigationArgs =
new(
this, identifier);
254 string? code = await navigationArgs.VarifyCode!.Task;
256 if (!
string.IsNullOrEmpty(code))
258 var parameters =
new Dictionary<string, object>
260 { isEmail ?
"EMail" :
"Nr", identifier },
261 {
"Code",
int.Parse(code, NumberStyles.None, CultureInfo.InvariantCulture) }
266 parameters,
new KeyValuePair<string, string>(
"Accept",
"application/json"));
268 bool verified = verifyResult is Dictionary<string, object> verifyResponse &&
269 verifyResponse.TryGetValue(
"Status", out var obj) &&
270 obj is
bool status && status;
279 private void StartTimer()
281 if (this.CountDownTimer is not
null)
283 this.CountDownSeconds = 300;
285 if (!this.CountDownTimer.IsRunning)
286 this.CountDownTimer.Start();
290 private void CountDownEventHandler(
object? sender, EventArgs e)
292 if (this.CountDownTimer is not
null)
294 if (this.CountDownSeconds > 0)
295 this.CountDownSeconds--;
297 this.CountDownTimer.Stop();
301 public bool CanScanQrCode => !this.IsBusy;
304 [RelayCommand(CanExecute = nameof(CanScanQrCode))]
305 private async Task ScanQrCode()
307 string? Url = await Services.UI.QR.QrCode.ScanQrCode(nameof(AppResources.QrPageTitleScanInvitation),
310 if (
string.IsNullOrEmpty(Url))
318 string[] Parts = Url.Split(
':');
320 if (Parts.Length != 5)
330 string Domain = Parts[1];
331 string Code = Parts[2];
332 string KeyStr = Parts[3];
333 string IVStr = Parts[4];
339 Uri =
new Uri(
"https://" + Domain +
"/Onboarding/GetInfo");
360 new KeyValuePair<string, string>(
"Accept",
"text/plain"));
364 EncryptedStr = (string)Decoded;
380 byte[] Key = Convert.FromBase64String(KeyStr);
381 byte[] IV = Convert.FromBase64String(IVStr);
382 byte[] Encrypted = Convert.FromBase64String(EncryptedStr);
384 using Aes Aes = Aes.Create();
387 Aes.Mode = CipherMode.CBC;
388 Aes.Padding = PaddingMode.PKCS7;
390 using ICryptoTransform Decryptor = Aes.CreateDecryptor(Key, IV);
391 byte[] Decrypted = Decryptor.TransformFinalBlock(Encrypted, 0, Encrypted.Length);
392 string Xml = Encoding.UTF8.GetString(Decrypted);
394 XmlDocument Doc =
new()
396 PreserveWhitespace =
true
402 throw new Exception(
"Invalid Invitation XML");
404 LinkedList<XmlElement> ToProcess =
new();
405 ToProcess.AddLast(Doc.DocumentElement);
407 bool AccountDone =
false;
408 XmlElement? LegalIdDefinition =
null;
411 while (ToProcess.First is not
null)
413 XmlElement E = ToProcess.First.Value;
414 ToProcess.RemoveFirst();
423 await SelectDomain(Domain, KeyStr, Secret);
434 string PasswordMethod =
XML.
Attribute(E,
"passwordMethod");
442 await SelectDomain(Domain,
string.Empty,
string.Empty);
444 if (!await this.ConnectToAccount(UserName, Password, PasswordMethod,
string.Empty, LegalIdDefinition, Pin ??
string.Empty))
447 throw new Exception(
"Invalid account.");
450 LegalIdDefinition =
null;
455 LegalIdDefinition = E;
463 foreach (XmlNode N
in E.ChildNodes)
465 if (N is XmlElement E2)
467 ToProcess.AddLast(E2);
473 throw new Exception(
"Invalid Invitation XML");
477 if (AccountDone && LegalIdDefinition is not
null)
482 else if (AccountDone)
505 private static async Task SelectDomain(
string Domain,
string Key,
string Secret)
507 bool DefaultConnectivity;
511 (
string HostName,
int PortNumber,
bool IsIpAddress) = await
ServiceRef.
NetworkService.LookupXmppHostnameAndPort(Domain);
517 DefaultConnectivity =
false;
524 private async Task<bool> ConnectToAccount(
string AccountName,
string Password,
string PasswordMethod,
string LegalIdentityJid, XmlElement? LegalIdDefinition,
string Pin)
530 DateTime now = DateTime.Now;
534 bool serviceDiscoverySucceeded;
542 serviceDiscoverySucceeded =
true;
547 bool DestroyContractsClient =
false;
553 DestroyContractsClient =
true;
558 if (LegalIdDefinition is not
null)
567 if ((
string.IsNullOrEmpty(LegalIdentityJid) ||
string.Compare(LegalIdentityJid, Identity.
Id, StringComparison.OrdinalIgnoreCase) == 0) &&
570 Identity.
From <= now &&
571 Identity.
To >= now &&
578 approvedIdentity = Identity;
582 createdIdentity ??= Identity;
600 LegalIdentity? selectedIdentity = approvedIdentity ?? createdIdentity;
604 if (selectedIdentity is not
null)
607 SelectedId = selectedIdentity.
Id;
612 SelectedId =
string.Empty;
615 if (!
string.IsNullOrEmpty(Pin))
617 ServiceRef.TagProfile.LocalPassword = Pin;
622 if (Identity.
Id == SelectedId)
627 switch (Identity.
State)
638 if (DestroyContractsClient)
649 (
bool succeeded,
string? errorMessage,
string[]? alternatives) = await
ServiceRef.
XmppService.TryConnectAndConnectToAccount(
652 typeof(
App).Assembly, OnConnected);
658 errorMessage ??
string.Empty,
The Application class, representing an instance of the Neuro-Access app.
static new? App Current
Gets the current application, type casted to App.
const string IdDomain
Neuro-Access domain.
const string Default
The default language code.
const string Onboarding
Onboarding URI Scheme (obinfo)
static ? string GetScheme(string Url)
Gets the predefined scheme from an IoT Code
A set of never changing property constants and helpful values.
Conversion between Country Names and ISO-3166-1 country codes.
static bool TryGetCountryByCode(string? CountryCode, [NotNullWhen(true)] out ISO_3166_Country? Country)
Tries to get the country, given its country code.
static ISO_3166_Country DefaultCountry
This collection built from Wikipedia entry on ISO3166-1 on 9th Feb 2016
Base class that references services in the app.
static ILogService LogService
Log service.
static INetworkService NetworkService
Network service.
static IUiService UiService
Service serializing and managing UI-related tasks.
static ITagProfile TagProfile
TAG Profile service.
static IStringLocalizer Localizer
Localization service
static IXmppService XmppService
The XMPP service for XMPP communication.
Static class managing encoding and decoding of internet content.
static Task< object > DecodeAsync(string ContentType, byte[] Data, Encoding Encoding, KeyValuePair< string, string >[] Fields, Uri BaseUri)
Decodes an object.
static Task< object > PostAsync(Uri Uri, object Data, params KeyValuePair< string, string >[] Headers)
Posts to a resource, using a Uniform Resource Identifier (or Locator).
Helps with common XML-related tasks.
static string Attribute(XmlElement E, string Name)
Gets the value of an XML attribute.
Adds support for legal identities, smart contracts and signatures to an XMPP client.
Task< bool > ImportKeys(string Xml)
Imports keys
Task< LegalIdentity > ObsoleteLegalIdentityAsync(string LegalIdentityId)
Obsoletes one of the legal identities of the account, given its ID.
Task< bool > HasPrivateKey(LegalIdentity Identity)
Checks if the private key of a legal identity is available. Private keys are required to be able to s...
const string NamespaceOnboarding
http://waher.se/schema/Onboarding/v1.xsd
Task< LegalIdentity[]> GetLegalIdentitiesAsync()
Gets legal identities registered with the account.
override void Dispose()
Disposes of the extension.
bool HasClientPublicKey
If the identity has a client public key
DateTime From
From what point in time the legal identity is valid.
DateTime To
To what point in time the legal identity is valid.
IdentityState State
Current state of identity
string Id
ID of the legal identity
bool ValidateClientSignature()
Validates the client signature of the legal identity
bool HasClientSignature
If the identity has a client signature
Manages an XMPP client connection. Implements XMPP, as defined in https://tools.ietf....
string PasswordHashMethod
Password hash method.
bool TryGetExtension(Type Type, out IXmppExtension Extension)
Tries to get a registered extension of a specific type from the client.
string PasswordHash
Hash value of password. Depends on method used to authenticate user.
Class containing credentials for an XMPP client connection.
const int DefaultPort
Default XMPP Server port.
void SetDomain(string DomainName, bool DefaultXmppConnectivity, string Key, string Secret)
Set the domain name to connect to.
string? ApiKey
API Key, for creating new account.
string? LegalJid
The Jabber Legal JID for this user/profile.
string? ApiSecret
API Secret, for creating new account.
bool DefaultXmppConnectivity
If connecting to the domain can be done using default parameters (host=domain, default c2s port).
bool NeedsUpdating()
Returns true if the current ITagProfile needs to have its values updated, false otherwise.
void SetAccount(string AccountName, string ClientPasswordHash, string ClientPasswordHashMethod)
Set the account name and password for a new account.
string? Domain
The domain this profile is connected to.
Task SetAccountAndLegalIdentity(string AccountName, string ClientPasswordHash, string ClientPasswordHashMethod, LegalIdentity Identity)
Set the account name and password for an existing account.
Task GoToAsync(string Route, BackMethod BackMethod=BackMethod.Inherited, string? UniqueId=null)
Navigates the AppShell to the specified route, with page arguments to match.
Task< bool > DisplayAlert(string Title, string Message, string? Accept=null, string? Cancel=null)
Displays an alert/message box to the user.
class ISO_3166_Country(string Name, string Alpha2, string Alpha3, int NumericCode, string DialCode, EmojiInfo? EmojiInfo=null)
Representation of an ISO3166-1 Country
RegistrationStep
The different steps of a TAG Profile registration journey.
BackMethod
Navigation Back Method
IdentityState
Lists recognized legal identity states.