Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ApplicationsViewModel.cs
1using System.Collections.ObjectModel;
2using System.Globalization;
3using System.Linq;
4using System.Threading;
5using System.Threading.Tasks;
6using CommunityToolkit.Mvvm.ComponentModel;
7using CommunityToolkit.Mvvm.Input;
8using EDaler;
9using Microsoft.Maui.ApplicationModel;
10using NeuroAccessMaui;
21using NeuroAccessMaui.UI.MVVM; // ObservableTask
35using NeuroFeatures;
41
43{
47 public partial class ApplicationsViewModel : XmppViewModel
48 {
49 private const int AvailableTemplatesPageSize = 10; // Unified page size for available applications pagination
50 public ObservableCollection<KycReference> Applications { get; } = new();
51 public ObservableCollection<KycApplicationTemplate> AvailableApplications { get; } = new();
52 private KycApplicationPage? availableApplicationsPage;
53
54 public string BannerUriLight => ServiceRef.ThemeService.GetImageUri(Constants.Branding.BannerSmallLight);
55 public string BannerUriDark => ServiceRef.ThemeService.GetImageUri(Constants.Branding.BannerSmallDark);
56 public string BannerUri =>
57 Application.Current?.UserAppTheme switch
58 {
59 AppTheme.Dark => this.BannerUriDark,
60 AppTheme.Light => this.BannerUriLight,
61 _ => this.BannerUriLight
62 } ?? this.BannerUriLight;
63 // Single current application (0 or 1)
64 [ObservableProperty]
65 private KycReference? currentApplication;
66
67 [ObservableProperty]
68 private bool hasMoreAvailableTemplates;
69
70 public bool HasCurrentApplication => this.CurrentApplication is not null;
71
72 public bool CanLoadMoreAvailableApplications => this.CanExecuteCommands && this.HasMoreAvailableTemplates;
73
74 // Expose loader state if you want to bind spinners/errors in XAML
75 public ObservableTask<int> Loader { get; init; }
76 public ObservableTask<int> AvailableLoader { get; init; }
77 public bool IsLoading => this.Loader.IsRunning;
78 public string? LoadError => this.Loader.ErrorMessage;
79
80 public bool HasApplications => this.Applications.Count > 0; // legacy, not used by current UI
81
82 public bool ShowProgressBar => this.CurrentApplication is not null
83 && (this.CurrentApplication.CreatedIdentityState is null
84 || this.CurrentApplication.CreatedIdentityState == IdentityState.Created);
85
86 partial void OnHasMoreAvailableTemplatesChanged(bool value)
87 {
88 this.LoadMoreAvailableApplicationsCommand.NotifyCanExecuteChanged();
89 }
90
95 : base()
96 {
97 // Disable auto-start to avoid immediate generation superseding reload in OnAppearing.
98 this.Loader = new ObservableTaskBuilder()
99 .Named("LoadApplications")
100 .AutoStart(false)
101 .WithPolicy(Policies.Retry(3, (attempt, ex) => TimeSpan.FromMilliseconds(250 * attempt * attempt)))
102 .WithTelemetry(new LoggerTelemetry())
103 .UseTaskRun(false)
104 .Run(this.LoadApplicationsAsync)
105 .Build(this.CreateNewApplicationCommand, this.OpenApplicationCommand);
106
107 this.AvailableLoader = new ObservableTaskBuilder()
108 .Named("LoadAvailableApplications")
109 .AutoStart(false)
110 .WithPolicy(Policies.Retry(3, (attempt, ex) => TimeSpan.FromMilliseconds(250 * attempt * attempt)))
111 .WithTelemetry(new LoggerTelemetry())
112 .UseTaskRun(false)
113 .Run(this.LoadAvailableApplicationsAsync)
114 .Build(this.CreateNewApplicationCommand, this.LoadMoreAvailableApplicationsCommand);
115 }
116
117 public override async Task OnInitializeAsync()
118 {
119 this.IdentityApplicationSent = ServiceRef.TagProfile.IdentityApplication is not null;
120
121 this.HasLegalIdentity = ServiceRef.TagProfile.LegalIdentity is not null &&
122 ServiceRef.TagProfile.LegalIdentity.State == IdentityState.Approved;
123
124 ServiceRef.XmppService.IdentityApplicationChanged += this.XmppService_IdentityApplicationChanged;
125 ServiceRef.XmppService.LegalIdentityChanged += this.XmppService_LegalIdentityChanged;
126 ServiceRef.TagProfile.OnPropertiesChanged += this.TagProfile_OnPropertiesChanged;
127
128 await base.OnInitializeAsync();
129
130 this.NotifyCommandsCanExecuteChanged();
131 }
132
133 public override Task OnDisposeAsync()
134 {
135 ServiceRef.XmppService.IdentityApplicationChanged -= this.XmppService_IdentityApplicationChanged;
136 ServiceRef.XmppService.LegalIdentityChanged -= this.XmppService_LegalIdentityChanged;
137 ServiceRef.TagProfile.OnPropertiesChanged -= this.TagProfile_OnPropertiesChanged;
138
139 // If desired, cancel any in-flight load.
140 this.Loader.Cancel();
141
142 return base.OnDisposeAsync();
143 }
144
145 private Task XmppService_IdentityApplicationChanged(object? Sender, LegalIdentityEventArgs e)
146 {
147 MainThread.BeginInvokeOnMainThread(() =>
148 {
149 this.IdentityApplicationSent = ServiceRef.TagProfile.IdentityApplication is not null;
150 });
151
152 return Task.CompletedTask;
153 }
154
155 private Task XmppService_LegalIdentityChanged(object Sender, LegalIdentityEventArgs e)
156 {
157 MainThread.BeginInvokeOnMainThread(() =>
158 {
159 this.HasLegalIdentity = ServiceRef.TagProfile.LegalIdentity is not null &&
160 ServiceRef.TagProfile.LegalIdentity.State == IdentityState.Approved;
161 });
162
163 return Task.CompletedTask;
164 }
165
166 private void TagProfile_OnPropertiesChanged(object? sender, EventArgs e)
167 {
168 // no-op for now
169 }
170
171 public override async Task OnAppearingAsync()
172 {
173 await base.OnAppearingAsync();
174
175 this.Loader.Run();
176 this.AvailableLoader.Run();
177
178 // Page is not correctly updated if changes happened when viewing a sub-view. Fix by resending notification.
179 bool IdApplicationSent = ServiceRef.TagProfile.IdentityApplication is not null;
180 if (this.IdentityApplicationSent != IdApplicationSent)
181 this.IdentityApplicationSent = IdApplicationSent;
182 else
183 this.OnPropertyChanged(nameof(this.IdentityApplicationSent));
184 }
185
187 protected override Task XmppService_ConnectionStateChanged(object? Sender, XmppState NewState)
188 {
189 return MainThread.InvokeOnMainThreadAsync(async () =>
190 {
191 await base.XmppService_ConnectionStateChanged(Sender, NewState);
192 this.NotifyCommandsCanExecuteChanged();
193 });
194 }
195
197 public override void SetIsBusy(bool IsBusy)
198 {
199 base.SetIsBusy(IsBusy);
200 this.NotifyCommandsCanExecuteChanged();
201 }
202
203 private void NotifyCommandsCanExecuteChanged()
204 {
205 this.CreateNewApplicationCommand.NotifyCanExecuteChanged();
206 this.OpenApplicationCommand.NotifyCanExecuteChanged();
207 this.LoadMoreAvailableApplicationsCommand.NotifyCanExecuteChanged();
208 }
209
210 #region Properties
211
215 public bool CanExecuteCommands => !this.IsBusy;
216
220 [ObservableProperty]
221 private bool identityApplicationSent;
222
226 [ObservableProperty]
227 private bool hasLegalIdentity;
228
229 #endregion
230
231 #region Commands
232
233 [RelayCommand(CanExecute = nameof(CanExecuteCommands))]
234 private async Task CreateNewApplication(KycApplicationTemplate? template)
235 {
236 try
237 {
238 KycFieldValue[]? PreviousFields = null;
239
240 if (this.CurrentApplication is not null)
241 {
242 try
243 {
244 if (this.CurrentApplication.Fields is not null)
245 {
246 PreviousFields = this.CurrentApplication.Fields
247 .Select(F => new KycFieldValue(F.FieldId, F.Value))
248 .ToArray();
249 }
250
251 if (!string.IsNullOrEmpty(this.CurrentApplication.CreatedIdentityId))
252 {
253 LegalIdentity Identity = await ServiceRef.XmppService.GetLegalIdentity(this.CurrentApplication.CreatedIdentityId);
254 if (Identity.State == IdentityState.Created)
255 {
257
258 if (!await Auth.AuthenticateUserAsync(AuthenticationPurpose.RevokeApplication, true))
259 return;
260
261 await ServiceRef.XmppService.ObsoleteLegalIdentity(Identity.Id);
262 }
263 }
264
265 await ServiceRef.TagProfile.SetIdentityApplication(null, true);
266 await Database.Delete(this.CurrentApplication);
267 await Database.Provider.Flush();
268 }
269 catch (Exception Ex)
270 {
271 ServiceRef.LogService.LogException(Ex);
272 }
273 }
274 else
275 {
276 // No current application: try to find the latest previous draft and reuse its fields
277 try
278 {
279 IEnumerable<KycReference> All = await Database.Find<KycReference>();
280 KycReference? LatestWithFields = All
281 .OrderByDescending(r => r.UpdatedUtc)
282 .FirstOrDefault(r => r.Fields is not null && r.Fields.Length > 0);
283
284 if (LatestWithFields?.Fields is not null)
285 {
286 PreviousFields = LatestWithFields.Fields
287 .Select(F => new KycFieldValue(F.FieldId, F.Value))
288 .ToArray();
289 }
290 }
291 catch (Exception Ex)
292 {
293 ServiceRef.LogService.LogException(Ex);
294 }
295 }
296
297 string Language = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
298 KycApplicationTemplate? TemplateToUse = template ?? this.AvailableApplications.FirstOrDefault();
299 KycReference Ref = await ServiceRef.KycService.LoadKycReferenceAsync(Language, TemplateToUse);
300
301 await ServiceRef.KycService.PrepareReferenceForNewApplicationAsync(Ref, Language, PreviousFields);
302
304 }
305 catch (Exception Ex)
306 {
307 ServiceRef.LogService.LogException(Ex);
309 }
310 }
311
312 [RelayCommand(CanExecute = nameof(CanLoadMoreAvailableApplications))]
313 private async Task LoadMoreAvailableApplications()
314 {
315 if (!this.CanLoadMoreAvailableApplications)
316 return;
317
318 try
319 {
320 if (this.availableApplicationsPage is null || string.IsNullOrWhiteSpace(this.availableApplicationsPage.NextAfter))
321 return;
322
323 string Language = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
324 string After = this.availableApplicationsPage.NextAfter;
325 KycApplicationPage Page = await ServiceRef.KycService.LoadKycApplicationsPageAsync(After, null, null, AvailableTemplatesPageSize, Language).ConfigureAwait(false);
326
327 await MainThread.InvokeOnMainThreadAsync(() =>
328 {
329 foreach (KycApplicationTemplate Template in Page.Templates)
330 {
331 bool Exists = this.AvailableApplications.Any(existing => TemplatesEqual(existing, Template));
332 if (!Exists)
333 this.AvailableApplications.Add(Template);
334 }
335
336 this.availableApplicationsPage = Page;
337 // Only allow more if we received a full page, the page indicates more, and it's not fallback.
338 this.HasMoreAvailableTemplates = !Page.UsedFallback && Page.Templates.Count == AvailableTemplatesPageSize && Page.HasMoreAfter;
339 });
340 }
341 catch (Exception Ex)
342 {
343 ServiceRef.LogService.LogException(Ex);
345 }
346 }
347
348 [RelayCommand]
349 private async Task RemoveApplication(KycReference Item)
350 {
351 try
352 {
353 if (Item is null)
354 return;
355
356 await Database.Delete(Item);
357 await Database.Provider.Flush();
358
359 this.Loader.Refresh();
360 }
361 catch (Exception Ex)
362 {
363 ServiceRef.LogService.LogException(Ex);
364 }
365 }
366
367 [RelayCommand(CanExecute = nameof(CanExecuteCommands))]
368 private async Task OpenApplication(KycReference Item)
369 {
370 try
371 {
372 if (Item is null)
373 return;
374
375 if (Item.CreatedIdentityState is not null)
376 {
378 if (State == IdentityState.Approved)
379 {
380 LegalIdentity? Identity = await ServiceRef.XmppService.GetLegalIdentity(Item.CreatedIdentityId);
381 // Preview in review/approved identity
382 await ServiceRef.NavigationService.GoToAsync(nameof(ViewIdentityPage), new ViewIdentityNavigationArgs(Identity));
383 }
384 else
385 {
386 // Rejected or other states: allow editing in KYC
388 }
389 }
390 else
391 {
392 // Open KYC process to resume
394 }
395 }
396 catch (Exception Ex)
397 {
398 ServiceRef.LogService.LogException(Ex);
400 }
401 }
402
403 // Optional: surface loader controls via your VM if you want to bind to buttons/gestures.
404 [RelayCommand]
405 private void RefreshApplications() => this.Loader.Refresh();
406
407 [RelayCommand]
408 private void ReloadApplications() => this.Loader.Reload();
409
410 [RelayCommand]
411 private void CancelLoading() => this.Loader.Cancel();
412
413 private static bool TemplatesEqual(KycApplicationTemplate first, KycApplicationTemplate second)
414 {
415 string? firstId = first.Source?.ItemId;
416 string? secondId = second.Source?.ItemId;
417
418 if (!string.IsNullOrEmpty(firstId) && !string.IsNullOrEmpty(secondId))
419 return string.Equals(firstId, secondId, System.StringComparison.Ordinal);
420
421 if (first.Source is null && second.Source is null &&
422 !string.IsNullOrEmpty(first.Reference.KycXml) &&
423 !string.IsNullOrEmpty(second.Reference.KycXml))
424 {
425 return string.Equals(first.Reference.KycXml, second.Reference.KycXml, System.StringComparison.Ordinal);
426 }
427
428 return false;
429 }
430
431 #endregion
432
433 #region Loading (refactored to ObservableTask)
434
439 private async Task LoadApplicationsAsync(TaskContext<int> ctx)
440 {
441 CancellationToken Ct = ctx.CancellationToken;
442 IProgress<int> Progress = ctx.Progress;
443
444 try
445 {
446 Progress.Report(0);
447
448 // Clear existing (UI thread)
449 await MainThread.InvokeOnMainThreadAsync(() =>
450 {
451 this.Applications.Clear();
452 this.OnPropertyChanged(nameof(this.HasApplications));
453 });
454
455 // 1) Drafts (local). We show at most one (latest) current application.
456 IEnumerable<KycReference> Refs = Array.Empty<KycReference>();
457 try
458 {
459 // Database.Find<T>() doesn't accept CT directly; ensure we respect CT around the call.
460 Ct.ThrowIfCancellationRequested();
461 Refs = await Database.Find<KycReference>();
462 }
463 catch (OperationCanceledException) { throw; }
464 catch (Exception Ex)
465 {
466 // Non-fatal; log and continue
467 ServiceRef.LogService.LogException(Ex);
468 }
469
470 Ct.ThrowIfCancellationRequested();
471
472 KycReference? Latest = Refs.OrderByDescending(r => r.UpdatedUtc).FirstOrDefault();
473
474 await MainThread.InvokeOnMainThreadAsync(() =>
475 {
476 this.Applications.Clear();
477 this.CurrentApplication = Latest;
478 if (Latest is not null)
479 this.Applications.Add(Latest);
480 });
481
482
483 Progress.Report(100);
484
485 // Final notify
486 await MainThread.InvokeOnMainThreadAsync(() =>
487 {
488 this.OnPropertyChanged(nameof(this.HasCurrentApplication));
489 this.OnPropertyChanged(nameof(this.HasApplications));
490 // If you want command states to react to loading completion:
491 this.NotifyCommandsCanExecuteChanged();
492 });
493 }
494 catch (OperationCanceledException)
495 {
496 // Let ObservableTask mark as Canceled; avoid extra UI updates here.
497 throw;
498 }
499 catch (Exception Ex)
500 {
501 // ObservableTask will capture/log, but keep behavior consistent.
502 ServiceRef.LogService.LogException(Ex);
503 throw;
504 }
505 }
506
507 private async Task LoadAvailableApplicationsAsync(TaskContext<int> ctx)
508 {
509 CancellationToken Ct = ctx.CancellationToken;
510 IProgress<int> Progress = ctx.Progress;
511
512 try
513 {
514 Progress.Report(0);
515
516 string Language = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
517 Ct.ThrowIfCancellationRequested();
518 KycApplicationPage Page = await ServiceRef.KycService.LoadKycApplicationsPageAsync(null, null, 0, AvailableTemplatesPageSize, Language, Ct).ConfigureAwait(false);
519
520 Ct.ThrowIfCancellationRequested();
521
522 await MainThread.InvokeOnMainThreadAsync(() =>
523 {
524 this.AvailableApplications.Clear();
525 foreach (KycApplicationTemplate Template in Page.Templates)
526 this.AvailableApplications.Add(Template);
527 this.availableApplicationsPage = Page;
528 // Only show Load More if full page, remote (not fallback), and server hints more.
529 this.HasMoreAvailableTemplates = !Page.UsedFallback && Page.Templates.Count == AvailableTemplatesPageSize && Page.HasMoreAfter;
530 });
531
532 Progress.Report(100);
533 }
534 catch (OperationCanceledException)
535 {
536 throw;
537 }
538 catch (Exception Ex)
539 {
540 ServiceRef.LogService.LogException(Ex);
541 throw;
542 }
543 }
544
545 private static string GetTextOrFallback(string key, string fallback)
546 {
547 Microsoft.Extensions.Localization.LocalizedString L = ServiceRef.Localizer[key, false];
548 return L.ResourceNotFound ? fallback : L.Value;
549 }
550
551 private static string GetIdentityStateText(IdentityState state)
552 {
553 Microsoft.Extensions.Localization.LocalizedString L = ServiceRef.Localizer["IdentityState_" + state.ToString(), false];
554 return L.ResourceNotFound ? state.ToString() : L.Value;
555 }
556
557 #endregion
558 }
559}
Image identifiers for branding.
Definition: Constants.cs:1083
A set of never changing property constants and helpful values.
Definition: Constants.cs:24
string ItemId
Gets the PubSub item identifier.
Represents a page of KYC application templates along with pagination metadata.
string? NextAfter
Gets the item identifier to use in an "after" query when requesting the next page.
bool UsedFallback
Gets a value indicating if the bundled fallback template was returned instead of remote data.
IReadOnlyList< KycApplicationTemplate > Templates
Gets the application templates on this page.
bool HasMoreAfter
Gets a value indicating whether more pages are likely available after the current one.
Represents a parsed KYC application template along with optional PubSub metadata.
KycReference Reference
Gets the parsed reference constructed from the template XML.
KycApplicationItem? Source
Gets the originating PubSub metadata, if available.
Contains a local reference to a KYC process.
Definition: KycReference.cs:22
IdentityState? CreatedIdentityState
Last known state of the created identity (if any), for quick status tagging offline.
Definition: KycReference.cs:81
string? CreatedIdentityId
The legal ID of the created identity (if any).
Definition: KycReference.cs:75
string? KycXml
XML describing the KYC process.
Definition: KycReference.cs:35
Represents a field value in a KYC process reference.
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 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 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
Basic telemetry that writes ObservableTask events to the app log.
Provides a data-binding friendly mechanism to manage and report the status of asynchronous operations...
void Cancel()
Cancels the running task.
void Run()
Start the configured task now (no CanExecute gating).
void Refresh()
Refreshes the current task. This cancels any running task and starts a new one using the stored facto...
The view model to bind to for when displaying the applications page.
override Task OnDisposeAsync()
Method called when the view is disposed, and will not be used more. Use this method to unregister eve...
override async Task OnAppearingAsync()
Method called when view is appearing on the screen.
override async Task OnInitializeAsync()
Method called when view is initialized for the first time. Use this method to implement registration ...
override Task XmppService_ConnectionStateChanged(object? Sender, XmppState NewState)
Listens to connection state changes from the XMPP server.
ApplicationsViewModel()
Creates an instance of the ApplicationsViewModel class.
A page to display when the user wants to view an identity.
Navigation arguments for the KYC process page. Carries the KycReference to edit/continue.
A view model that holds the XMPP state.
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static IDatabaseProvider Provider
Registered database provider.
Definition: Database.cs:59
static async Task Delete(object Object)
Deletes an object in the database.
Definition: Database.cs:1291
static Task< IEnumerable< object > > Find(string Collection, params string[] SortOrder)
Finds objects in a given collection.
Definition: Database.cs:238
Task< KycApplicationPage > LoadKycApplicationsPageAsync(string? After=null, string? Before=null, int? Index=null, int? Max=null, string? Lang=null, CancellationToken CancellationToken=default)
Loads a page of KYC application templates using PubSub pagination, with bundled fallback when remote ...
Task PrepareReferenceForNewApplicationAsync(KycReference Reference, string? Language, IReadOnlyList< KycFieldValue >? SeedFields)
Resets reference state and seeds optional field values for a fresh application session.
Task< KycReference > LoadKycReferenceAsync(string? Lang=null, KycApplicationTemplate? Template=null)
Loads (or creates) the persisted KYC reference and ensures process XML is available.
Task GoToAsync(string Route)
Navigates to the specified route and pushes the page onto the navigation stack.
Task DisplayException(Exception Exception, string? Title=null)
Displays an alert/message box to the user.
Task Flush()
Persists any pending changes.
Definition: ImplTypes.g.cs:58
AuthenticationPurpose
Purpose for requesting the user to authenticate itself.
IdentityState
Lists recognized legal identity states.
XmppState
State of XMPP connection.
Definition: XmppState.cs:7