Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
OnboardingViewModel.cs
1using System;
3using System.Collections.ObjectModel;
4using System.Threading.Tasks;
5using System.Xml;
6using CommunityToolkit.Mvvm.ComponentModel;
7using CommunityToolkit.Mvvm.Input;
20
22{
23 public partial class OnboardingViewModel : BaseViewModel
24 {
25 private sealed record StepDescriptor(OnboardingStep Step, Func<bool> Include, Func<bool> Skip);
26
27 private readonly Dictionary<OnboardingStep, BaseOnboardingStepViewModel> stepViewModels = new Dictionary<OnboardingStep, BaseOnboardingStepViewModel>();
28 private readonly Dictionary<OnboardingStep, StepDescriptor> descriptorMap = new Dictionary<OnboardingStep, StepDescriptor>();
29 private readonly List<StepDescriptor> descriptorOrder = new List<StepDescriptor>();
30 private readonly List<OnboardingStep> scenarioSequence = new List<OnboardingStep>();
31 private readonly HashSet<OnboardingStep> loggedSkips = new HashSet<OnboardingStep>();
32 private readonly HashSet<OnboardingStep> completedSteps = new HashSet<OnboardingStep>();
33 private List<OnboardingStep> activeSequence = new List<OnboardingStep>();
34 private readonly OnboardingNavigationArgs navigationArgs;
35 private readonly OnboardingScenario scenario;
36 private OnboardingTransferContext? transferContext;
37 private bool isUpdatingSelection;
38 private bool isCompleting;
39 private bool hasLoggedSequence;
40 private bool allStepsSkippable;
41
42 [ObservableProperty]
43 [NotifyPropertyChangedFor(nameof(IsSummaryStep))]
44 [NotifyPropertyChangedFor(nameof(IsOnWelcomeStep))]
45 private OnboardingStep currentStep;
46
47 [ObservableProperty]
48 private string selectedStateKey = string.Empty;
49
50 [ObservableProperty]
51 [NotifyCanExecuteChangedFor(nameof(GoBackCommand))]
52 private bool canGoBack;
53
54 [ObservableProperty]
55 private string headerTitle = string.Empty;
56
57 [ObservableProperty]
58 private ObservableCollection<LanguageInfo> languages = new ObservableCollection<LanguageInfo>(App.SupportedLanguages);
59
60 [ObservableProperty]
61 private LanguageInfo selectedLanguage = App.SelectedLanguage;
62
63 public OnboardingViewModel()
64 {
66 if (Args is null)
67 {
68 ServiceRef.LogService.LogWarning("Onboarding navigation arguments missing. Using defaults.");
69 Args = new OnboardingNavigationArgs();
70 }
71
72 this.navigationArgs = Args;
73 this.scenario = this.navigationArgs.Scenario;
74 ServiceRef.LogService.LogInformational("Onboarding scenario selected.",
75 new KeyValuePair<string, object?>("Scenario", this.scenario.ToString()));
76 this.InitializeDescriptors();
77 }
78
79 public bool IsSummaryStep => this.CurrentStep == OnboardingStep.Finalize;
80
81 public bool IsOnWelcomeStep => this.CurrentStep == OnboardingStep.Welcome;
82
83 public OnboardingScenario Scenario => this.scenario;
84
85 internal bool HasPendingTransfer => this.transferContext is not null;
86
87 internal bool PendingTransferIncludesLegalIdentity => this.transferContext?.HasLegalIdentity == true;
88
89 public void RegisterStep(BaseOnboardingStepViewModel stepViewModel)
90 {
91 if (this.stepViewModels.ContainsKey(stepViewModel.Step))
92 {
93 return;
94 }
95
96 this.stepViewModels[stepViewModel.Step] = stepViewModel;
97 stepViewModel.AttachCoordinator(this);
98 if (stepViewModel.Step == this.CurrentStep)
99 {
100 this.HeaderTitle = this.GetStepTitle(stepViewModel.Step);
101 }
102 }
103
104 internal Task ApplyTransferContextAsync(OnboardingTransferContext context)
105 {
106 this.transferContext = context;
107 ServiceRef.LogService.LogInformational("Transfer context applied.",
108 new KeyValuePair<string, object?>("HasLegalIdentity", context.HasLegalIdentity));
109 this.BuildActiveSequence();
110 return Task.CompletedTask;
111 }
112
113 internal async Task<bool> TryFinalizeTransferAsync(bool showBusyOverlay)
114 {
115 OnboardingTransferContext? context = this.transferContext;
116 if (context is null)
117 {
118 return false;
119 }
120
121 if (showBusyOverlay)
122 {
123 await MainThread.InvokeOnMainThreadAsync(() => this.SetIsBusy(true));
124 }
125
126 try
127 {
128 bool connected = await ConnectToAccountAsync(
129 context.AccountName,
130 context.Password,
131 context.PasswordMethod,
132 string.Empty,
133 context.LegalIdDefinition,
134 string.Empty).ConfigureAwait(false);
135
136 if (connected)
137 {
138 this.transferContext = null;
139 this.BuildActiveSequence();
140 }
141
142 return connected;
143 }
144 finally
145 {
146 if (showBusyOverlay)
147 {
148 await MainThread.InvokeOnMainThreadAsync(() => this.SetIsBusy(false));
149 }
150 }
151 }
152
163 internal static async Task<bool> ConnectToAccountAsync(string accountName, string password, string passwordMethod, string legalIdentityJid, XmlElement? legalIdDefinition, string pin)
164 {
165 ServiceRef.LogService.LogInformational("Attempting transferred account connection.",
166 new KeyValuePair<string, object?>("Account", accountName),
167 new KeyValuePair<string, object?>("Domain", ServiceRef.TagProfile.Domain));
168
169 try
170 {
171 async Task OnConnected(XmppClient client)
172 {
173 ServiceRef.LogService.LogInformational("XMPP account connected. Discovering identities.");
174 DateTime Now = DateTime.Now;
175 LegalIdentity? CreatedIdentity = null;
176 LegalIdentity? ApprovedIdentity = null;
177 bool ServiceDiscoverySucceeded = ServiceRef.TagProfile.NeedsUpdating() ? await ServiceRef.XmppService.DiscoverServices(client) : true;
178 ServiceRef.LogService.LogInformational($"Service discovery result: {ServiceDiscoverySucceeded}.");
179 if (ServiceDiscoverySucceeded && !string.IsNullOrEmpty(ServiceRef.TagProfile.LegalJid))
180 {
181 bool DestroyContractsClient = false;
182 if (!client.TryGetExtension(typeof(ContractsClient), out IXmppExtension Extension) || Extension is not ContractsClient contractsClient)
183 {
184 contractsClient = new ContractsClient(client, ServiceRef.TagProfile.LegalJid);
185 DestroyContractsClient = true;
186 ServiceRef.LogService.LogDebug("ContractsClient created for identity operations.");
187 }
188
189 try
190 {
191 if (legalIdDefinition is not null)
192 {
193 ServiceRef.LogService.LogInformational("Importing provided LegalId keys.");
194 await contractsClient.ImportKeys(legalIdDefinition);
195 }
196
197 LegalIdentity[] identities = await contractsClient.GetLegalIdentitiesAsync();
198 ServiceRef.LogService.LogInformational($"Fetched {identities.Length} identities.");
199 foreach (LegalIdentity identity in identities)
200 {
201 try
202 {
203 if ((string.IsNullOrEmpty(legalIdentityJid) || string.Compare(legalIdentityJid, identity.Id, StringComparison.OrdinalIgnoreCase) == 0) &&
204 identity.HasClientSignature && identity.HasClientPublicKey && identity.From <= Now && identity.To >= Now &&
205 (identity.State == IdentityState.Approved || identity.State == IdentityState.Created) && identity.ValidateClientSignature() && await contractsClient.HasPrivateKey(identity))
206 {
207 if (identity.State == IdentityState.Approved)
208 {
209 ApprovedIdentity = identity;
210 break;
211 }
212
213 CreatedIdentity ??= identity;
214 }
215 }
216 catch (Exception ex2)
217 {
218 ServiceRef.LogService.LogException(ex2);
219 }
220 }
221
222 LegalIdentity? SelectedIdentity = ApprovedIdentity ?? CreatedIdentity;
223 string SelectedId;
224 if (SelectedIdentity is not null)
225 {
226 ServiceRef.LogService.LogInformational($"Selected identity '{SelectedIdentity.Id}' (State={SelectedIdentity.State}).");
227 await ServiceRef.TagProfile.SetAccountAndLegalIdentity(accountName, client.PasswordHash, client.PasswordHashMethod, SelectedIdentity);
228 SelectedId = SelectedIdentity.Id;
230 }
231 else
232 {
233 ServiceRef.LogService.LogInformational("No identity selected; storing account only.");
235 SelectedId = string.Empty;
236 }
237
238 if (!string.IsNullOrEmpty(pin))
239 {
240 ServiceRef.LogService.LogInformational("Local PIN set from invitation.");
241 ServiceRef.TagProfile.LocalPassword = pin;
242 }
243
244 foreach (LegalIdentity identity in identities)
245 {
246 if (identity.Id == SelectedId)
247 continue;
248
249 switch (identity.State)
250 {
251 case IdentityState.Approved:
252 case IdentityState.Created:
253 ServiceRef.LogService.LogDebug($"Obsoleting identity '{identity.Id}'.");
254 await contractsClient.ObsoleteLegalIdentityAsync(identity.Id);
255 break;
256 }
257 }
258 }
259 finally
260 {
261 if (DestroyContractsClient)
262 {
263 ServiceRef.LogService.LogDebug("Disposing temporary ContractsClient.");
264 contractsClient.Dispose();
265 }
266 }
267 }
268 }
269
270 (string HostName, int PortNumber, bool IsIp) = await ServiceRef.NetworkService.LookupXmppHostnameAndPort(ServiceRef.TagProfile.Domain ?? string.Empty);
271 ServiceRef.LogService.LogInformational($"Connecting to XMPP account Host='{HostName}' Port={PortNumber} IsIp={IsIp}.");
272 (bool Succeeded, string? ErrorMessage, string[]? _) = await ServiceRef.XmppService.TryConnectAndConnectToAccount(
273 ServiceRef.TagProfile.Domain ?? string.Empty,
274 IsIp,
275 HostName,
276 PortNumber,
277 accountName,
278 password,
279 passwordMethod,
281 typeof(App).Assembly,
282 OnConnected);
283 ServiceRef.LogService.LogInformational($"XMPP connect result: {(Succeeded ? "Succeeded" : "Failed")} Error='{ErrorMessage}'.");
284 if (!Succeeded && !string.IsNullOrEmpty(ErrorMessage))
285 ServiceRef.LogService.LogWarning(ErrorMessage);
286 return Succeeded;
287 }
288 catch (Exception ex)
289 {
290 ServiceRef.LogService.LogException(ex);
291 }
292
293 return false;
294 }
295
296 internal OnboardingStep? GetNextActiveStep(OnboardingStep step)
297 {
298 this.BuildActiveSequence();
299 OnboardingStep? Candidate = this.FindStepInDirection(step, NavigationDirection.Forward);
300 if (!Candidate.HasValue)
301 {
302 return null;
303 }
304
305 return this.ResolveStepWithSkipping(Candidate.Value, NavigationDirection.Forward, false);
306 }
307
308 internal void MarkStepCompleted(OnboardingStep step)
309 {
310 if (!this.completedSteps.Add(step))
311 {
312 return;
313 }
314
315 ServiceRef.LogService.LogInformational("Onboarding step marked as completed.",
316 new KeyValuePair<string, object?>("Step", step.ToString()),
317 new KeyValuePair<string, object?>("Scenario", this.scenario.ToString()));
318
319 this.BuildActiveSequence();
320 }
321
322 public T GetStepViewModel<T>(OnboardingStep step) where T : BaseOnboardingStepViewModel
323 {
324 if (this.stepViewModels.TryGetValue(step, out BaseOnboardingStepViewModel? stepViewModel) && stepViewModel is T typed)
325 {
326 return typed;
327 }
328
329 throw new InvalidOperationException($"Step '{step}' has not been registered.");
330 }
331
332 public override async Task OnInitializeAsync()
333 {
334 await base.OnInitializeAsync().ConfigureAwait(false);
335
336 this.BuildActiveSequence();
337 if (this.allStepsSkippable)
338 {
339 await this.MoveToStepAsync(OnboardingStep.Finalize, NavigationDirection.Forward).ConfigureAwait(false);
340 await this.CompleteOnboardingAsync(false, true).ConfigureAwait(false);
341 return;
342 }
343
344 if (this.activeSequence.Count == 0)
345 {
346 this.activeSequence.Add(OnboardingStep.Finalize);
347 }
348
349 OnboardingStep StartStep = this.ResolveInitialStep();
350 await this.MoveToStepAsync(StartStep, NavigationDirection.Forward).ConfigureAwait(false);
351
352 if (!string.IsNullOrEmpty(this.navigationArgs.PendingIntentUri))
353 {
354 // Find the viewmodel of WelcomeOnboardingPage
355 WelcomeOnboardingStepViewModel WelcomeVM = this.GetStepViewModel<WelcomeOnboardingStepViewModel>(OnboardingStep.Welcome);
356
357 await WelcomeVM.HandleInviteCodeFromIntent(this.navigationArgs.PendingIntentUri);
358 }
359 }
360
367 [RelayCommand]
368 private async Task GoToNext()
369 {
370 if (this.stepViewModels.TryGetValue(this.CurrentStep, out BaseOnboardingStepViewModel? CurrentStepViewModel))
371 {
372 bool CanAdvance = await CurrentStepViewModel.OnNextAsync().ConfigureAwait(false);
373 if (!CanAdvance)
374 {
375 return;
376 }
377 }
378
379 this.BuildActiveSequence();
380 OnboardingStep? NextStep = this.FindStepInDirection(this.CurrentStep, NavigationDirection.Forward);
381 if (NextStep is null)
382 {
383 await this.CompleteOnboardingAsync().ConfigureAwait(false);
384 return;
385 }
386
387 await this.MoveToStepAsync(NextStep.Value, NavigationDirection.Forward).ConfigureAwait(false);
388 }
389
397 public override async Task GoBack()
398 {
399 if (IsBackRestrictedStep(this.CurrentStep))
400 {
401 return;
402 }
403
404 if (this.stepViewModels.TryGetValue(this.CurrentStep, out BaseOnboardingStepViewModel? CurrentStepViewModel))
405 {
406 await CurrentStepViewModel.OnBackAsync().ConfigureAwait(false);
407 }
408
409 this.BuildActiveSequence();
410 OnboardingStep? PreviousStep = this.FindStepInDirection(this.CurrentStep, NavigationDirection.Backward);
411 if (PreviousStep is null)
412 {
413 return;
414 }
415
416 await this.MoveToStepAsync(PreviousStep.Value, NavigationDirection.Backward).ConfigureAwait(false);
417 }
418
426 [RelayCommand]
427 private async Task GoToStep(OnboardingStep Step)
428 {
429 NavigationDirection Direction = this.DetermineDirection(this.CurrentStep, Step);
430 if (Direction == NavigationDirection.Backward && IsBackRestrictedStep(this.CurrentStep))
431 {
432 return;
433 }
434
435 await this.MoveToStepAsync(Step, Direction).ConfigureAwait(false);
436 }
437
438 [RelayCommand]
439 private async Task ChangeLanguage()
440 {
441 await ServiceRef.PopupService.PushAsync<SelectLanguagePopup>().ConfigureAwait(false);
442 this.SelectedLanguage = App.SelectedLanguage;
443 }
444
445 [RelayCommand]
446 private async Task ExistingAccount()
447 {
448 OnboardingHelpPopup Popup = new();
449
450 await ServiceRef.PopupService.PushAsync(Popup);
451 }
452
461 private void InitializeDescriptors()
462 {
463 this.descriptorOrder.Clear();
464 this.descriptorMap.Clear();
465 this.scenarioSequence.Clear();
466
467 List<OnboardingStep> BaseSequence = this.GetBaseSequence(this.scenario);
468 foreach (OnboardingStep Step in BaseSequence)
469 {
470 this.scenarioSequence.Add(Step);
471 }
472
473 HashSet<OnboardingStep> ScenarioSet = new HashSet<OnboardingStep>(this.scenarioSequence);
474
475 this.AddDescriptor(OnboardingStep.Welcome,
476 () => ScenarioSet.Contains(OnboardingStep.Welcome),
477 () => this.TryGetSkipReason(OnboardingStep.Welcome, out _));
478 this.AddDescriptor(OnboardingStep.ValidatePhone,
479 () => ScenarioSet.Contains(OnboardingStep.ValidatePhone),
480 () => this.TryGetSkipReason(OnboardingStep.ValidatePhone, out _));
481 this.AddDescriptor(OnboardingStep.ValidateEmail,
482 () => ScenarioSet.Contains(OnboardingStep.ValidateEmail),
483 () => this.TryGetSkipReason(OnboardingStep.ValidateEmail, out _));
484 this.AddDescriptor(OnboardingStep.NameEntry,
485 () => ScenarioSet.Contains(OnboardingStep.NameEntry),
486 () => this.TryGetSkipReason(OnboardingStep.NameEntry, out _));
487 this.AddDescriptor(OnboardingStep.CreateAccount,
488 () => ScenarioSet.Contains(OnboardingStep.CreateAccount),
489 () => this.TryGetSkipReason(OnboardingStep.CreateAccount, out _));
490 this.AddDescriptor(OnboardingStep.DefinePassword,
491 () => ScenarioSet.Contains(OnboardingStep.DefinePassword),
492 () => this.TryGetSkipReason(OnboardingStep.DefinePassword, out _));
493 this.AddDescriptor(OnboardingStep.Biometrics,
494 () => ScenarioSet.Contains(OnboardingStep.Biometrics),
495 () => this.TryGetSkipReason(OnboardingStep.Biometrics, out _));
496 this.AddDescriptor(OnboardingStep.Finalize,
497 () => ScenarioSet.Contains(OnboardingStep.Finalize),
498 () => false);
499 this.AddDescriptor(OnboardingStep.ContactSupport,
500 () => false,
501 () => false);
502 }
503
504 private void AddDescriptor(OnboardingStep step, Func<bool> include, Func<bool> skip)
505 {
506 StepDescriptor descriptor = new StepDescriptor(step, include, skip);
507 this.descriptorOrder.Add(descriptor);
508 this.descriptorMap[step] = descriptor;
509 }
510
519 private void BuildActiveSequence()
520 {
521 List<OnboardingStep> PreviousSequence = this.activeSequence;
522 List<OnboardingStep> NewSequence = new List<OnboardingStep>();
523 bool HasVisibleSteps = false;
524
525 foreach (StepDescriptor Descriptor in this.descriptorOrder)
526 {
527 if (!Descriptor.Include())
528 {
529 continue;
530 }
531
532 OnboardingStep Step = Descriptor.Step;
533 NewSequence.Add(Step);
534
535 if (Step == OnboardingStep.Finalize)
536 {
537 continue;
538 }
539
540 bool Skip = Descriptor.Skip();
541 if (!Skip)
542 {
543 HasVisibleSteps = true;
544 }
545 }
546
547 if (NewSequence.Count == 0)
548 {
549 NewSequence.Add(OnboardingStep.Finalize);
550 }
551
552 this.activeSequence = NewSequence;
553 this.allStepsSkippable = !HasVisibleSteps;
554
555 bool SequencesEqual = this.AreSequencesEqual(PreviousSequence, NewSequence);
556 if (!this.hasLoggedSequence || !SequencesEqual)
557 {
558 this.hasLoggedSequence = true;
559 this.LogSequence("Built onboarding sequence.");
560 }
561 }
562
570 private List<OnboardingStep> GetBaseSequence(OnboardingScenario onboardingScenario)
571 {
572 List<OnboardingStep> Sequence = new List<OnboardingStep>();
573 switch (onboardingScenario)
574 {
575 case OnboardingScenario.FullSetup:
576 Sequence.Add(OnboardingStep.Welcome);
577 Sequence.Add(OnboardingStep.ValidatePhone);
578 Sequence.Add(OnboardingStep.ValidateEmail);
579 Sequence.Add(OnboardingStep.NameEntry);
580 Sequence.Add(OnboardingStep.CreateAccount);
581 Sequence.Add(OnboardingStep.DefinePassword);
582 Sequence.Add(OnboardingStep.Biometrics);
583 Sequence.Add(OnboardingStep.Finalize);
584 break;
585 case OnboardingScenario.ChangePin:
586 Sequence.Add(OnboardingStep.DefinePassword);
587 Sequence.Add(OnboardingStep.Biometrics);
588 Sequence.Add(OnboardingStep.Finalize);
589 break;
590 case OnboardingScenario.ReverifyIdentity:
591 Sequence.Add(OnboardingStep.ValidatePhone);
592 Sequence.Add(OnboardingStep.ValidateEmail);
593 Sequence.Add(OnboardingStep.CreateAccount);
594 Sequence.Add(OnboardingStep.Finalize);
595 break;
596 default:
597 Sequence.Add(OnboardingStep.Welcome);
598 Sequence.Add(OnboardingStep.Finalize);
599 break;
600 }
601
602 return Sequence;
603 }
604
616 private bool TryGetSkipReason(OnboardingStep Step, out string Reason)
617 {
619 bool HasEstablishedIdentity = this.HasEstablishedIdentity(Profile);
620 if (this.completedSteps.Contains(Step))
621 {
622 Reason = "StepCompleted";
623 return true;
624 }
625
626 switch (Step)
627 {
628 case OnboardingStep.Welcome:
629 // Always show Welcome Now.
630 break;
631 case OnboardingStep.ValidatePhone:
632 if (this.scenario != OnboardingScenario.ReverifyIdentity &&
633 (this.transferContext?.HasLegalIdentity == true || HasEstablishedIdentity))
634 {
635 Reason = "IdentityAlreadyEstablished";
636 return true;
637 }
638 break;
639 case OnboardingStep.ValidateEmail:
640 if (Profile.TestOtpTimestamp is not null)
641 {
642 Reason = "TemporaryOtpUsed";
643 return true;
644 }
645 if (this.scenario != OnboardingScenario.ReverifyIdentity &&
646 (this.transferContext?.HasLegalIdentity == true || HasEstablishedIdentity))
647 {
648 Reason = "IdentityAlreadyEstablished";
649 return true;
650 }
651 break;
652 case OnboardingStep.NameEntry:
653 if (this.scenario != OnboardingScenario.ReverifyIdentity &&
654 (!string.IsNullOrEmpty(Profile.Account) || this.transferContext is not null))
655 {
656 Reason = this.transferContext is not null ? "AccountTransferred" : "AccountAlreadySelected";
657 return true;
658 }
659 break;
660 case OnboardingStep.CreateAccount:
661 if (this.scenario != OnboardingScenario.ReverifyIdentity)
662 {
663 bool HasTransferIdentity = this.transferContext?.HasLegalIdentity == true;
664 bool HasApprovedIdentity = Profile.LegalIdentity is LegalIdentity identity && identity.State == IdentityState.Approved;
665 if (HasTransferIdentity || HasApprovedIdentity)
666 {
667 Reason = HasTransferIdentity ? "AccountTransferred" : "IdentityAlreadyPresent";
668 return true;
669 }
670 }
671 break;
672 case OnboardingStep.DefinePassword:
673 if (this.scenario != OnboardingScenario.ChangePin && Profile.HasLocalPassword)
674 {
675 Reason = "PasswordAlreadyDefined";
676 return true;
677 }
678 break;
679 case OnboardingStep.Biometrics:
680 if (Profile.AuthenticationMethod == AuthenticationMethod.Fingerprint)
681 {
682 Reason = "BiometricsAlreadyEnabled";
683 return true;
684 }
686 {
687 Reason = "BiometricsUnavailable";
688 return true;
689 }
690 break;
691 }
692
693 Reason = string.Empty;
694 return false;
695 }
696
697 private bool HasEstablishedIdentity(ITagProfile Profile)
698 {
699 if (Profile.LegalIdentity is LegalIdentity Identity)
700 {
701 return Identity.State == IdentityState.Approved || Identity.State == IdentityState.Created;
702 }
703
704 return false;
705 }
706
707 private OnboardingStep ResolveInitialStep()
708 {
710 bool HasIdentity = this.HasEstablishedIdentity(Profile);
711 bool HasAccount = !string.IsNullOrEmpty(Profile.Account);
712 switch (this.scenario)
713 {
714 case OnboardingScenario.FullSetup:
715 if (HasIdentity)
716 return OnboardingStep.DefinePassword;
717 if (HasAccount && !HasIdentity)
718 {
719 ServiceRef.LogService.LogInformational("Resuming FullSetup with existing account. Restarting 2FA.",
720 new KeyValuePair<string, object?>("Account", Profile.Account));
721 return OnboardingStep.ValidatePhone;
722 }
723 return OnboardingStep.Welcome;
724 case OnboardingScenario.ReverifyIdentity:
725 return OnboardingStep.ValidatePhone;
726 case OnboardingScenario.ChangePin:
727 return OnboardingStep.DefinePassword;
728 default:
729 return this.activeSequence.Count > 0 ? this.activeSequence[0] : OnboardingStep.Finalize;
730 }
731 }
732
733 private async Task MoveToStepAsync(OnboardingStep Step, NavigationDirection Direction)
734 {
735 this.BuildActiveSequence();
736
737 OnboardingStep TargetStep = this.ResolveStepWithSkipping(Step, Direction);
738 this.CurrentStep = TargetStep;
739
740 try
741 {
742 this.isUpdatingSelection = true;
743 this.SelectedStateKey = TargetStep.ToStateKey();
744 }
745 finally
746 {
747 this.isUpdatingSelection = false;
748 }
749
750 this.HeaderTitle = this.GetStepTitle(TargetStep);
751 this.CanGoBack = !IsBackRestrictedStep(TargetStep) && this.FindStepInDirection(TargetStep, NavigationDirection.Backward).HasValue;
752
753 if (this.stepViewModels.TryGetValue(TargetStep, out BaseOnboardingStepViewModel? StepViewModel))
754 {
755 await StepViewModel.OnActivatedAsync().ConfigureAwait(false);
756 }
757 }
758
759 private OnboardingStep ResolveStepWithSkipping(OnboardingStep Step, NavigationDirection Direction, bool logSkips = true)
760 {
761 if (Step == OnboardingStep.ContactSupport)
762 {
763 return Step;
764 }
765
766 NavigationDirection EffectiveDirection = Direction == NavigationDirection.None ? NavigationDirection.Forward : Direction;
767 OnboardingStep TargetStep = Step;
768 int Guard = this.descriptorOrder.Count;
769
770 while (Guard > 0)
771 {
772 if (!this.descriptorMap.TryGetValue(TargetStep, out StepDescriptor? Descriptor))
773 {
774 return TargetStep;
775 }
776
777 bool Include = Descriptor.Include();
778 bool Skip = Descriptor.Skip();
779
780 if (Include && (!Skip || TargetStep == OnboardingStep.Finalize))
781 {
782 return TargetStep;
783 }
784
785 if (Skip && logSkips && this.TryGetSkipReason(TargetStep, out string Reason) && !string.IsNullOrEmpty(Reason) && !this.loggedSkips.Contains(TargetStep))
786 {
787 this.loggedSkips.Add(TargetStep);
788 ServiceRef.LogService.LogInformational("Runtime skipped onboarding step.",
789 new KeyValuePair<string, object?>("Step", TargetStep.ToString()),
790 new KeyValuePair<string, object?>("Reason", Reason));
791 }
792
793 OnboardingStep? NextStep = this.FindStepInDirection(TargetStep, EffectiveDirection);
794 if (!NextStep.HasValue)
795 {
796 break;
797 }
798
799 TargetStep = NextStep.Value;
800 Guard--;
801 }
802
803 return TargetStep;
804 }
805
806 private OnboardingStep? FindStepInDirection(OnboardingStep ReferenceStep, NavigationDirection Direction)
807 {
808 NavigationDirection EffectiveDirection = Direction == NavigationDirection.None ? NavigationDirection.Forward : Direction;
809
810 if (this.activeSequence.Count == 0)
811 {
812 return null;
813 }
814
815 int ActiveIndex = this.activeSequence.IndexOf(ReferenceStep);
816 if (ActiveIndex >= 0)
817 {
818 if (EffectiveDirection == NavigationDirection.Forward)
819 {
820 if (ActiveIndex >= this.activeSequence.Count - 1)
821 {
822 return null;
823 }
824
825 return this.activeSequence[ActiveIndex + 1];
826 }
827
828 if (ActiveIndex == 0)
829 {
830 return null;
831 }
832
833 return this.activeSequence[ActiveIndex - 1];
834 }
835
836 int CanonicalIndex = this.GetCanonicalIndex(ReferenceStep);
837 if (CanonicalIndex < 0)
838 {
839 return null;
840 }
841
842 if (EffectiveDirection == NavigationDirection.Forward)
843 {
844 for (int i = CanonicalIndex + 1; i < this.descriptorOrder.Count; i++)
845 {
846 StepDescriptor Descriptor = this.descriptorOrder[i];
847 if (!Descriptor.Include())
848 {
849 continue;
850 }
851
852 if (Descriptor.Skip() && Descriptor.Step != OnboardingStep.Finalize)
853 {
854 continue;
855 }
856
857 if (this.activeSequence.Contains(Descriptor.Step))
858 {
859 return Descriptor.Step;
860 }
861 }
862 }
863 else
864 {
865 for (int i = CanonicalIndex - 1; i >= 0; i--)
866 {
867 StepDescriptor Descriptor = this.descriptorOrder[i];
868 if (!Descriptor.Include())
869 {
870 continue;
871 }
872
873 if (Descriptor.Skip() && Descriptor.Step != OnboardingStep.Finalize)
874 {
875 continue;
876 }
877
878 if (this.activeSequence.Contains(Descriptor.Step))
879 {
880 return Descriptor.Step;
881 }
882 }
883 }
884
885 return null;
886 }
887
888 private int GetCanonicalIndex(OnboardingStep Step)
889 {
890 for (int i = 0; i < this.descriptorOrder.Count; i++)
891 {
892 if (this.descriptorOrder[i].Step == Step)
893 {
894 return i;
895 }
896 }
897
898 return -1;
899 }
900
901 private NavigationDirection DetermineDirection(OnboardingStep OriginStep, OnboardingStep TargetStep)
902 {
903 if (OriginStep == TargetStep)
904 {
905 return NavigationDirection.None;
906 }
907
908 int OriginIndex = this.scenarioSequence.IndexOf(OriginStep);
909 int TargetIndex = this.scenarioSequence.IndexOf(TargetStep);
910 if (OriginIndex >= 0 && TargetIndex >= 0)
911 {
912 return TargetIndex >= OriginIndex ? NavigationDirection.Forward : NavigationDirection.Backward;
913 }
914
915 int CanonicalOriginIndex = this.GetCanonicalIndex(OriginStep);
916 int CanonicalTargetIndex = this.GetCanonicalIndex(TargetStep);
917 if (CanonicalOriginIndex >= 0 && CanonicalTargetIndex >= 0)
918 {
919 return CanonicalTargetIndex >= CanonicalOriginIndex ? NavigationDirection.Forward : NavigationDirection.Backward;
920 }
921
922 return NavigationDirection.None;
923 }
924
925 private string GetStepTitle(OnboardingStep Step)
926 {
927 if (this.stepViewModels.TryGetValue(Step, out BaseOnboardingStepViewModel? StepViewModel) && StepViewModel is not null && !string.IsNullOrWhiteSpace(StepViewModel.Title))
928 {
929 return StepViewModel.Title;
930 }
931
932 return Step.ToString();
933 }
934
935 private async Task CompleteOnboardingAsync(bool EnsureFinalizeVisible = true, bool AllStepsSkipped = false)
936 {
937 if (this.isCompleting)
938 {
939 return;
940 }
941
942 this.isCompleting = true;
943
944 if (EnsureFinalizeVisible && this.CurrentStep != OnboardingStep.Finalize)
945 {
946 await this.MoveToStepAsync(OnboardingStep.Finalize, NavigationDirection.Forward).ConfigureAwait(false);
947 }
948
949 ServiceRef.LogService.LogInformational("Onboarding completion triggered.",
950 new KeyValuePair<string, object?>("Scenario", this.scenario.ToString()),
951 new KeyValuePair<string, object?>("Path", AllStepsSkipped ? "AllStepsSkipped" : "Standard"));
952
953 await Task.Delay(TimeSpan.FromMilliseconds(300)).ConfigureAwait(false);
954 await ServiceRef.NavigationService.SetRootAsync(nameof(LoadingPage)).ConfigureAwait(false);
955 }
956
957 private void LogSequence(string Message)
958 {
959 string StepsValue = string.Join(",", this.activeSequence);
960 ServiceRef.LogService.LogInformational(Message,
961 new KeyValuePair<string, object?>("Scenario", this.scenario.ToString()),
962 new KeyValuePair<string, object?>("Steps", StepsValue));
963 }
964
965 private bool AreSequencesEqual(List<OnboardingStep> Left, List<OnboardingStep> Right)
966 {
967 if (ReferenceEquals(Left, Right))
968 {
969 return true;
970 }
971
972 if (Left.Count != Right.Count)
973 {
974 return false;
975 }
976
977 for (int i = 0; i < Left.Count; i++)
978 {
979 if (Left[i] != Right[i])
980 {
981 return false;
982 }
983 }
984
985 return true;
986 }
987
988 partial void OnSelectedStateKeyChanged(string value)
989 {
990 if (this.isUpdatingSelection)
991 {
992 return;
993 }
994
995 if (Enum.TryParse(value, out OnboardingStep ParsedStep) && ParsedStep != this.CurrentStep && this.activeSequence.Contains(ParsedStep))
996 {
997 NavigationDirection Direction = this.DetermineDirection(this.CurrentStep, ParsedStep);
998 if (Direction == NavigationDirection.Backward && IsBackRestrictedStep(this.CurrentStep))
999 {
1000 return;
1001 }
1002
1003 _ = this.MoveToStepAsync(ParsedStep, Direction);
1004 }
1005 }
1006
1007 partial void OnSelectedLanguageChanged(LanguageInfo value)
1008 {
1009 if (value is null)
1010 {
1011 return;
1012 }
1013
1014 if (!Equals(App.SelectedLanguage, value))
1015 {
1016 // Language synchronization handled elsewhere.
1017 }
1018 }
1019
1020 private enum NavigationDirection
1021 {
1022 None = 0,
1023 Forward = 1,
1024 Backward = 2
1025 }
1026
1027 private static bool IsBackRestrictedStep(OnboardingStep step)
1028 {
1029 return step == OnboardingStep.DefinePassword ||
1030 step == OnboardingStep.Biometrics ||
1031 step == OnboardingStep.Finalize;
1032 }
1033 }
1034}
Represents an instance of the Neuro-Access app.
Definition: App.xaml.cs:125
static LanguageInfo SelectedLanguage
Gets the selected language.
Definition: App.xaml.cs:199
static readonly LanguageInfo[] SupportedLanguages
Supported languages.
Definition: App.xaml.cs:179
const string Default
The default language code.
Definition: Constants.cs:105
A set of never changing property constants and helpful values.
Definition: Constants.cs:24
Base class that references services in the app.
Definition: ServiceRef.cs:43
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
static INetworkService NetworkService
Network service.
Definition: ServiceRef.cs:226
static INavigationService NavigationService
The navigation service for navigating between pages.
Definition: ServiceRef.cs:178
static IPopupService PopupService
Popup service for presenting application popups.
Definition: ServiceRef.cs:142
static ITagProfile TagProfile
TAG Profile service.
Definition: ServiceRef.cs:202
static IXmppService XmppService
The XMPP service for XMPP communication.
Definition: ServiceRef.cs:190
static IPlatformSpecific PlatformSpecific
Localization service
Definition: ServiceRef.cs:383
A base class for all view models, inheriting from the BindableObject. NOTE: using this class requir...
virtual void SetIsBusy(bool IsBusy)
Sets the IsBusy property.
Navigation arguments for onboarding flow. Scenario determines dynamic starting step.
override async Task OnInitializeAsync()
Method called when view is initialized for the first time. Use this method to implement registration ...
override async Task GoBack()
Navigates to the previous onboarding step if backward navigation is permitted from the current step.
Provides state and logic for the welcome onboarding step, including invite and QR handling.
Adds support for legal identities, smart contracts and signatures to an XMPP client.
Manages an XMPP client connection. Implements XMPP, as defined in https://tools.ietf....
Definition: XmppClient.cs:58
string PasswordHashMethod
Password hash method.
Definition: XmppClient.cs:3484
bool TryGetExtension(Type Type, out IXmppExtension Extension)
Tries to get a registered extension of a specific type from the client.
Definition: XmppClient.cs:7391
string PasswordHash
Hash value of password. Depends on method used to authenticate user.
Definition: XmppClient.cs:3475
bool SupportsFingerprintAuthentication
If the device supports authenticating the user using fingerprints.
The TAG Profile is the heart of the digital identity for a specific user/device. Use this instance to...
Definition: ITagProfile.cs:18
AuthenticationMethod AuthenticationMethod
How the user authenticates itself with the App.
Definition: ITagProfile.cs:202
string? Account
The account name for this profile
Definition: ITagProfile.cs:102
void SetXmppPasswordNeedsUpdating(bool Value)
Sets the local flag for if xmpp password needs updating.
string? LegalJid
The Jabber Legal JID for this user/profile.
Definition: ITagProfile.cs:117
bool HasLocalPassword
Indicates if the user has a LocalPassword.
Definition: ITagProfile.cs:192
DateTime? TestOtpTimestamp
Returns a timestamp if the user used a Test OTP Code.
Definition: ITagProfile.cs:217
LegalIdentity? LegalIdentity
The legal identity of the current user/profile.
Definition: ITagProfile.cs:222
bool NeedsUpdating()
Returns true if the current ITagProfile needs to have its values updated, false otherwise.
void SetAccount(string AccountName, string ClientPasswordHash, string ClientPasswordHashMethod)
Set the account name and password for a new account.
string? Domain
The domain this profile is connected to.
Definition: ITagProfile.cs:52
Task SetAccountAndLegalIdentity(string AccountName, string ClientPasswordHash, string ClientPasswordHashMethod, LegalIdentity Identity)
Set the account name and password for an existing account.
Definition: ImplTypes.g.cs:58
class Sequence(Array Elements, byte[] SubSection)
A generic sequence class used if dedicated security objects cannot be found.
Definition: Sequence.cs:10
AuthenticationMethod
How the user authenticates itself with the App.
OnboardingScenario
Distinct onboarding scenarios.
OnboardingStep
Unified onboarding steps matching the registration journey.
IdentityState
Lists recognized legal identity states.
Reason
Reason a token is not valid.
Definition: JwtFactory.cs:15