Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
KycProcessViewModel.cs
2using System.Collections.ObjectModel;
3using System.Globalization;
4using System.Linq;
5using System.IO;
6using System.Threading;
7using System.Threading.Tasks;
8using CommunityToolkit.Mvvm.ComponentModel;
9using CommunityToolkit.Mvvm.Input;
23using SkiaSharp;
33using NeuroAccessMaui.UI; // For IKeyboardInsetAware
35
37{
41 public partial class KycProcessViewModel : BaseViewModel, IDisposable, IKeyboardInsetAware
42 {
43 private readonly IKycService kycService = ServiceRef.KycService;
44 private bool disposedValue; // Disposal flag
45 private readonly KycProcessNavigationArgs? navigationArguments;
46 private KycProcess? process;
47 private KycReference? kycReference;
48 private int currentPageIndex = 0;
49 private KycNavigationSnapshot navigation = new KycNavigationSnapshot(0, -1, KycFlowState.Form);
50 private bool applicationSent;
51 private string? applicationId;
52 private readonly ReentrancyGuard navigationGuard = new();
53 private readonly ReentrancyGuard applyGuard = new();
54
55 private List<Property> mappedValues;
56 private List<LegalIdentityAttachment> attachments;
57 private ServiceProviderWithLegalId[]? peerReviewServices = null;
58
59 private bool editingFromSummary;
60 [ObservableProperty] private bool isLoading;
61
62 [ObservableProperty]
63 [NotifyCanExecuteChangedFor(nameof(ScanQrCodeCommand))]
64 [NotifyCanExecuteChangedFor(nameof(RequestReviewCommand))]
65 [NotifyCanExecuteChangedFor(nameof(RevokeApplicationCommand))]
66 private bool applicationSentPublic;
67
68 [ObservableProperty] private bool peerReview;
69 [ObservableProperty] private int nrReviews;
70 [ObservableProperty] private int nrReviewers;
71 [ObservableProperty]
72 [NotifyCanExecuteChangedFor(nameof(RequestReviewCommand))]
73 private bool hasFeaturedPeerReviewers;
74
75 [ObservableProperty] private int currentPagePosition;
76 [ObservableProperty] private KycPage? currentPage;
77 [ObservableProperty] private string? currentPageTitle;
78 [ObservableProperty] private string? currentPageDescription;
79 [ObservableProperty] private bool hasCurrentPageDescription;
80 [ObservableProperty] private ReadOnlyObservableCollection<KycSection>? currentPageSections;
81 [ObservableProperty] private bool hasSections;
82 [ObservableProperty] private string nextButtonText = "Next";
83
84 [ObservableProperty] private ObservableCollection<DisplayQuad> personalInformationSummary;
85 [ObservableProperty] private ObservableCollection<DisplayQuad> addressInformationSummary;
86 [ObservableProperty] private ObservableCollection<DisplayQuad> attachmentInformationSummary;
87 [ObservableProperty] private ObservableCollection<DisplayQuad> companyInformationSummary;
88 [ObservableProperty] private ObservableCollection<DisplayQuad> companyAddressSummary;
89 [ObservableProperty] private ObservableCollection<DisplayQuad> companyRepresentativeSummary;
90 [ObservableProperty] private ObservableCollection<string> invalidatedItems = new ObservableCollection<string>();
91 [ObservableProperty] private ObservableCollection<string> unvalidatedItems = new ObservableCollection<string>();
92
93 [ObservableProperty] private string? errorDescription;
94 [ObservableProperty] private bool hasErrorDescription;
95 [ObservableProperty] private string unvalidatedSummaryText = string.Empty;
96
97 [ObservableProperty] private bool hasPersonalInformation;
98 [ObservableProperty] private bool hasAddressInformation;
99 [ObservableProperty] private bool hasAttachments;
100 [ObservableProperty] private bool hasCompanyInformation;
101 [ObservableProperty] private bool hasCompanyAddress;
102 [ObservableProperty] private bool hasCompanyRepresentative;
103 [ObservableProperty] private bool isNavigating; // Combined busy state for navigation/apply
104
105 // Throttling support for page refresh (avoid per-keystroke SetCurrentPage work)
106 private static readonly TimeSpan pageRefreshThrottle = TimeSpan.FromMilliseconds(250);
107 private static readonly TimeSpan fieldSnapshotThrottle = TimeSpan.FromMilliseconds(500);
108 private readonly ObservableTask<int> pageRefreshTask;
109 private readonly ObservableTask<int> fieldSnapshotTask;
110
111 public string BannerUriLight => ServiceRef.ThemeService.GetImageUri(Constants.Branding.BannerSmallLight);
112 public string BannerUriDark => ServiceRef.ThemeService.GetImageUri(Constants.Branding.BannerSmallDark);
113
114 public string BannerUri => (Application.Current?.RequestedTheme ?? AppTheme.Light) switch
115 {
116 AppTheme.Dark => this.BannerUriDark,
117 AppTheme.Light => this.BannerUriLight,
118 _ => this.BannerUriLight
119 };
120
121 public ObservableCollection<KycPage> Pages => this.process is not null ? this.process.Pages : [];
122
123 public bool IsInSummary => this.navigation.State == KycFlowState.Summary || this.navigation.State == KycFlowState.PendingSummary;
124 public bool IsEditingFromSummary => this.editingFromSummary;
125 public bool HasUnvalidatedItems => (this.kycReference?.ApplicationReview?.UnvalidatedClaims?.Length ?? 0) > 0 ||
126 (this.kycReference?.ApplicationReview?.UnvalidatedPhotos?.Length ?? 0) > 0;
127
128 public bool HasInvalidatedItems => this.InvalidatedItems.Count > 0;
129 public bool ShouldShowUnvalidatedBanner => this.HasUnvalidatedItems && this.InvalidatedItems.Count == 0 && this.kycReference?.CreatedIdentityState == IdentityState.Created;
130 public bool ShouldShowRejectionBanner => ((!string.IsNullOrEmpty(this.kycReference?.ApplicationReview?.Code)) && this.kycReference?.ApplicationReview?.Code != "ManualReview") || this.kycReference?.CreatedIdentityState == IdentityState.Rejected;
131
132 private void SetEditingFromSummary(bool value)
133 {
134 if (this.editingFromSummary == value) return;
135 this.editingFromSummary = value;
136 this.OnPropertyChanged(nameof(this.IsEditingFromSummary));
137 }
138
139 public double Progress
140 {
141 get
142 {
143 if (this.process is null || this.CurrentPage is null)
144 {
145 this.ProgressPercent = "0%";
146 return 0;
147 }
148
149 ObservableCollection<KycPage> Visible = [.. this.Pages.Where(p => p.IsVisible(this.process.Values))];
150 if (Visible.Count == 0)
151 {
152 this.ProgressPercent = "0%";
153 return 0;
154 }
155 if (this.IsInSummary)
156 {
157 this.ProgressPercent = "100%";
158 return 1;
159 }
160 int index = Visible.IndexOf(this.CurrentPage);
161 if (index < 0)
162 {
163 // Current page disappeared (visibility rule change) -> treat as start until navigation picks another page.
164 this.ProgressPercent = "0%";
165 return 0;
166 }
167 double progress = Math.Clamp((double)index / Visible.Count, 0, 1);
168 this.ProgressPercent = $"{(progress * 100):0}%";
169 return progress;
170 }
171 }
172
173 [ObservableProperty] private string progressPercent = "0%";
174
178 public bool IsKeyboardVisible => this.isKeyboardVisible;
179
183 private bool isKeyboardVisible;
184
189 public bool ShowBottomBar => !this.ApplicationSentPublic && !this.IsKeyboardVisible;
190
195 public void OnKeyboardInsetChanged(Services.UI.KeyboardInsetChangedEventArgs args)
196 {
197 bool Visible = args?.IsVisible ?? false;
198 if (this.isKeyboardVisible == Visible)
199 return;
200 this.isKeyboardVisible = Visible;
201 this.OnPropertyChanged(nameof(this.IsKeyboardVisible));
202 this.OnPropertyChanged(nameof(this.ShowBottomBar));
203 }
204
205
206 public IAsyncRelayCommand NextCommand { get; }
207 public IRelayCommand PreviousCommand { get; }
208
209 public KycProcessViewModel()
210 {
211 this.navigationArguments = ServiceRef.NavigationService.PopLatestArgs<KycProcessNavigationArgs>();
212 this.NextCommand = new AsyncRelayCommand(this.ExecuteNextAsync, this.CanExecuteNext);
213 this.PreviousCommand = new AsyncRelayCommand(this.ExecutePrevious);
214 this.PersonalInformationSummary = new ObservableCollection<DisplayQuad>();
215 this.AddressInformationSummary = new ObservableCollection<DisplayQuad>();
216 this.AttachmentInformationSummary = new ObservableCollection<DisplayQuad>();
217 this.CompanyInformationSummary = new ObservableCollection<DisplayQuad>();
218 this.CompanyAddressSummary = new ObservableCollection<DisplayQuad>();
219 this.CompanyRepresentativeSummary = new ObservableCollection<DisplayQuad>();
220 this.mappedValues = new List<Property>();
221 this.attachments = new List<LegalIdentityAttachment>();
222 this.ApplicationSentPublic = ServiceRef.TagProfile.IdentityApplication is not null;
223 this.NrReviews = ServiceRef.TagProfile.NrReviews;
224 this.pageRefreshTask = new ObservableTaskBuilder()
225 .Named("KYC Page Refresh")
226 .AutoStart(false)
227 .UseTaskRun(false)
228 .WithPolicy(Policies.Debounce(pageRefreshThrottle))
229 .Run(async context =>
230 {
231 CancellationToken CancellationToken = context.CancellationToken;
232 if (CancellationToken.IsCancellationRequested || this.disposedValue)
233 {
234 return;
235 }
236
237 try
238 {
239 await MainThread.InvokeOnMainThreadAsync(() =>
240 {
241 if (CancellationToken.IsCancellationRequested || this.disposedValue)
242 {
243 return;
244 }
245 this.SetCurrentPage(this.currentPageIndex);
246 this.NextCommand.NotifyCanExecuteChanged();
247 });
248 }
249 catch (TaskCanceledException)
250 {
251 }
252 catch (Exception Ex)
253 {
254 ServiceRef.LogService.LogException(Ex);
255 }
256 })
257 .Build();
258 this.fieldSnapshotTask = new ObservableTaskBuilder()
259 .Named("KYC Field Snapshot")
260 .AutoStart(false)
261 .UseTaskRun(true)
262 .WithPolicy(Policies.Debounce(fieldSnapshotThrottle))
263 .Run(async context =>
264 {
265 if (context.CancellationToken.IsCancellationRequested || this.disposedValue)
266 {
267 return;
268 }
269
270 if (this.kycReference is null || this.process is null)
271 {
272 return;
273 }
274
275 KycReference Reference = this.kycReference;
276 KycProcess Process = this.process;
277 string? CurrentPageId = this.CurrentPage?.Id;
278 KycNavigationSnapshot NavigationSnapshot = this.navigation;
279 double ProgressValue = this.Progress;
280
281 await this.kycService.ScheduleSnapshotAsync(Reference, Process, NavigationSnapshot, ProgressValue, CurrentPageId).ConfigureAwait(false);
282 })
283 .Build();
284 }
285
286 public override async Task OnInitializeAsync()
287 {
288 this.IsLoading = true;
289 await base.OnInitializeAsync();
290
291 string Lang = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
292 this.kycReference = this.navigationArguments?.Reference;
293 if (this.kycReference is null)
294 {
295 await ServiceRef.UiService.DisplayAlert(ServiceRef.Localizer[nameof(AppResources.ErrorTitle)], "Missing KYC reference.", ServiceRef.Localizer[nameof(AppResources.Ok)]);
296 await this.GoBack();
297 return;
298 }
299
300 this.process = await this.kycReference.ToProcess(Lang);
301 this.applicationId = this.kycReference.CreatedIdentityId;
302 this.OnPropertyChanged(nameof(this.Pages));
303 if (this.process is null)
304 {
305 await ServiceRef.UiService.DisplayAlert(ServiceRef.Localizer[nameof(AppResources.ErrorTitle)], "Unable to load KYC process.", ServiceRef.Localizer[nameof(AppResources.Ok)]);
306 await this.GoBack();
307 return;
308 }
309 this.process.Initialize();
310
311 bool Pending = this.kycReference.CreatedIdentityState == IdentityState.Created && !string.IsNullOrEmpty(this.kycReference.CreatedIdentityId);
312 bool Rejected = this.kycReference.CreatedIdentityState == IdentityState.Rejected && !string.IsNullOrEmpty(this.kycReference.CreatedIdentityId);
313 this.applicationSent = Pending;
314 this.ApplicationSentPublic = Pending;
315 if (!Pending && !Rejected)
316 this.PrefillFieldsFromProfile();
317
318 ServiceRef.XmppService.IdentityApplicationChanged += this.XmppService_IdentityApplicationChanged;
319 if (ServiceRef.TagProfile.IdentityApplication is not null)
320 {
321 await this.LoadApplicationAttributes();
322 await this.LoadFeaturedPeerReviewers();
323 }
324
325 ApplicationReview? ExistingReview = this.kycReference.ApplicationReview;
326 if (ExistingReview is not null)
327 {
328 this.ErrorDescription = ExistingReview.Message;
329 this.HasErrorDescription = !string.IsNullOrWhiteSpace(this.ErrorDescription);
330 KycInvalidation.ApplyInvalidations(this.process, ExistingReview, this.ErrorDescription);
331 this.UpdateReviewIndicators();
332 }
333
334 this.process.ClearValidation();
335 foreach (KycPage Page in this.process.Pages)
336 {
337 Page.PropertyChanged += this.Page_PropertyChanged;
338 foreach (ObservableKycField Field in Page.AllFields)
339 Field.PropertyChanged += this.Field_PropertyChanged;
340 foreach (KycSection Section in Page.AllSections)
341 {
342 Section.PropertyChanged += this.Section_PropertyChanged;
343 foreach (ObservableKycField Field in Section.AllFields)
344 Field.PropertyChanged += this.Field_PropertyChanged;
345 }
346 }
347 foreach (KycPage Page in this.process.Pages)
348 Page.UpdateVisibilities(this.process.Values);
349
350 int ResumeIndex = -1;
351 if (!string.IsNullOrEmpty(this.kycReference.LastVisitedPageId))
352 {
353 for (int I = 0; I < this.Pages.Count; I++)
354 {
355 KycPage PageItem = this.Pages[I];
356 if (PageItem.Id == this.kycReference.LastVisitedPageId && PageItem.IsVisible(this.process.Values))
357 {
358 ResumeIndex = I;
359 break;
360 }
361 }
362 }
363
364 bool LastWasSummary = string.Equals(this.kycReference.LastVisitedMode, "Summary", StringComparison.OrdinalIgnoreCase);
365
366 if (LastWasSummary)
367 {
368 int FirstInvalid = await this.GetFirstInvalidVisiblePageIndexAsync();
369 if (FirstInvalid >= 0)
370 {
371 // There are invalid fields -> go to first invalid page but mark editing from summary
372 this.SetEditingFromSummary(true);
373 this.currentPageIndex = FirstInvalid;
374 this.CurrentPagePosition = FirstInvalid;
375 this.SetCurrentPage(FirstInvalid);
376 this.NextButtonText = ServiceRef.Localizer["Kyc_Next"].Value;
377 }
378 else
379 {
380 await this.BuildMappedValuesAsync();
381 // Keep a valid page index for navigation (use last visited if available else first visible)
382 this.currentPageIndex = ResumeIndex >= 0 ? ResumeIndex : this.GetFirstVisibleIndex();
383 if (this.currentPageIndex >= 0)
384 {
385 this.CurrentPagePosition = this.currentPageIndex;
386 this.SetCurrentPage(this.currentPageIndex);
387 }
388 int AnchorIndex = this.currentPageIndex >= 0 ? this.currentPageIndex : this.navigation.AnchorPageIndex;
389 this.navigation = this.navigation with { State = KycFlowState.Summary, AnchorPageIndex = AnchorIndex, CurrentPageIndex = AnchorIndex >= 0 ? AnchorIndex : this.navigation.CurrentPageIndex };
390 this.SetEditingFromSummary(false);
391 this.NotifyNavigationChanged();
392 this.NextButtonText = ServiceRef.Localizer["Kyc_Apply"].Value;
393 }
394 }
395 else if (Rejected)
396 {
397 await this.BuildMappedValuesAsync();
398 // For rejected applications, allow re-application: show summary but keep a valid page index
399 int FirstInvalid = await this.GetFirstInvalidVisiblePageIndexAsync();
400 this.currentPageIndex = FirstInvalid >= 0 ? FirstInvalid : this.GetFirstVisibleIndex();
401 if (this.currentPageIndex >= 0)
402 {
403 this.CurrentPagePosition = this.currentPageIndex;
404 this.SetCurrentPage(this.currentPageIndex);
405 }
406 int AnchorIndexRejected = this.currentPageIndex >= 0 ? this.currentPageIndex : this.navigation.AnchorPageIndex;
407 this.navigation = this.navigation with { State = KycFlowState.Summary, AnchorPageIndex = AnchorIndexRejected, CurrentPageIndex = AnchorIndexRejected >= 0 ? AnchorIndexRejected : this.navigation.CurrentPageIndex };
408 this.SetEditingFromSummary(false);
409 this.NotifyNavigationChanged();
410 this.NextButtonText = ServiceRef.Localizer["Kyc_Apply"].Value;
411 }
412 else
413 {
414 // Normal form resume
415 this.currentPageIndex = ResumeIndex >= 0 ? ResumeIndex : this.GetFirstVisibleIndex();
416 // Synchronize navigation snapshot so Back() logic and snapshot persistence have correct page index.
417 if (this.currentPageIndex >= 0)
418 this.navigation = this.navigation with { CurrentPageIndex = this.currentPageIndex };
419 this.CurrentPagePosition = this.currentPageIndex; // Property change no longer triggers page set.
420 this.SetCurrentPage(this.currentPageIndex);
421 }
422
423 this.NextButtonText = this.IsInSummary ? ServiceRef.Localizer["Kyc_Apply"].Value : ServiceRef.Localizer["Kyc_Next"].Value;
424 MainThread.BeginInvokeOnMainThread(this.NextCommand.NotifyCanExecuteChanged);
425 if (Pending)
426 {
427 await this.BuildMappedValuesAsync();
428 int AnchorIndexPending = this.currentPageIndex >= 0 ? this.currentPageIndex : this.navigation.AnchorPageIndex;
429 this.navigation = this.navigation with { State = KycFlowState.Summary, AnchorPageIndex = AnchorIndexPending, CurrentPageIndex = AnchorIndexPending >= 0 ? AnchorIndexPending : this.navigation.CurrentPageIndex };
430 this.SetEditingFromSummary(false);
431 this.NotifyNavigationChanged();
432 this.NextButtonText = ServiceRef.Localizer["Kyc_Apply"].Value;
433 }
434 this.IsLoading = false;
435 }
436
437 private int GetFirstVisibleIndex()
438 {
439 if (this.process is null) return -1;
440 for (int I = 0; I < this.Pages.Count; I++)
441 {
442 if (this.Pages[I].IsVisible(this.process.Values)) return I;
443 }
444 return -1;
445 }
446
447 [RelayCommand]
448 private async Task GoToPageWithMapping(string? mapping)
449 {
450 if (string.IsNullOrWhiteSpace(mapping)) return;
451 if (this.process is null) return;
452 if (this.ApplicationSentPublic) return; // Cannot edit after application sent
453
454 int TargetIndex = this.FindPageIndexByMapping(mapping);
455 if (TargetIndex < 0) return;
456
457 // Transition from summary to edit mode
458 this.navigation = this.navigation with { State = KycFlowState.Form };
459 this.SetEditingFromSummary(true); // Allow quick return to summary
460 this.currentPageIndex = TargetIndex;
461 this.CurrentPagePosition = TargetIndex;
462 this.SetCurrentPage(TargetIndex);
463 this.NotifyNavigationChanged();
464 await MainThread.InvokeOnMainThreadAsync(this.ScrollUp);
465 }
466
467 private int FindPageIndexByMapping(string Mapping)
468 {
469 if (this.process is null) return -1;
470 string MappingKey = Mapping.Trim();
471 for (int I = 0; I < this.Pages.Count; I++)
472 {
473 KycPage Page = this.Pages[I];
474 if (!Page.IsVisible(this.process.Values)) continue;
475 foreach (ObservableKycField Field in Page.AllFields)
476 {
477 if (FieldMatches(Field, MappingKey)) return I;
478 }
479 foreach (KycSection Section in Page.AllSections)
480 {
481 foreach (ObservableKycField Field in Section.AllFields)
482 {
483 if (FieldMatches(Field, MappingKey)) return I;
484 }
485 }
486 }
487 return -1;
488 static bool FieldMatches(ObservableKycField field, string mappingKey)
489 {
490 foreach (KycMapping Map in field.Mappings)
491 {
492 if (string.Equals(Map.Key, mappingKey, StringComparison.OrdinalIgnoreCase)) return true;
493 if (mappingKey.Equals("BDATE", StringComparison.OrdinalIgnoreCase) &&
494 (string.Equals(Map.Key, Constants.XmppProperties.BirthDay, StringComparison.OrdinalIgnoreCase) ||
495 string.Equals(Map.Key, Constants.XmppProperties.BirthMonth, StringComparison.OrdinalIgnoreCase) ||
496 string.Equals(Map.Key, Constants.XmppProperties.BirthYear, StringComparison.OrdinalIgnoreCase))) return true;
497 if (mappingKey.StartsWith("ORGREP", StringComparison.OrdinalIgnoreCase) && Map.Key.StartsWith("ORGREP", StringComparison.OrdinalIgnoreCase)) return true;
498 }
499 return false;
500 }
501 }
502
503 public bool CanRequestFeaturedPeerReviewer => this.ApplicationSentPublic && this.HasFeaturedPeerReviewers;
504 public bool FeaturedPeerReviewers => this.CanRequestFeaturedPeerReviewer && this.PeerReview;
505
506 partial void OnApplicationSentPublicChanged(bool value)
507 {
508 if (value)
509 {
510 int anchor = this.currentPageIndex >= 0 ? this.currentPageIndex : this.navigation.AnchorPageIndex;
511 this.navigation = this.navigation with { State = KycFlowState.Summary, AnchorPageIndex = anchor, CurrentPageIndex = anchor >= 0 ? anchor : this.navigation.CurrentPageIndex };
512 this.SetEditingFromSummary(false);
513 this.NotifyNavigationChanged();
514 }
515 this.OnPropertyChanged(nameof(this.ShowBottomBar));
516 this.OnPropertyChanged(nameof(this.CanRequestFeaturedPeerReviewer));
517 this.OnPropertyChanged(nameof(this.FeaturedPeerReviewers));
518 this.NextCommand.NotifyCanExecuteChanged();
519 }
520
521 partial void OnPeerReviewChanged(bool value)
522 {
523 this.OnPropertyChanged(nameof(this.FeaturedPeerReviewers));
524 }
525
526 partial void OnHasFeaturedPeerReviewersChanged(bool value)
527 {
528 this.OnPropertyChanged(nameof(this.CanRequestFeaturedPeerReviewer));
529 this.OnPropertyChanged(nameof(this.FeaturedPeerReviewers));
530 }
531
532 private void PrefillFieldsFromProfile()
533 {
534 if (this.process is null) return;
535 LegalIdentity? Identity = ServiceRef.TagProfile.LegalIdentity;
536 if (Identity?.Properties is null) return;
537 Dictionary<string, string> ByName = Identity.Properties
538 .Where(p => p is not null && !string.IsNullOrWhiteSpace(p.Name))
539 .GroupBy(p => p.Name!, StringComparer.OrdinalIgnoreCase)
540 .Select(g => g.OrderByDescending(p => p?.Value?.Length ?? 0).First())
541 .ToDictionary(p => p.Name!, p => p.Value ?? string.Empty, StringComparer.OrdinalIgnoreCase);
542 IEnumerable<ObservableKycField> AllFields = this.process.Pages.SelectMany(p => p.AllFields)
543 .Concat(this.process.Pages.SelectMany(p => p.AllSections).SelectMany(s => s.AllFields));
544 foreach (ObservableKycField Field in AllFields)
545 {
546 if (!string.IsNullOrWhiteSpace(Field.StringValue)) continue;
547 if (Field.Mappings.Count == 0) continue;
548 foreach (KycMapping Map in Field.Mappings)
549 {
550 if (string.IsNullOrWhiteSpace(Map.Key)) continue;
551 if (ByName.TryGetValue(Map.Key, out string? Value) && !string.IsNullOrWhiteSpace(Value))
552 {
553 Field.StringValue = Value;
554 this.process.Values[Field.Id] = Field.StringValue;
555 break;
556 }
557 }
558 }
559 }
560
561 partial void OnCurrentPagePositionChanged(int value)
562 {
563 // Removed recursive SetCurrentPage call to avoid double work.
564 if (value >= 0 && value < this.Pages.Count)
565 {
566 this.currentPageIndex = value;
567 }
568 }
569
570 private void Field_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
571 {
572 if (this.disposedValue) return;
573 if (e.PropertyName == nameof(ObservableKycField.RawValue))
574 {
575 this.SchedulePageRefresh();
576 this.ScheduleFieldSnapshot();
577 }
578 if (e.PropertyName == nameof(ObservableKycField.IsValid))
579 {
580 MainThread.BeginInvokeOnMainThread(this.NextCommand.NotifyCanExecuteChanged);
581 }
582 }
583
584 private void ScheduleFieldSnapshot()
585 {
586 if (this.disposedValue)
587 return;
588 if (this.kycReference is null || this.process is null)
589 return;
590 this.fieldSnapshotTask.Run();
591 }
592
593 private void SchedulePageRefresh()
594 {
595 if (this.disposedValue) return;
596 this.pageRefreshTask.Run();
597 }
598
599 private void Section_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
600 {
601 if (this.disposedValue) return;
602 if (e.PropertyName == nameof(KycSection.IsVisible))
603 this.SchedulePageRefresh();
604 }
605
606 private void Page_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
607 {
608 if (this.disposedValue) return;
609 if (e.PropertyName == nameof(KycPage.IsVisible))
610 this.SchedulePageRefresh();
611 }
612
613 private void SetCurrentPage(int index)
614 {
615 if (this.disposedValue) return;
616 if (this.process is null) return;
617 if (index < 0 || index >= this.Pages.Count) return;
618 // If target page is not visible (visibility changed), pick first visible page instead (unless in summary where we keep current page context).
619 if (!this.IsInSummary && !this.Pages[index].IsVisible(this.process.Values))
620 {
621 int firstVisible = this.GetFirstVisibleIndex();
622 if (firstVisible < 0) return; // No visible pages -> nothing to show.
623 index = firstVisible;
624 }
625
626 // Clamp stale index if needed (visibility could have changed since last schedule)
627 if (!this.IsInSummary && (this.currentPageIndex >= this.Pages.Count || (this.currentPageIndex >= 0 && !this.Pages[this.currentPageIndex].IsVisible(this.process.Values))))
628 {
629 int firstVisible = this.GetFirstVisibleIndex();
630 if (firstVisible >= 0) index = firstVisible;
631 }
632
633 KycPage Page = this.Pages[index];
634 bool pageChanged = !object.ReferenceEquals(this.CurrentPage, Page) || this.currentPageIndex != index;
635
636 this.currentPageIndex = index;
637 if (this.CurrentPagePosition != index)
638 {
639 this.CurrentPagePosition = index;
640 }
641 this.CurrentPage = Page;
642 this.CurrentPageTitle = Page.Title?.Text ?? Page.Id;
643 this.CurrentPageDescription = Page.Description?.Text;
644 this.HasCurrentPageDescription = !string.IsNullOrWhiteSpace(this.CurrentPageDescription);
645 this.CurrentPageSections = Page.VisibleSections;
646 this.HasSections = this.CurrentPageSections is not null && this.CurrentPageSections.Count > 0;
647 if (!this.IsInSummary && this.kycReference is not null && this.process is not null && pageChanged)
648 _ = this.kycService.ScheduleSnapshotAsync(this.kycReference, this.process, this.navigation, this.Progress, Page.Id);
649 this.OnPropertyChanged(nameof(this.Progress));
650 this.NextCommand.NotifyCanExecuteChanged();
651 }
652
653 private bool CanExecuteNext()
654 {
655 if (this.IsInSummary)
656 return !this.ApplicationSentPublic;
657 if (this.CurrentPage is null) return false;
658 IEnumerable<ObservableKycField> Fields = this.CurrentPage.VisibleFields;
659 if (this.CurrentPageSections is not null)
660 Fields = Fields.Concat(this.CurrentPageSections.SelectMany(s => s.VisibleFields));
661 return Fields.All(f => f.IsValid);
662 }
663
664 private async Task ExecuteNextAsync()
665 {
666 if (!await this.navigationGuard.RunIfNotBusy(async () =>
667 {
668 this.IsNavigating = true;
670 if (this.IsInSummary)
671 {
672 await this.ExecuteApplyAsync();
673 return;
674 }
675 if (this.editingFromSummary)
676 {
677 bool OkEditing = await this.ValidateCurrentPageAsync();
678 if (!OkEditing)
679 {
680 return;
681 }
682 int FirstInvalidFromSummary = await this.GetFirstInvalidVisiblePageIndexAsync();
683 if (FirstInvalidFromSummary >= 0)
684 {
685 this.currentPageIndex = FirstInvalidFromSummary;
686 this.CurrentPagePosition = FirstInvalidFromSummary;
687 this.SetCurrentPage(FirstInvalidFromSummary);
688 this.NextButtonText = ServiceRef.Localizer["Kyc_Next"].Value;
689 return;
690 }
691 await this.GoToSummaryAsync();
692 this.SetEditingFromSummary(false);
693 return;
694 }
695 bool Ok = await this.ValidateCurrentPageAsync();
696 if (!Ok) return;
697 if (this.kycReference is not null && this.process is not null)
698 await this.kycService.FlushSnapshotAsync(this.kycReference, this.process, this.navigation, this.Progress, this.CurrentPage?.Id);
699 if (this.process is null) return;
700 List<int> VisibleIndices = new List<int>();
701 for (int I = 0; I < this.Pages.Count; I++)
702 {
703 if (this.Pages[I].IsVisible(this.process.Values)) VisibleIndices.Add(I);
704 }
705 KycProcessState ProcessState = this.BuildProcessState();
706 KycNavigationSnapshot NextSnap = KycTransitions.Advance(ProcessState, VisibleIndices);
707 if (NextSnap.State == KycFlowState.Summary || NextSnap.State == KycFlowState.PendingSummary)
708 {
709 await this.BuildMappedValuesAsync();
710 this.ScrollUp();
711 this.SetEditingFromSummary(false);
712 this.navigation = NextSnap with { AnchorPageIndex = NextSnap.AnchorPageIndex >= 0 ? NextSnap.AnchorPageIndex : this.navigation.AnchorPageIndex >= 0 ? this.navigation.AnchorPageIndex : this.currentPageIndex };
713 this.NotifyNavigationChanged();
714 this.OnPropertyChanged(nameof(this.Progress));
715 this.NextButtonText = ServiceRef.Localizer["Kyc_Apply"].Value;
716 if (this.kycReference is not null && this.process is not null)
717 await this.kycService.FlushSnapshotAsync(this.kycReference, this.process, this.navigation, this.Progress, this.CurrentPage?.Id);
718 }
719 else
720 {
721 this.navigation = NextSnap;
722 this.currentPageIndex = NextSnap.CurrentPageIndex;
723 this.CurrentPagePosition = this.currentPageIndex;
724 this.SetCurrentPage(this.currentPageIndex);
725 this.ScrollUp();
726 this.NotifyNavigationChanged();
727 }
728 }).ConfigureAwait(false))
729 {
730 return; // Skipped due to ongoing navigation
731 }
732 this.IsNavigating = false;
733 }
734
735 public event EventHandler? ScrollToTop;
736 private void ScrollUp() => this.ScrollToTop?.Invoke(this, EventArgs.Empty);
737
738 public override Task OnDisposeAsync()
739 {
740 this.Dispose(true);
741 return base.OnDisposeAsync();
742 }
743
744 private void UnsubscribeProcessHandlers()
745 {
746 if (this.process is null) return;
747 foreach (KycPage Page in this.process.Pages)
748 {
749 Page.PropertyChanged -= this.Page_PropertyChanged;
750 foreach (ObservableKycField Field in Page.AllFields)
751 Field.PropertyChanged -= this.Field_PropertyChanged;
752 foreach (KycSection Section in Page.AllSections)
753 {
754 Section.PropertyChanged -= this.Section_PropertyChanged;
755 foreach (ObservableKycField Field in Section.AllFields)
756 Field.PropertyChanged -= this.Field_PropertyChanged;
757 }
758 }
759 }
760
761 protected virtual void Dispose(bool disposing)
762 {
763 if (this.disposedValue)
764 {
765 return;
766 }
767 if (disposing)
768 {
769 try { this.UnsubscribeProcessHandlers(); } catch { }
770 try { this.pageRefreshTask.Dispose(); } catch { }
771 ServiceRef.XmppService.IdentityApplicationChanged -= this.XmppService_IdentityApplicationChanged;
772 }
773 this.disposedValue = true;
774 }
775
776 public void Dispose()
777 {
778 this.Dispose(true);
779 GC.SuppressFinalize(this);
780 }
781
782 [RelayCommand]
783 private async Task GoToSummaryAsync()
784 {
785 if (this.process is null) return;
786 bool Ok = await this.ValidateCurrentPageAsync();
787 if (!Ok) return;
788 if (this.kycReference is not null && this.process is not null)
789 await this.kycService.FlushSnapshotAsync(this.kycReference, this.process, this.navigation, this.Progress, this.CurrentPage?.Id);
790 await this.BuildMappedValuesAsync();
791 this.ScrollUp();
792 KycProcessState ProcessState = this.BuildProcessState();
793 this.NextButtonText = ServiceRef.Localizer["Kyc_Apply"].Value;
794 this.navigation = KycTransitions.EnterSummary(ProcessState);
795 int Anchor = this.navigation.AnchorPageIndex >= 0 ? this.navigation.AnchorPageIndex : this.currentPageIndex;
796 if (Anchor >= 0)
797 {
798 this.currentPageIndex = Anchor;
799 this.CurrentPagePosition = Anchor;
800 }
801 this.SetEditingFromSummary(false);
802 this.NotifyNavigationChanged();
803 if (this.kycReference is not null && this.process is not null)
804 await this.kycService.FlushSnapshotAsync(this.kycReference, this.process, this.navigation, this.Progress, this.CurrentPage?.Id);
805 this.OnPropertyChanged(nameof(this.Progress));
806 }
807
808 private async Task ExecutePrevious()
809 {
810 if (this.process is null)
811 {
812 await base.GoBack();
813 return;
814 }
815 List<int> VisibleIndices = new List<int>();
816 for (int I = 0; I < this.Pages.Count; I++)
817 {
818 if (this.Pages[I].IsVisible(this.process.Values)) VisibleIndices.Add(I);
819 }
820 KycProcessState ProcessState = this.BuildProcessState();
821 KycNavigationSnapshot PrevSnap = KycTransitions.Back(ProcessState, VisibleIndices);
822 if (this.IsInSummary)
823 {
824 this.navigation = PrevSnap;
825 this.currentPageIndex = PrevSnap.CurrentPageIndex;
826 this.CurrentPagePosition = this.currentPageIndex;
827 this.SetCurrentPage(this.currentPageIndex);
828 this.NextButtonText = ServiceRef.Localizer["Kyc_Next"].Value;
829 this.ScrollUp();
830 this.OnPropertyChanged(nameof(this.Progress));
831 this.NotifyNavigationChanged();
832 return;
833 }
834 if (PrevSnap.CurrentPageIndex == this.currentPageIndex)
835 {
836 // Already at first visible page -> exit
837 await base.GoBack();
838 return;
839 }
840 this.navigation = PrevSnap;
841 this.currentPageIndex = PrevSnap.CurrentPageIndex;
842 this.CurrentPagePosition = this.currentPageIndex;
843 this.SetCurrentPage(this.currentPageIndex);
844 this.ScrollUp();
845 this.NotifyNavigationChanged();
846 await this.ValidateCurrentPageAsync();
847 }
848
849 private void NotifyNavigationChanged()
850 {
851 this.OnPropertyChanged(nameof(this.IsInSummary));
852 this.OnPropertyChanged(nameof(this.Progress));
853 }
854
855 public override async Task GoBack()
856 {
857 if (this.applicationSent)
858 return;
859 if (this.editingFromSummary)
860 {
861 int FirstInvalid = await this.GetFirstInvalidVisiblePageIndexAsync();
862 if (FirstInvalid >= 0)
863 {
864 this.currentPageIndex = FirstInvalid;
865 this.CurrentPagePosition = FirstInvalid;
866 this.SetCurrentPage(FirstInvalid);
867 this.NextButtonText = ServiceRef.Localizer["Kyc_Next"].Value;
868 }
869 else
870 {
871 await this.GoToSummaryAsync();
872 this.SetEditingFromSummary(false);
873 }
874 return;
875 }
876 await this.ExecutePrevious();
877 }
878
879 [RelayCommand]
880 public async Task Exit()
881 {
882 if (this.kycReference is not null && this.process is not null)
883 await this.kycService.FlushSnapshotAsync(this.kycReference, this.process, this.navigation, this.Progress, this.CurrentPage?.Id);
884
885 if (!this.IsInSummary)
886 {
887 if (!await AreYouSure(ServiceRef.Localizer[nameof(AppResources.Kyc_Exit)]))
888 return;
889 }
890
891 await base.GoBack();
892 }
893
894 private async Task<bool> ValidateCurrentPageAsync()
895 {
896 if (this.CurrentPage is null) return false;
897 bool Ok = await this.kycService.ValidatePageAsync(this.CurrentPage);
898 if (Ok && this.process is not null)
899 {
900 IEnumerable<ObservableKycField> Fields = this.CurrentPage.VisibleFields;
901 if (this.CurrentPageSections is not null)
902 Fields = Fields.Concat(this.CurrentPageSections.SelectMany(s => s.VisibleFields));
903 foreach (ObservableKycField Field in Fields)
904 this.process.Values[Field.Id] = Field.StringValue;
905 }
906 MainThread.BeginInvokeOnMainThread(this.NextCommand.NotifyCanExecuteChanged);
907 return Ok;
908 }
909
910 private async Task<int> GetFirstInvalidVisiblePageIndexAsync()
911 {
912 if (this.process is null) return -1;
913 return await this.kycService.GetFirstInvalidVisiblePageIndexAsync(this.process);
914 }
915
916 [RelayCommand]
917 private async Task ExecuteApplyAsync()
918 {
919 if (this.applicationSent) return;
920 if (!await this.applyGuard.RunIfNotBusy(async () =>
921 {
923 return;
924
926 if (!await Auth.AuthenticateUserAsync(AuthenticationPurpose.SignApplication, true))
927 return;
928 if (this.attachments is null || this.mappedValues is null) return;
929 bool HasIdWithKey = false;
930 try
931 {
932 HasIdWithKey = ServiceRef.TagProfile.LegalIdentity is not null && await ServiceRef.XmppService.HasPrivateKey(ServiceRef.TagProfile.LegalIdentity.Id);
933 }
934 catch (Exception Ex)
935 {
936 ServiceRef.LogService.LogWarning("Error checking for existing identity private key, genereting new keys...: " + Ex.Message);
937 }
938 (bool Succeeded, LegalIdentity? Added) = await ServiceRef.NetworkService.TryRequest(() => ServiceRef.XmppService.AddLegalIdentity(this.mappedValues.ToArray(), !HasIdWithKey, this.attachments.ToArray()));
939 if (Succeeded && Added is not null)
940 {
941 await ServiceRef.TagProfile.SetIdentityApplication(Added, true);
942 this.applicationSent = true;
943 if (this.kycReference is not null)
944 {
945 try { await this.kycService.ApplySubmissionAsync(this.kycReference, Added); }
946 catch (Exception Ex) { ServiceRef.LogService.LogException(Ex); }
947 }
948 this.ErrorDescription = null;
949 this.HasErrorDescription = false;
950 this.InvalidatedItems.Clear();
951 this.UnvalidatedItems.Clear();
952 this.UnvalidatedSummaryText = string.Empty;
953 this.OnPropertyChanged(nameof(this.HasUnvalidatedItems));
954 this.OnPropertyChanged(nameof(this.ShouldShowUnvalidatedBanner));
955 this.OnPropertyChanged(nameof(this.ShouldShowRejectionBanner));
956 this.RemovePendingAndResubmitCommand?.NotifyCanExecuteChanged();
957 foreach (LegalIdentityAttachment LocalAttachment in this.attachments)
958 {
959 Attachment? Match = Added.Attachments.FirstOrDefault(a => string.Equals(a.FileName, LocalAttachment.FileName, StringComparison.OrdinalIgnoreCase));
960 if (Match != null && LocalAttachment.Data is not null && LocalAttachment.ContentType is not null)
961 {
962 await ServiceRef.AttachmentCacheService.Add(Match.Url, Added.Id, true, LocalAttachment.Data, LocalAttachment.ContentType);
963 }
964 }
965 this.applicationId = Added.Id;
966 this.ApplicationSentPublic = true;
967 this.NrReviews = ServiceRef.TagProfile.NrReviews;
968 await this.LoadApplicationAttributes();
969 await this.LoadFeaturedPeerReviewers();
970 int AnchorAfterApply = this.currentPageIndex >= 0 ? this.currentPageIndex : this.navigation.AnchorPageIndex;
971 this.navigation = this.navigation with { State = KycFlowState.Summary, AnchorPageIndex = AnchorAfterApply, CurrentPageIndex = AnchorAfterApply >= 0 ? AnchorAfterApply : this.navigation.CurrentPageIndex };
972 this.NotifyNavigationChanged();
973 }
974 }).ConfigureAwait(false)) return;
975 }
976
977 private async Task LoadApplicationAttributes()
978 {
979 try
980 {
981 IdApplicationAttributesEventArgs AttributesEventArgs = await ServiceRef.XmppService.GetIdApplicationAttributes();
982 MainThread.BeginInvokeOnMainThread(() =>
983 {
984 this.PeerReview = AttributesEventArgs.PeerReview;
985 this.NrReviewers = AttributesEventArgs.NrReviewers;
986 });
987 }
988 catch (Exception Ex) { ServiceRef.LogService.LogException(Ex); }
989 }
990
991 private async Task LoadFeaturedPeerReviewers()
992 {
993 await ServiceRef.NetworkService.TryRequest(async () =>
994 {
995 this.peerReviewServices = await ServiceRef.XmppService.GetServiceProvidersForPeerReviewAsync();
996 MainThread.BeginInvokeOnMainThread(() =>
997 {
998 this.HasFeaturedPeerReviewers = (this.peerReviewServices?.Length ?? 0) > 0;
999 });
1000 });
1001 }
1002
1003 private async Task XmppService_IdentityApplicationChanged(object? sender, LegalIdentityEventArgs E)
1004 {
1005 await MainThread.InvokeOnMainThreadAsync(async () =>
1006 {
1007 this.ApplicationSentPublic = ServiceRef.TagProfile.IdentityApplication is not null;
1008 this.NrReviews = ServiceRef.TagProfile.NrReviews;
1009 if (this.kycReference is not null && this.kycReference.CreatedIdentityId == E.Identity.Id)
1010 {
1011 try { await this.kycService.UpdateSubmissionStateAsync(this.kycReference, E.Identity); } catch (Exception Ex) { ServiceRef.LogService.LogException(Ex); }
1012 if (E.Identity.State == IdentityState.Approved)
1013 {
1014 await base.GoBack();
1015 return;
1016 }
1017 else if (E.Identity.State == IdentityState.Rejected || E.Identity.State == IdentityState.Obsoleted || E.Identity.State == IdentityState.Compromised)
1018 {
1019 this.applicationSent = false;
1020 this.ApplicationSentPublic = false;
1021 try
1022 {
1023 await ServiceRef.TagProfile.SetIdentityApplication(null, true);
1024 }
1025 catch (Exception Ex)
1026 {
1027 ServiceRef.LogService.LogException(Ex);
1028 }
1029 await this.BuildMappedValuesAsync();
1030 int AnchorRejected = this.currentPageIndex >= 0 ? this.currentPageIndex : this.navigation.AnchorPageIndex;
1031 this.navigation = this.navigation with { State = KycFlowState.Summary, AnchorPageIndex = AnchorRejected, CurrentPageIndex = AnchorRejected >= 0 ? AnchorRejected : this.navigation.CurrentPageIndex };
1032 this.SetEditingFromSummary(false);
1033 if (this.kycReference is not null && this.process is not null)
1034 await this.kycService.FlushSnapshotAsync(this.kycReference, this.process, this.navigation, this.Progress, this.CurrentPage?.Id);
1035 this.NextButtonText = ServiceRef.Localizer["Kyc_Apply"].Value;
1036 this.OnPropertyChanged(nameof(this.Progress));
1037 string AlertTitle;
1038 string AlertMessage;
1039 switch (E.Identity.State)
1040 {
1041 case IdentityState.Obsoleted:
1042 AlertTitle = ServiceRef.Localizer[nameof(AppResources.ErrorTitle)];
1044 break;
1045 case IdentityState.Compromised:
1046 AlertTitle = ServiceRef.Localizer[nameof(AppResources.ErrorTitle)];
1048 break;
1049 default:
1050 AlertTitle = ServiceRef.Localizer[nameof(AppResources.Rejected)];
1052 break;
1053 }
1054 await ServiceRef.UiService.DisplayAlert(AlertTitle, AlertMessage);
1055 }
1056 }
1057 if (this.ApplicationSentPublic && this.peerReviewServices is null)
1058 await this.LoadFeaturedPeerReviewers();
1059
1060 this.UpdateReviewIndicators();
1061 });
1062 }
1063
1064 [RelayCommand(CanExecute = nameof(ApplicationSentPublic))]
1065 private async Task ScanQrCode()
1066 {
1067 string? Url = await Services.UI.QR.QrCode.ScanQrCode(nameof(AppResources.QrPageTitleScanPeerId), [Constants.UriSchemes.IotId]);
1068 if (string.IsNullOrEmpty(Url) || !Constants.UriSchemes.StartsWithIdScheme(Url)) return;
1069 await this.SendPeerReviewRequest(Constants.UriSchemes.RemoveScheme(Url));
1070 }
1071
1072 private async Task SendPeerReviewRequest(string? reviewerId)
1073 {
1074 LegalIdentity? ToReview = ServiceRef.TagProfile.IdentityApplication;
1075 if (ToReview is null || string.IsNullOrEmpty(reviewerId)) return;
1076 try
1077 {
1078 await ServiceRef.XmppService.PetitionPeerReviewId(reviewerId, ToReview, Guid.NewGuid().ToString(), ServiceRef.Localizer[nameof(AppResources.CouldYouPleaseReviewMyIdentityInformation)]);
1080 }
1081 catch (Exception Ex)
1082 {
1083 ServiceRef.LogService.LogException(Ex);
1084 await ServiceRef.UiService.DisplayException(Ex);
1085 }
1086 }
1087
1088 [RelayCommand(CanExecute = nameof(CanRequestFeaturedPeerReviewer))]
1089 private async Task RequestReview()
1090 {
1091 if (this.peerReviewServices is null)
1092 await this.LoadFeaturedPeerReviewers();
1093 if ((this.peerReviewServices?.Length ?? 0) > 0)
1094 {
1095 ServiceProviderWithLegalId[] LocalPeerReview = this.peerReviewServices ?? Array.Empty<ServiceProviderWithLegalId>();
1096 List<ServiceProviderWithLegalId> List = [.. LocalPeerReview, new RequestFromPeer()];
1097 ServiceProvidersNavigationArgs NavigationArgs = new(List.ToArray(), ServiceRef.Localizer[nameof(AppResources.RequestReview)], ServiceRef.Localizer[nameof(AppResources.SelectServiceProviderPeerReview)]);
1098 await ServiceRef.NavigationService.GoToAsync(nameof(ServiceProvidersPage), NavigationArgs, Services.UI.BackMethod.Pop);
1099 if (NavigationArgs.ServiceProvider is not null)
1100 {
1101 IServiceProvider? ServiceProvider = await NavigationArgs.ServiceProvider.Task;
1102 if (ServiceProvider is ServiceProviderWithLegalId SPWL && !string.IsNullOrEmpty(SPWL.LegalId))
1103 {
1104 if (!SPWL.External)
1105 {
1106 if (!await ServiceRef.NetworkService.TryRequest(async () => await ServiceRef.XmppService.SelectPeerReviewService(ServiceProvider.Id, ServiceProvider.Type)))
1107 return;
1108 }
1109 await this.SendPeerReviewRequest(SPWL.LegalId);
1110 return;
1111 }
1112 else if (ServiceProvider is null) return;
1113 }
1114 }
1115 await this.ScanQrCode();
1116 }
1117
1118 [RelayCommand(CanExecute = nameof(ApplicationSentPublic))]
1119 private async Task RevokeApplication()
1120 {
1121 await this.RevokeCurrentApplicationAsync(true);
1122 }
1123
1124 private Task BuildMappedValuesAsync()
1125 {
1126 if (this.process is null)
1127 {
1128 this.mappedValues = new();
1129 this.attachments = new();
1130 return Task.CompletedTask;
1131 }
1132 return Task.Run(async () =>
1133 {
1134 (IReadOnlyList<Property> Props, IReadOnlyList<LegalIdentityAttachment> Atts) = await this.kycService.PreparePropertiesAndAttachmentsAsync(this.process, CancellationToken.None);
1135 this.mappedValues = Props.ToList();
1136 this.attachments = Atts.ToList();
1137 string? Jid = ServiceRef.TagProfile.LegalIdentity?.Properties.FirstOrDefault(p => p.Name == Constants.XmppProperties.Jid)?.Value ?? ServiceRef.XmppService.BareJid ?? string.Empty;
1138 string? Phone = ServiceRef.TagProfile.LegalIdentity?.Properties.FirstOrDefault(p => p.Name == Constants.XmppProperties.Phone)?.Value ?? ServiceRef.TagProfile.PhoneNumber ?? string.Empty;
1139 string? Email = ServiceRef.TagProfile.LegalIdentity?.Properties.FirstOrDefault(p => p.Name == Constants.XmppProperties.EMail)?.Value ?? ServiceRef.TagProfile.EMail ?? string.Empty;
1141 if (!this.process.HasMapping(Constants.XmppProperties.Jid)) this.mappedValues.Add(new Property(Constants.XmppProperties.Jid, Jid));
1142 if (!this.process.HasMapping(Constants.XmppProperties.Phone)) this.mappedValues.Add(new Property(Constants.XmppProperties.Phone, Phone));
1143 if (!this.process.HasMapping(Constants.XmppProperties.EMail)) this.mappedValues.Add(new Property(Constants.XmppProperties.EMail, Email));
1144 if (!this.process.HasMapping(Constants.XmppProperties.Country) && !string.IsNullOrEmpty(ServiceRef.TagProfile.SelectedCountry))
1145 this.mappedValues.Add(new Property(Constants.XmppProperties.Country, ServiceRef.TagProfile.SelectedCountry));
1146 ISet<string> Invalid = KycSummary.BuildInvalidMappingSet(this.kycReference);
1147 KycSummaryModel Model = KycSummary.Generate(this.process, this.mappedValues, this.attachments, Invalid);
1148 MainThread.BeginInvokeOnMainThread(() => this.ApplySummaryModel(Model));
1149 });
1150 }
1151
1152 private void ApplySummaryModel(KycSummaryModel model)
1153 {
1154 this.PersonalInformationSummary = new ObservableCollection<DisplayQuad>(model.Get(KycSummary.Personal)?.Items ?? Array.Empty<DisplayQuad>());
1155 this.AddressInformationSummary = new ObservableCollection<DisplayQuad>(model.Get(KycSummary.Address)?.Items ?? Array.Empty<DisplayQuad>());
1156 this.AttachmentInformationSummary = new ObservableCollection<DisplayQuad>(model.Get(KycSummary.Attachments)?.Items ?? Array.Empty<DisplayQuad>());
1157 this.CompanyInformationSummary = new ObservableCollection<DisplayQuad>(model.Get(KycSummary.CompanyInfo)?.Items ?? Array.Empty<DisplayQuad>());
1158 this.CompanyAddressSummary = new ObservableCollection<DisplayQuad>(model.Get(KycSummary.CompanyAddress)?.Items ?? Array.Empty<DisplayQuad>());
1159 this.CompanyRepresentativeSummary = new ObservableCollection<DisplayQuad>(model.Get(KycSummary.CompanyRepresentative)?.Items ?? Array.Empty<DisplayQuad>());
1160 this.HasPersonalInformation = this.PersonalInformationSummary.Count > 0;
1161 this.HasAddressInformation = this.AddressInformationSummary.Count > 0;
1162 this.HasAttachments = this.AttachmentInformationSummary.Count > 0;
1163 this.HasCompanyInformation = this.CompanyInformationSummary.Count > 0;
1164 this.HasCompanyAddress = this.CompanyAddressSummary.Count > 0;
1165 this.HasCompanyRepresentative = this.CompanyRepresentativeSummary.Count > 0;
1166 }
1167
1168 private KycProcessState BuildProcessState()
1169 {
1170 if (this.process is null)
1171 return new KycProcessState(Array.Empty<KycPageState>(), this.navigation, this.applicationSent);
1172
1173 List<KycPageState> PageStates = new List<KycPageState>(this.process.Pages.Count);
1174 foreach (KycPage Page in this.process.Pages)
1175 {
1176 bool IsVisible = Page.IsVisible(this.process.Values);
1177 List<KycFieldState> FieldStates = new List<KycFieldState>();
1178 foreach (ObservableKycField Field in Page.AllFields)
1179 {
1180 if (!Field.IsVisible) continue;
1181 string? Value = string.IsNullOrWhiteSpace(Field.StringValue) ? null : Field.StringValue;
1182 FieldStates.Add(new KycFieldState(Field.Id, Field.IsValid, Value));
1183 }
1184 foreach (KycSection Section in Page.AllSections)
1185 {
1186 if (!Section.IsVisible) continue;
1187 foreach (ObservableKycField Field in Section.AllFields)
1188 {
1189 if (!Field.IsVisible) continue;
1190 string? Value = string.IsNullOrWhiteSpace(Field.StringValue) ? null : Field.StringValue;
1191 FieldStates.Add(new KycFieldState(Field.Id, Field.IsValid, Value));
1192 }
1193 }
1194 PageStates.Add(new KycPageState(Page.Id, IsVisible, FieldStates));
1195 }
1196 return new KycProcessState(PageStates, this.navigation, this.applicationSent);
1197 }
1198
1199 public async Task ApplyApplicationReviewAsync(ApplicationReview review)
1200 {
1201 this.ErrorDescription = review.Message;
1202 this.HasErrorDescription = !string.IsNullOrWhiteSpace(review.Message);
1203 if (this.kycReference is not null)
1204 await this.kycService.ApplyApplicationReviewAsync(this.kycReference, review);
1205 KycInvalidation.ApplyInvalidations(this.process, review, this.ErrorDescription);
1206 this.UpdateReviewIndicators();
1207 await this.BuildMappedValuesAsync();
1208 }
1209
1210 [RelayCommand(CanExecute = nameof(ShouldShowUnvalidatedBanner))]
1211 private async Task RemovePendingAndResubmitAsync()
1212 {
1213 string Confirmation = ServiceRef.Localizer[nameof(AppResources.KycConfirmRemovePending), false];
1214 if (!await AreYouSure(Confirmation))
1215 return;
1216
1217 if (!await this.RevokeCurrentApplicationAsync(false))
1218 return;
1219
1220 if (!await this.ClearUnvalidatedFieldsAsync())
1221 return;
1222
1223 if (this.process is not null && this.kycReference is not null)
1224 {
1225 try
1226 {
1227 await this.kycService.FlushSnapshotAsync(this.kycReference, this.process, this.navigation, this.Progress, this.CurrentPage?.Id);
1228 }
1229 catch (Exception ex)
1230 {
1231 ServiceRef.LogService.LogException(ex);
1232 }
1233 }
1234
1235 await this.BuildMappedValuesAsync();
1236 await this.ExecuteApplyAsync();
1237 }
1238
1239 private async Task<bool> ClearUnvalidatedFieldsAsync()
1240 {
1241 if (this.process is null || this.kycReference?.ApplicationReview is null)
1242 return false;
1243
1244 ApplicationReview review = this.kycReference.ApplicationReview;
1245 HashSet<string> mappingKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
1246 foreach (string Claim in review.UnvalidatedClaims ?? Array.Empty<string>())
1247 {
1248 if (!string.IsNullOrWhiteSpace(Claim))
1249 mappingKeys.Add(Claim.Trim());
1250 }
1251 foreach (string Photo in review.UnvalidatedPhotos ?? Array.Empty<string>())
1252 {
1253 if (!string.IsNullOrWhiteSpace(Photo))
1254 {
1255 string trimmed = Photo.Trim();
1256 mappingKeys.Add(trimmed);
1257 string? baseName = Path.GetFileNameWithoutExtension(trimmed);
1258 if (!string.IsNullOrWhiteSpace(baseName))
1259 mappingKeys.Add(baseName.Trim());
1260 }
1261 }
1262
1263 if (mappingKeys.Count == 0)
1264 return false;
1265
1266 bool cleared = this.ClearFieldsForMappings(mappingKeys);
1267
1268 if (!cleared)
1269 return false;
1270
1271 this.mappedValues = this.mappedValues
1272 .Where(p => !mappingKeys.Contains(p.Name ?? string.Empty))
1273 .ToList();
1274 this.attachments = this.attachments
1275 .Where(a =>
1276 {
1277 string fileName = a.FileName ?? string.Empty;
1278 string baseName = Path.GetFileNameWithoutExtension(fileName) ?? fileName;
1279 return !mappingKeys.Contains(fileName.Trim()) && !mappingKeys.Contains(baseName.Trim());
1280 })
1281 .ToList();
1282
1283 review.UnvalidatedClaims = Array.Empty<string>();
1284 review.UnvalidatedPhotos = Array.Empty<string>();
1285
1286 await this.kycService.ApplyApplicationReviewAsync(this.kycReference, review);
1287 this.UpdateReviewIndicators();
1288 return true;
1289 }
1290
1291 private bool ClearFieldsForMappings(HashSet<string> mappingKeys)
1292 {
1293 if (this.process is null || mappingKeys.Count == 0)
1294 return false;
1295
1296 bool cleared = false;
1297 foreach (KycPage Page in this.process.Pages)
1298 {
1299 cleared |= this.ClearFields(Page.AllFields, mappingKeys);
1300
1301 foreach (KycSection Section in Page.AllSections)
1302 {
1303 cleared |= this.ClearFields(Section.AllFields, mappingKeys);
1304 }
1305 }
1306 return cleared;
1307 }
1308
1309 private async Task<bool> RevokeCurrentApplicationAsync(bool requireConfirmation)
1310 {
1311 LegalIdentity? Application = ServiceRef.TagProfile.IdentityApplication;
1312 if (Application is null)
1313 {
1314 this.ApplicationSentPublic = false;
1315 this.applicationSent = false;
1316 this.peerReviewServices = null;
1317 this.HasFeaturedPeerReviewers = false;
1318 if (this.kycReference is not null)
1319 await this.kycService.ClearSubmissionAsync(this.kycReference);
1320 return true;
1321 }
1322
1323 if (requireConfirmation && !await AreYouSure(ServiceRef.Localizer[nameof(AppResources.AreYouSureYouWantToRevokeTheCurrentIdApplication)]))
1324 return false;
1325
1327 if (!await Auth.AuthenticateUserAsync(AuthenticationPurpose.RevokeApplication, true))
1328 return false;
1329
1330 try
1331 {
1332 await ServiceRef.XmppService.ObsoleteLegalIdentity(Application.Id);
1333 await ServiceRef.TagProfile.SetIdentityApplication(null, true);
1334 this.ApplicationSentPublic = false;
1335 this.applicationSent = false;
1336 this.peerReviewServices = null;
1337 this.HasFeaturedPeerReviewers = false;
1338 if (this.kycReference is not null)
1339 await this.kycService.ClearSubmissionAsync(this.kycReference);
1340 await this.BuildMappedValuesAsync();
1341 int AnchorAfterRevoke = this.currentPageIndex >= 0 ? this.currentPageIndex : this.navigation.AnchorPageIndex;
1342 this.navigation = this.navigation with { State = KycFlowState.Summary, AnchorPageIndex = AnchorAfterRevoke, CurrentPageIndex = AnchorAfterRevoke >= 0 ? AnchorAfterRevoke : this.navigation.CurrentPageIndex };
1343 this.SetEditingFromSummary(false);
1344 this.NotifyNavigationChanged();
1345 return true;
1346 }
1347 catch (Exception Ex)
1348 {
1349 ServiceRef.LogService.LogException(Ex);
1350 await ServiceRef.UiService.DisplayException(Ex);
1351 return false;
1352 }
1353 }
1354
1355 private bool ClearFields(IEnumerable<ObservableKycField> fields, HashSet<string> mappingKeys)
1356 {
1357 bool cleared = false;
1358 foreach (ObservableKycField Field in fields)
1359 {
1360 if (!Field.Mappings.Any(m => mappingKeys.Contains(m.Key)))
1361 continue;
1362
1363 if (!string.IsNullOrWhiteSpace(Field.StringValue))
1364 {
1365 Field.StringValue = null;
1366 if (this.process is not null && this.process.Values.ContainsKey(Field.Id))
1367 this.process.Values[Field.Id] = null;
1368 cleared = true;
1369 }
1370 }
1371 return cleared;
1372 }
1373
1374 private void UpdateReviewIndicators()
1375 {
1376 ApplicationReview? review = this.kycReference?.ApplicationReview;
1377 if (review is null)
1378 {
1379 this.UnvalidatedSummaryText = string.Empty;
1380 this.InvalidatedItems.Clear();
1381 this.UnvalidatedItems.Clear();
1382 }
1383 else
1384 {
1385 int claimCount = review.UnvalidatedClaims?.Length ?? 0;
1386 int photoCount = review.UnvalidatedPhotos?.Length ?? 0;
1387 if (claimCount == 0 && photoCount == 0)
1388 {
1389 this.UnvalidatedSummaryText = string.Empty;
1390 }
1391 else
1392 {
1393 this.UnvalidatedSummaryText = ServiceRef.Localizer["KycUnvalidatedWarningDescription", false, claimCount, photoCount];
1394 }
1395
1396 this.InvalidatedItems.Clear();
1397 foreach (ApplicationReviewClaimDetail detail in review.InvalidClaimDetails ?? Array.Empty<ApplicationReviewClaimDetail>())
1398 {
1399 if (detail is null)
1400 continue;
1401 string label = this.ResolveDisplayLabel(detail.Claim, detail.DisplayName);
1402 string reason = string.IsNullOrWhiteSpace(detail.Reason) ? string.Empty : $" — {detail.Reason}";
1403 this.InvalidatedItems.Add($"{label}{reason}");
1404 }
1405 foreach (ApplicationReviewPhotoDetail Detail in review.InvalidPhotoDetails ?? Array.Empty<ApplicationReviewPhotoDetail>())
1406 {
1407 if (Detail is null)
1408 continue;
1409 string Label = this.ResolveDisplayLabel(null, Detail.DisplayName);
1410 string Reason = string.IsNullOrWhiteSpace(Detail.Reason) ? string.Empty : $" — {Detail.Reason}";
1411 this.InvalidatedItems.Add($"{Label}{Reason}");
1412 }
1413
1414 this.UnvalidatedItems.Clear();
1415 foreach (string claim in review.UnvalidatedClaims ?? Array.Empty<string>())
1416 {
1417 if (string.IsNullOrWhiteSpace(claim))
1418 continue;
1419 string label = this.ResolveDisplayLabel(claim.Trim(), claim.Trim());
1420 this.UnvalidatedItems.Add(label);
1421 }
1422 foreach (string photo in review.UnvalidatedPhotos ?? Array.Empty<string>())
1423 {
1424 if (string.IsNullOrWhiteSpace(photo))
1425 continue;
1426 string label = this.ResolveDisplayLabel(photo.Trim(), photo.Trim());
1427 this.UnvalidatedItems.Add(label);
1428 }
1429 }
1430
1431 this.OnPropertyChanged(nameof(this.HasUnvalidatedItems));
1432 this.OnPropertyChanged(nameof(this.ShouldShowUnvalidatedBanner));
1433 this.OnPropertyChanged(nameof(this.ShouldShowRejectionBanner));
1434 this.RemovePendingAndResubmitCommand?.NotifyCanExecuteChanged();
1435 }
1436
1437 private string ResolveDisplayLabel(string? MappingKey, string? Fallback)
1438 {
1439 string? FromMapping = string.IsNullOrWhiteSpace(MappingKey) ? null : this.FindLabelByMapping(MappingKey);
1440 if (!string.IsNullOrWhiteSpace(FromMapping))
1441 return FromMapping;
1442
1443 if (!string.IsNullOrWhiteSpace(Fallback))
1444 {
1445 string? FromLabel = this.FindLabelByDisplayName(Fallback);
1446 if (!string.IsNullOrWhiteSpace(FromLabel))
1447 return FromLabel;
1448 }
1449
1450 return Fallback ?? string.Empty;
1451 }
1452
1453 private string? FindLabelByMapping(string mappingKey)
1454 {
1455 foreach ((DisplayQuad Item, bool PreferValue) entry in this.IterateSummaryItems())
1456 {
1457 if (!string.IsNullOrWhiteSpace(entry.Item.Mapping) && entry.Item.Mapping.Equals(mappingKey, StringComparison.OrdinalIgnoreCase))
1458 return this.GetDisplayLabel(entry.Item, entry.PreferValue);
1459 }
1460
1461 return null;
1462 }
1463
1464 private string? FindLabelByDisplayName(string label)
1465 {
1466 foreach ((DisplayQuad Item, bool PreferValue) entry in this.IterateSummaryItems())
1467 {
1468 string candidate = this.GetDisplayLabel(entry.Item, entry.PreferValue);
1469 if (!string.IsNullOrWhiteSpace(candidate) && candidate.Equals(label, StringComparison.OrdinalIgnoreCase))
1470 return candidate;
1471 }
1472
1473 return null;
1474 }
1475
1476 private IEnumerable<(DisplayQuad Item, bool PreferValue)> IterateSummaryItems()
1477 {
1478 IEnumerable<(ObservableCollection<DisplayQuad>? Collection, bool PreferValue)> collections = new (ObservableCollection<DisplayQuad>?, bool)[]
1479 {
1480 (this.PersonalInformationSummary, false),
1481 (this.AddressInformationSummary, false),
1482 (this.AttachmentInformationSummary, true),
1483 (this.CompanyInformationSummary, false),
1484 (this.CompanyAddressSummary, false),
1485 (this.CompanyRepresentativeSummary, false)
1486 };
1487
1488 foreach ((ObservableCollection<DisplayQuad>? Collection, bool PreferValue) entry in collections)
1489 {
1490 if (entry.Collection is null)
1491 continue;
1492
1493 foreach (DisplayQuad item in entry.Collection)
1494 yield return (item, entry.PreferValue);
1495 }
1496 }
1497
1498 private string GetDisplayLabel(DisplayQuad item, bool preferValue)
1499 {
1500 if (item is null)
1501 return string.Empty;
1502
1503 if (preferValue && !string.IsNullOrWhiteSpace(item.Value))
1504 return item.Value;
1505
1506 if (!string.IsNullOrWhiteSpace(item.Label))
1507 return item.Label;
1508
1509 return item.Value ?? string.Empty;
1510 }
1511 }
1512}
Image identifiers for branding.
Definition: Constants.cs:1083
static bool StartsWithIdScheme(string Url)
Checks if the specified code starts with the IoT ID scheme.
Definition: Constants.cs:231
static ? string RemoveScheme(string Url)
Removes the URI Schema from an URL.
Definition: Constants.cs:272
const string IotId
The IoT ID URI Scheme (iotid)
Definition: Constants.cs:153
const string BirthYear
Birth Year
Definition: Constants.cs:454
const string Phone
Phone number
Definition: Constants.cs:524
const string EMail
e-Mail address
Definition: Constants.cs:529
const string BirthMonth
Birth Month
Definition: Constants.cs:449
A set of never changing property constants and helpful values.
Definition: Constants.cs:24
A strongly-typed resource class, for looking up localized strings, etc.
static string AreYouSureYouWantToRevokeTheCurrentIdApplication
Looks up a localized string similar to Are you sure you want to revoke the current ID application?...
static string CouldYouPleaseReviewMyIdentityInformation
Looks up a localized string similar to Could you please review my identity information?...
static string KycConfirmRemovePending
Looks up a localized string similar to This will revoke your current application, remove the pending ...
static string YourLegalIdentityHasBeenCompromised
Looks up a localized string similar to Your identity has been marked compromised. You have therefore ...
static string RequestReview
Looks up a localized string similar to Request review.
static string SelectServiceProviderPeerReview
Looks up a localized string similar to Select where to send the review request..
static string YourLegalIdentityHasBeenObsoleted
Looks up a localized string similar to Your identity has been marked obsolete. You have therefore bee...
static string QrPageTitleScanPeerId
Looks up a localized string similar to Scan the peers ID.
static string YourApplicationWasRejected
Looks up a localized string similar to Your application was rejected..
static string APetitionHasBeenSentToYourPeer
Looks up a localized string similar to A petition has been sent to your peer.
static string AreYouSureYouWantToSendThisIdApplication
Looks up a localized string similar to Are you sure you want to send a new ID Application?...
static string PetitionSent
Looks up a localized string similar to Petition sent.
static string Ok
Looks up a localized string similar to OK.
static string Rejected
Looks up a localized string similar to Rejected.
static string Kyc_Exit
Looks up a localized string similar to Are you sure you would like to leave the Identity Application ...
static string ErrorTitle
Looks up a localized string similar to An error has occurred.
Contains additional data about an invalid claim.
string DisplayName
Localized display name for the claim.
string Reason
The textual reason explaining the invalidation.
Captures the result of an application review returned from backend services.
ApplicationReviewPhotoDetail[] InvalidPhotoDetails
Optional details about invalid photos.
string[] UnvalidatedPhotos
Photos still pending validation.
string Message
The localized or raw message describing the review result.
string[] UnvalidatedClaims
Claims still pending validation.
ApplicationReviewClaimDetail[] InvalidClaimDetails
Optional details about invalid claims.
Contains additional data about an invalid photo.
Represents a displayable set of three related strings: a label, a value, and an optional mapping.
Contains a local reference to a KYC process.
Definition: KycReference.cs:22
string? LastVisitedPageId
Last visited page identifier to support resuming.
Definition: KycReference.cs:93
ApplicationReview? ApplicationReview
Latest backend review for this application, if any.
IdentityState? CreatedIdentityState
Last known state of the created identity (if any), for quick status tagging offline.
Definition: KycReference.cs:81
string LastVisitedMode
Last visited mode: "Form" or "Summary". Default is "Form".
Definition: KycReference.cs:99
string? CreatedIdentityId
The legal ID of the created identity (if any).
Definition: KycReference.cs:75
async Task< KycProcess?> ToProcess(string? lang=null)
Creates a KycProcess from this reference, populating its fields.
string Text
Gets the localized text for the current UI culture, or an appropriate fallback.
Mapping from a field value to an identity property, including optional transform pipeline.
Definition: KycMapping.cs:9
Represents a page in a KYC process containing fields and sections.
Definition: KycPage.cs:20
ObservableCollection< KycSection > AllSections
Gets all sections contained in the page.
Definition: KycPage.cs:64
string Id
Gets the page identifier.
Definition: KycPage.cs:34
void UpdateVisibilities(IDictionary< string, string?> Values)
Updates visibility flags for fields and sections based on current values.
Definition: KycPage.cs:78
KycLocalizedText? Description
Gets or sets the localized page description.
Definition: KycPage.cs:42
ReadOnlyObservableCollection< KycSection > VisibleSections
Gets the read-only collection of visible sections in the page.
Definition: KycPage.cs:72
KycLocalizedText? Title
Gets or sets the localized page title.
Definition: KycPage.cs:38
bool IsVisible(IDictionary< string, string?> Values)
Gets a value indicating whether the page is visible based on current values and its condition.
Definition: KycPage.cs:98
ObservableCollection< ObservableKycField > AllFields
Gets all fields directly contained in the page (excluding sections).
Definition: KycPage.cs:51
Represents a parsed KYC process with pages, fields, and current values.
Definition: KycProcess.cs:11
void ClearValidation()
Clears validation state (error messages and flags) across all fields.
Definition: KycProcess.cs:42
ObservableCollection< KycPage > Pages
Gets the collection of pages in the KYC process.
Definition: KycProcess.cs:26
void Initialize()
Initializes page value-change notifications.
Definition: KycProcess.cs:31
IDictionary< string, string?> Values
Gets a modifiable dictionary of field values keyed by field identifier.
Definition: KycProcess.cs:21
bool HasMapping(string Mapping)
Determines if the process contains a mapping for a specific identity property.
Definition: KycProcess.cs:73
The base KYC field model. Use as-is for generic fields, or subclass for custom logic.
string Id
The unique identifier for the field.
abstract ? string StringValue
Gets or sets the value as a string, parsing or formatting as needed for the field type.
ObservableCollection< KycMapping > Mappings
Mappings to apply output.
Base class that references services in the app.
Definition: ServiceRef.cs:43
static IServiceProvider Provider
The service provider for the app. This is set before the app is started, and will be used to resolve ...
Definition: ServiceRef.cs:48
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
static INetworkService NetworkService
Network service.
Definition: ServiceRef.cs:226
static IUiService UiService
Service serializing and managing UI-related tasks.
Definition: ServiceRef.cs:130
static INavigationService NavigationService
The navigation service for navigating between pages.
Definition: ServiceRef.cs:178
static IAttachmentCacheService AttachmentCacheService
AttachmentCache service.
Definition: ServiceRef.cs:274
static ITagProfile TagProfile
TAG Profile service.
Definition: ServiceRef.cs:202
static IReportingStringLocalizer Localizer
Localization service
Definition: ServiceRef.cs:370
static IXmppService XmppService
The XMPP service for XMPP communication.
Definition: ServiceRef.cs:190
static IPlatformSpecific PlatformSpecific
Localization service
Definition: ServiceRef.cs:383
An base class holding page specific navigation parameters.
Provides a data-binding friendly mechanism to manage and report the status of asynchronous operations...
Simple asynchronous reentrancy guard that ensures a critical async section is not executed concurrent...
Represents representing a peer review from a peer, by scanning their QR-code.
A base class for all view models, inheriting from the BindableObject. NOTE: using this class requir...
Navigation arguments for the KYC process page. Carries the KycReference to edit/continue.
Orchestrates the interactive KYC flow, covering navigation, validation, summary projection,...
override async Task OnInitializeAsync()
Method called when view is initialized for the first time. Use this method to implement registration ...
override async Task GoBack()
Method called when user wants to navigate to the previous screen.
override Task OnDisposeAsync()
Method called when the view is disposed, and will not be used more. Use this method to unregister eve...
bool ShowBottomBar
Gets a value indicating whether the bottom navigation bar should be shown. Hidden when the applicatio...
bool IsKeyboardVisible
Gets a value indicating whether any on-screen keyboard is currently visible.
void OnKeyboardInsetChanged(Services.UI.KeyboardInsetChangedEventArgs args)
Receives keyboard inset change notifications from the shell.
Contains a reference to an attachment assigned to a legal object.
Definition: Attachment.cs:10
string Url
URL to retrieve attachment, if provided.
Definition: Attachment.cs:66
Event arguments for callback methods to ID Application attributes queries.
int NrReviewers
Number of peer reviewers required to get an ID approved using peer review.
bool PeerReview
If peer-review is allowed as a mechanism to approve ID applications.
Contains information about a service provider.
string? GetDeviceId()
Gets the ID of the device
void HideKeyboard()
Force hide the keyboard
Service for loading KYC processes, performing validation, building identity artifacts and managing pe...
Definition: IKycService.cs:17
Represents a component that consumes keyboard inset updates manually.
Interface for information about a service provider.
Definition: ImplTypes.g.cs:58
sealed record KycProcessState(IReadOnlyList< KycPageState > Pages, KycNavigationSnapshot Navigation, bool ApplicationSent)
Root aggregate-like snapshot passed through pure functions.
Definition: KycDomain.cs:45
KycFlowState
High-level flow states for the KYC process UI.
Definition: KycDomain.cs:7
sealed record KycPageState(string Id, bool IsVisible, IReadOnlyList< KycFieldState > FieldStates)
Immutable page state abstraction for domain calculations (validation, navigation decisions).
sealed record KycFieldState(string Id, bool IsValid, string? Value)
Immutable field state used by pure domain operations.
sealed record KycNavigationSnapshot(int CurrentPageIndex, int AnchorPageIndex, KycFlowState State)
Navigation snapshot capturing minimal mutable UI navigation state.
class Photo(byte[] Binary, int Rotation, Attachment? Attachment)
Class containing information about a photo.
Definition: Photo.cs:10
AuthenticationPurpose
Purpose for requesting the user to authenticate itself.
IdentityState
Lists recognized legal identity states.
Reason
Reason a token is not valid.
Definition: JwtFactory.cs:15
Definition: App.xaml.cs:4