1using System.Collections.ObjectModel;
2using System.ComponentModel;
3using CommunityToolkit.Mvvm.ComponentModel;
4using CommunityToolkit.Mvvm.Input;
29 private const string profilePhotoFileName =
"ProfilePhoto.jpg";
30 private readonly
string localPhotoFileName;
31 private readonly PhotosLoader photosLoader;
41 this.localPhotoFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), profilePhotoFileName);
42 this.photosLoader =
new PhotosLoader();
44 this.genders =
new ObservableCollection<ISO_5218_Gender>(
ISO_5218.
Genders);
49 this.ApplicationId =
null;
62 this.ApplicationSent =
true;
63 this.ApplicationId = IdentityReference.
Id;
66 await Task.Run(this.LoadFeaturedPeerReviewers);
70 this.ApplicationSent =
false;
73 this.peerReviewServices =
null;
74 this.HasFeaturedPeerReviewers =
false;
81 this.Personal = Args.Personal;
82 this.Organizational = Args.Organizational;
84 else if (IdentityReference is not
null)
86 this.Organizational = IdentityReference.IsOrganizational();
87 this.Personal = !this.Organizational;
90 if (IdentityReference is not
null)
92 await this.
SetProperties(IdentityReference, Args?.ReusePhoto ??
true,
true,
true, this.Organizational,
true);
94 if (
string.IsNullOrEmpty(this.OrgCountryCode) && this.Organizational)
95 this.OrgCountryCode = this.CountryCode;
97 if (
string.IsNullOrEmpty(this.NationalityCode) && this.RequiresNationality)
98 this.NationalityCode = this.CountryCode;
101 this.RequiresOrgName = this.Organizational;
102 this.RequiresOrgDepartment = this.Organizational;
103 this.RequiresOrgRole = this.Organizational;
104 this.RequiresOrgNumber = this.Organizational;
106 ServiceRef.XmppService.IdentityApplicationChanged += this.XmppService_IdentityApplicationChanged;
108 await base.OnInitialize();
110 if (!this.HasApplicationAttributes && this.IsConnected)
111 await Task.Run(this.LoadApplicationAttributes);
116 this.photosLoader.CancelLoadPhotos();
118 ServiceRef.XmppService.IdentityApplicationChanged -= this.XmppService_IdentityApplicationChanged;
120 return base.OnDispose();
123 private Task XmppService_IdentityApplicationChanged(
object? Sender, LegalIdentityEventArgs e)
125 MainThread.BeginInvokeOnMainThread(async () =>
127 this.ApplicationSent = ServiceRef.TagProfile.IdentityApplication is not
null;
130 if (this.ApplicationId is not
null && this.ApplicationId ==
ServiceRef.
TagProfile.LegalIdentity?.Id)
134 if (this.ApplicationSent)
136 if (this.peerReviewServices is null)
137 await Task.Run(this.LoadFeaturedPeerReviewers);
141 this.peerReviewServices = null;
142 this.HasFeaturedPeerReviewers = false;
145 if (!this.ApplicationSent && !this.IsRevoking)
154 return Task.CompletedTask;
160 await base.XmppService_ConnectionStateChanged(Sender, NewState);
161 this.OnPropertyChanged(nameof(this.ApplicationSentAndConnected));
167 await base.OnConnected();
169 if (!this.HasApplicationAttributes && this.IsConnected)
170 await Task.Run(this.LoadApplicationAttributes);
176 base.SetIsBusy(IsBusy);
177 this.NotifyCommandsCanExecuteChanged();
190 bool SetOrganizationalProperties,
bool SetAppProperties)
192 await base.SetProperties(Identity, ClearPropertiesNotFound, SetPersonalProperties, SetOrganizationalProperties, SetAppProperties);
198 if (Country.Alpha2 ==
this.CountryCode)
200 this.Countries.Move(i, 0);
207 if (SetPhoto && Identity?.Attachments is not
null)
209 Photo? First = await this.photosLoader.LoadPhotos(Identity.Attachments,
SignWith.LatestApprovedIdOrCurrentKeys);
213 if (ClearPropertiesNotFound)
217 this.ImageBin =
null;
218 this.HasPhoto =
false;
223 this.Image = First.Source;
224 this.ImageBin = First.Binary;
225 this.HasPhoto =
true;
230 private async Task LoadApplicationAttributes()
237 MainThread.BeginInvokeOnMainThread(() =>
239 bool RequiresFirstName =
false;
240 bool RequiresMiddleNames =
false;
241 bool RequiresLastNames =
false;
242 bool RequiresPersonalNumber =
false;
243 bool RequiresAddress =
false;
244 bool RequiresAddress2 =
false;
245 bool RequiresZipCode =
false;
246 bool RequiresArea =
false;
247 bool RequiresCity =
false;
248 bool RequiresRegion =
false;
249 bool RequiresCountry =
false;
250 bool RequiresNationality =
false;
251 bool RequiresGender =
false;
252 bool RequiresBirthDate =
false;
254 foreach (
string Name
in e.RequiredProperties)
258 case Constants.XmppProperties.FirstName:
259 RequiresFirstName = true;
262 case Constants.XmppProperties.MiddleNames:
263 RequiresMiddleNames = true;
266 case Constants.XmppProperties.LastNames:
267 RequiresLastNames = true;
270 case Constants.XmppProperties.PersonalNumber:
271 RequiresPersonalNumber = true;
274 case Constants.XmppProperties.Address:
275 RequiresAddress = true;
278 case Constants.XmppProperties.Address2:
279 RequiresAddress2 = true;
282 case Constants.XmppProperties.Area:
286 case Constants.XmppProperties.City:
290 case Constants.XmppProperties.ZipCode:
291 RequiresZipCode = true;
294 case Constants.XmppProperties.Region:
295 RequiresRegion = true;
298 case Constants.XmppProperties.Country:
299 RequiresCountry = true;
302 case Constants.XmppProperties.Nationality:
303 RequiresNationality = true;
306 case Constants.XmppProperties.Gender:
307 RequiresGender = true;
310 case Constants.XmppProperties.BirthDay:
311 case Constants.XmppProperties.BirthMonth:
312 case Constants.XmppProperties.BirthYear:
313 RequiresBirthDate = true;
318 this.PeerReview = e.PeerReview;
319 this.NrPhotos = e.NrPhotos;
320 this.NrReviewers = e.NrReviewers;
321 this.RequiresCountryIso3166 = e.Iso3166;
322 this.RequiresFirstName = RequiresFirstName;
323 this.RequiresMiddleNames = RequiresMiddleNames;
324 this.RequiresLastNames = RequiresLastNames;
325 this.RequiresPersonalNumber = RequiresPersonalNumber;
326 this.RequiresAddress = RequiresAddress;
327 this.RequiresAddress2 = RequiresAddress2;
328 this.RequiresZipCode = RequiresZipCode;
329 this.RequiresArea = RequiresArea;
330 this.RequiresCity = RequiresCity;
331 this.RequiresRegion = RequiresRegion;
332 this.RequiresCountry = RequiresCountry;
333 this.RequiresNationality = RequiresNationality;
334 this.RequiresGender = RequiresGender;
335 this.RequiresBirthDate = RequiresBirthDate;
336 this.RequiresOrgAddress = this.Organizational && RequiresAddress;
337 this.RequiresOrgAddress2 = this.Organizational && RequiresAddress2;
338 this.RequiresOrgZipCode = this.Organizational && RequiresZipCode;
339 this.RequiresOrgArea = this.Organizational && RequiresArea;
340 this.RequiresOrgCity = this.Organizational && RequiresCity;
341 this.RequiresOrgRegion = this.Organizational && RequiresRegion;
342 this.RequiresOrgCountry = this.Organizational && RequiresCountry;
343 this.HasApplicationAttributes =
true;
359 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
360 private bool consent;
366 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
367 private bool correct;
373 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
374 private bool hasApplicationAttributes;
380 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
381 private int nrPhotos;
387 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
388 private int nrReviewers;
394 private int nrReviews;
400 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
401 [NotifyPropertyChangedFor(nameof(FeaturedPeerReviewers))]
402 private bool peerReview;
408 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
409 [NotifyCanExecuteChangedFor(nameof(ScanQrCodeCommand))]
410 [NotifyCanExecuteChangedFor(nameof(RequestReviewCommand))]
411 [NotifyCanExecuteChangedFor(nameof(RevokeApplicationCommand))]
412 [NotifyPropertyChangedFor(nameof(CanEdit))]
413 [NotifyPropertyChangedFor(nameof(CanRemovePhoto))]
414 [NotifyPropertyChangedFor(nameof(CanTakePhoto))]
415 [NotifyPropertyChangedFor(nameof(ApplicationSentAndConnected))]
416 [NotifyPropertyChangedFor(nameof(CanRequestFeaturedPeerReviewer))]
417 [NotifyPropertyChangedFor(nameof(FeaturedPeerReviewers))]
418 private bool applicationSent;
424 private bool personal;
430 private bool organizational;
436 [NotifyPropertyChangedFor(nameof(CanRemovePhoto))]
437 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
438 [NotifyCanExecuteChangedFor(nameof(RemovePhotoCommand))]
439 private bool hasPhoto;
445 private ImageSource? image;
451 private byte[]? imageBin;
457 private int imageRotation;
462 public bool CanEdit => !this.ApplicationSent;
467 public bool CanRemovePhoto => this.CanEdit && this.HasPhoto;
472 public bool CanTakePhoto => this.CanEdit && MediaPicker.IsCaptureSupported;
477 public bool ApplicationSentAndConnected => this.ApplicationSent && this.IsConnected;
483 private bool isApplying;
489 private bool isRevoking;
495 private string? applicationId;
501 [NotifyCanExecuteChangedFor(nameof(this.RequestReviewCommand))]
502 [NotifyPropertyChangedFor(nameof(CanRequestFeaturedPeerReviewer))]
503 [NotifyPropertyChangedFor(nameof(FeaturedPeerReviewers))]
504 private bool hasFeaturedPeerReviewers;
509 public bool CanRequestFeaturedPeerReviewer => this.ApplicationSent && this.HasFeaturedPeerReviewers;
514 public bool FeaturedPeerReviewers => this.CanRequestFeaturedPeerReviewer && this.PeerReview;
520 private ObservableCollection<ISO_3166_Country> countries;
523 [NotifyPropertyChangedFor(nameof(NationalityCode))]
524 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
531 private ObservableCollection<ISO_5218_Gender> genders;
534 [NotifyPropertyChangedFor(nameof(GenderCode))]
535 [NotifyCanExecuteChangedFor(nameof(ApplyCommand))]
541 switch (e.PropertyName)
543 case nameof(this.Nationality):
544 this.NationalityCode = this.Nationality?.Alpha2 ??
string.Empty;
547 case nameof(this.Gender):
548 this.GenderCode = this.Gender?.Letter ??
string.Empty;
552 base.OnPropertyChanged(e);
565 this.Consent = !this.Consent;
574 this.Correct = !this.Correct;
580 public override bool CanApply
584 if (!this.CanExecuteCommands || !this.Consent || !this.Correct || this.ApplicationSent || !this.HasPhoto)
587 if (this.HasApplicationAttributes)
589 if (!this.FirstNameOk ||
590 !this.MiddleNamesOk ||
592 !this.PersonalNumberOk ||
600 !this.NationalityOk ||
610 if (this.Organizational)
612 if (!this.OrgNameOk ||
613 !this.OrgDepartmentOk ||
616 !this.OrgAddressOk ||
617 !this.OrgAddress2Ok ||
618 !this.OrgZipCodeOk ||
636 protected override async Task
Apply()
638 if (this.ApplicationSent)
641 if (!await AreYouSure(
ServiceRef.
Localizer[nameof(AppResources.AreYouSureYouWantToSendThisIdApplication)]))
651 this.SetIsBusy(
true);
652 this.IsApplying =
true;
657 bool HasIdWithPrivateKey = ServiceRef.TagProfile.LegalIdentity is not
null &&
663 if (Succeeded && AddedIdentity is not
null)
666 this.ApplicationSent =
true;
667 this.ApplicationId = AddedIdentity.Id;
669 await Task.Run(this.LoadFeaturedPeerReviewers);
673 Attachment? FirstImage = AddedIdentity.Attachments.GetFirstImageAttachment();
675 if (FirstImage is not
null && this.ImageBin is not
null)
687 this.SetIsBusy(
false);
688 this.IsApplying =
false;
695 [RelayCommand(CanExecute = nameof(ApplicationSent))]
696 private async Task RevokeApplication()
699 if (Application is
null)
701 this.ApplicationSent =
false;
702 this.peerReviewServices =
null;
703 this.HasFeaturedPeerReviewers =
false;
707 if (!await AreYouSure(
ServiceRef.
Localizer[nameof(AppResources.AreYouSureYouWantToRevokeTheCurrentIdApplication)]))
715 this.SetIsBusy(
true);
716 this.IsRevoking =
true;
729 this.ApplicationSent =
false;
730 this.peerReviewServices =
null;
731 this.HasFeaturedPeerReviewers =
false;
737 this.IsRevoking =
false;
741 this.SetIsBusy(
false);
748 [RelayCommand(CanExecute = nameof(ApplicationSent))]
749 private async Task ScanQrCode()
751 string? Url = await Services.UI.QR.QrCode.ScanQrCode(nameof(AppResources.QrPageTitleScanPeerId),
762 private async Task SendPeerReviewRequest(
string? ReviewerId)
765 if (ToReview is
null ||
string.IsNullOrEmpty(ReviewerId))
770 this.SetIsBusy(
true);
785 this.SetIsBusy(
false);
792 [RelayCommand(CanExecute = nameof(CanTakePhoto))]
793 private async Task TakePhoto()
795 if (!this.CanTakePhoto)
800 FileResult? Result = await MediaPicker.Default.CapturePhotoAsync(
new MediaPickerOptions()
808 Stream stream = await Result.OpenReadAsync();
809 await this.AddPhoto(stream, Result.FullPath,
true);
826 public async Task
AddPhoto(
byte[] Bin,
string ContentType,
int Rotation,
bool saveLocalCopy,
bool showAlert)
838 this.RemovePhoto(saveLocalCopy);
844 File.WriteAllBytes(this.localPhotoFileName, Bin);
853 this.ImageRotation = Rotation;
854 this.Image = ImageSource.FromStream(() =>
new MemoryStream(Bin));
856 this.HasPhoto =
true;
865 public async Task
AddPhoto(Stream InputStream,
string FilePath,
bool SaveLocalCopy)
867 SKData? ImageData =
null;
871 bool FallbackOriginal =
true;
876 ImageData = CompressImage(InputStream);
878 if (ImageData is not
null)
880 FallbackOriginal =
false;
885 if (FallbackOriginal)
887 byte[] Bin = File.ReadAllBytes(FilePath);
889 ContentType =
"application/octet-stream";
891 await this.AddPhoto(Bin, ContentType, PhotosLoader.GetImageRotation(Bin), SaveLocalCopy,
true);
903 ImageData?.Dispose();
907 private static SKData? CompressImage(Stream inputStream)
911 using SKManagedStream ManagedStream =
new(inputStream);
912 using SKData ImageData = SKData.Create(ManagedStream);
914 SKCodec Codec = SKCodec.Create(ImageData);
915 SKBitmap SkBitmap = SKBitmap.Decode(ImageData);
917 SkBitmap = HandleOrientation(SkBitmap, Codec.EncodedOrigin);
920 int Height = SkBitmap.Height;
921 int Width = SkBitmap.Width;
924 if ((Width >= Height) && (Width > 1920))
926 Height = (int)(Height * (1920.0 / Width) + 0.5);
930 else if ((Height > Width) && (Height > 1920))
932 Width = (int)(Width * (1920.0 / Height) + 0.5);
939 SKImageInfo Info = SkBitmap.Info;
940 SKImageInfo NewInfo =
new(Width, Height, Info.ColorType, Info.AlphaType, Info.ColorSpace);
941 SkBitmap = SkBitmap.Resize(NewInfo, SKFilterQuality.High);
944 return SkBitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
953 private static SKBitmap HandleOrientation(SKBitmap Bitmap, SKEncodedOrigin Orientation)
959 case SKEncodedOrigin.BottomRight:
960 Rotated =
new SKBitmap(Bitmap.Width, Bitmap.Height);
962 using (SKCanvas Surface =
new(Rotated))
964 Surface.RotateDegrees(180, Bitmap.Width / 2, Bitmap.Height / 2);
965 Surface.DrawBitmap(Bitmap, 0, 0);
969 case SKEncodedOrigin.RightTop:
970 Rotated =
new SKBitmap(Bitmap.Height, Bitmap.Width);
972 using (SKCanvas Surface =
new(Rotated))
974 Surface.Translate(Rotated.Width, 0);
975 Surface.RotateDegrees(90);
976 Surface.DrawBitmap(Bitmap, 0, 0);
980 case SKEncodedOrigin.LeftBottom:
981 Rotated =
new SKBitmap(Bitmap.Height, Bitmap.Width);
983 using (SKCanvas Surface =
new(Rotated))
985 Surface.Translate(0, Rotated.Height);
986 Surface.RotateDegrees(270);
987 Surface.DrawBitmap(Bitmap, 0, 0);
998 private void RemovePhoto(
bool RemoveFileOnDisc)
1004 this.ImageBin =
null;
1005 this.HasPhoto =
false;
1007 if (RemoveFileOnDisc && File.Exists(
this.localPhotoFileName))
1008 File.Delete(this.localPhotoFileName);
1010 catch (Exception ex)
1019 [RelayCommand(CanExecute = nameof(CanEdit))]
1020 private async Task PickPhoto()
1024 FileResult? Result = await MediaPicker.Default.PickPhotoAsync(
new MediaPickerOptions()
1032 Stream stream = await Result.OpenReadAsync();
1033 await this.AddPhoto(stream, Result.FullPath,
true);
1035 catch (Exception ex)
1045 [RelayCommand(CanExecute = nameof(CanRemovePhoto))]
1046 private void RemovePhoto()
1048 this.RemovePhoto(
true);
1051 private async Task LoadFeaturedPeerReviewers()
1057 MainThread.BeginInvokeOnMainThread(() =>
1059 this.HasFeaturedPeerReviewers = this.peerReviewServices.Length > 0;
1067 [RelayCommand(CanExecute = nameof(CanRequestFeaturedPeerReviewer))]
1068 private async Task RequestReview()
1070 if (this.peerReviewServices is
null)
1071 await this.LoadFeaturedPeerReviewers();
1073 if ((this.peerReviewServices?.Length ?? 0) > 0)
1075 List<ServiceProviderWithLegalId> ServiceProviders = [.. this.peerReviewServices,
new RequestFromPeer()];
1077 ServiceProvidersNavigationArgs e =
new([.. ServiceProviders],
1083 if (e.ServiceProvider is not
null)
1107 await this.ScanQrCode();
The Application class, representing an instance of the Neuro-Access app.
static Task< bool > AuthenticateUser(AuthenticationPurpose Purpose, bool Force=false)
Authenticates the user using the configured authentication method.
const string Jpeg
The JPEG MIME type.
static bool StartsWithIdScheme(string Url)
Checks if the specified code starts with the IoT ID scheme.
static ? string RemoveScheme(string Url)
Removes the URI Schema from an URL.
const string IotId
The IoT ID URI Scheme (iotid)
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[] Countries
This collection built from Wikipedia entry on ISO3166-1 on 9th Feb 2016
Static class containing ISO 5218 gender codes
static readonly ISO_5218_Gender[] Genders
Available gender codes
Personal Number Schemes available in different countries.
static async Task< NumberInformation > Validate(string CountryCode, string PersonalNumber)
Checks if a personal number is valid, in accordance with registered personal number schemes.
Represent an attachment to a LegalIdentity.
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 IAttachmentCacheService AttachmentCacheService
AttachmentCache service.
static ITagProfile TagProfile
TAG Profile service.
static IStringLocalizer Localizer
Localization service
static IXmppService XmppService
The XMPP service for XMPP communication.
The view model to bind to for when displaying the an application for a Personal ID.
async Task AddPhoto(Stream InputStream, string FilePath, bool SaveLocalCopy)
Adds a photo from a filestream to use as a profile photo.
async Task AddPhoto(byte[] Bin, string ContentType, int Rotation, bool saveLocalCopy, bool showAlert)
Adds a photo from the specified path to use as a profile photo.
override void SetIsBusy(bool IsBusy)
Sets the IsBusy property.
override async Task Apply()
Executes the application command.
void ToggleConsent()
Toggles Consent
override Task OnDispose()
Method called when the view is disposed, and will not be used more. Use this method to unregister eve...
override async Task OnInitialize()
Method called when view is initialized for the first time. Use this method to implement registration ...
ApplyIdViewModel()
Creates an instance of the ApplyIdViewModel class.
void ToggleCorrect()
Toggles Correct
override void OnPropertyChanged(PropertyChangedEventArgs e)
override async Task XmppService_ConnectionStateChanged(object? Sender, XmppState NewState)
Listens to connection state changes from the XMPP server.
override async Task OnConnected()
Gets called when the app gets connected.
async Task SetProperties(LegalIdentity Identity, bool SetPhoto, bool ClearPropertiesNotFound, bool SetPersonalProperties, bool SetOrganizationalProperties, bool SetAppProperties)
Sets the properties of the view model.
A page to display when the user wants to view an identity.
The data model for registering an identity.
A page that allows the user to view its tokens.
Static class managing encoding and decoding of internet content.
static bool TryGetContentType(string FileExtension, out string ContentType)
Tries to get the content type of an item, given its file extension.
Contains a reference to an attachment assigned to a legal object.
string ContentType
Internet Content Type of binary attachment.
string Url
URL to retrieve attachment, if provided.
string Id
ID of the legal identity
Contains information about a service provider.
ServiceProvider()
Contains information about a service provider.
string Type
Type of service provider.
string Id
ID of service provider.
Contains information about a service provider with a legal identity.
bool External
If legal identity is external (true) or belongs to the server (false).
string LegalId
Legal identity
The requesting entity does not possess the necessary permissions to perform an action that only certa...
Task DisplayException(Exception Exception, string? Title=null)
Displays an alert/message box to the user.
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.
Interface for information about a service provider.
class ISO_3166_Country(string Name, string Alpha2, string Alpha3, int NumericCode, string DialCode, EmojiInfo? EmojiInfo=null)
Representation of an ISO3166-1 Country
class ISO_5218_Gender(string Gender, int Code, string Letter, string LocalizedNameId, char Unicode)
Contains one record of the ISO 5218 data set.
class Photo(byte[] Binary, int Rotation)
Class containing information about a photo.
BackMethod
Navigation Back Method
class ApplyIdNavigationArgs(bool Personal, bool ReusePhoto)
Navigation arguments for the ApplyIdPage and ApplyIdViewModel.
AuthenticationPurpose
Purpose for requesting the user to authenticate itself.
SignWith
Options on what keys to use when signing data.
XmppState
State of XMPP connection.