3using System.Collections.ObjectModel;
4using System.Threading.Tasks;
6using CommunityToolkit.Mvvm.ComponentModel;
7using CommunityToolkit.Mvvm.Input;
25 private sealed record StepDescriptor(
OnboardingStep Step, Func<bool> Include, Func<bool> Skip);
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>();
36 private OnboardingTransferContext? transferContext;
37 private bool isUpdatingSelection;
38 private bool isCompleting;
39 private bool hasLoggedSequence;
40 private bool allStepsSkippable;
43 [NotifyPropertyChangedFor(nameof(IsSummaryStep))]
44 [NotifyPropertyChangedFor(nameof(IsOnWelcomeStep))]
48 private string selectedStateKey =
string.Empty;
51 [NotifyCanExecuteChangedFor(nameof(GoBackCommand))]
52 private bool canGoBack;
55 private string headerTitle =
string.Empty;
58 private ObservableCollection<LanguageInfo> languages =
new ObservableCollection<LanguageInfo>(
App.
SupportedLanguages);
72 this.navigationArgs = Args;
73 this.scenario = this.navigationArgs.
Scenario;
75 new KeyValuePair<string, object?>(
"Scenario", this.scenario.ToString()));
76 this.InitializeDescriptors();
79 public bool IsSummaryStep => this.CurrentStep ==
OnboardingStep.Finalize;
81 public bool IsOnWelcomeStep => this.CurrentStep ==
OnboardingStep.Welcome;
85 internal bool HasPendingTransfer => this.transferContext is not
null;
87 internal bool PendingTransferIncludesLegalIdentity => this.transferContext?.HasLegalIdentity ==
true;
91 if (this.stepViewModels.ContainsKey(stepViewModel.Step))
96 this.stepViewModels[stepViewModel.Step] = stepViewModel;
97 stepViewModel.AttachCoordinator(
this);
98 if (stepViewModel.Step ==
this.CurrentStep)
100 this.HeaderTitle = this.GetStepTitle(stepViewModel.Step);
104 internal Task ApplyTransferContextAsync(OnboardingTransferContext context)
106 this.transferContext = context;
108 new KeyValuePair<string, object?>(
"HasLegalIdentity", context.HasLegalIdentity));
109 this.BuildActiveSequence();
110 return Task.CompletedTask;
113 internal async Task<bool> TryFinalizeTransferAsync(
bool showBusyOverlay)
115 OnboardingTransferContext? context = this.transferContext;
123 await MainThread.InvokeOnMainThreadAsync(() => this.
SetIsBusy(
true));
128 bool connected = await ConnectToAccountAsync(
131 context.PasswordMethod,
133 context.LegalIdDefinition,
134 string.Empty).ConfigureAwait(
false);
138 this.transferContext =
null;
139 this.BuildActiveSequence();
148 await MainThread.InvokeOnMainThreadAsync(() => this.
SetIsBusy(
false));
163 internal static async Task<bool> ConnectToAccountAsync(
string accountName,
string password,
string passwordMethod,
string legalIdentityJid, XmlElement? legalIdDefinition,
string pin)
166 new KeyValuePair<string, object?>(
"Account", accountName),
174 DateTime Now = DateTime.Now;
178 ServiceRef.
LogService.LogInformational($
"Service discovery result: {ServiceDiscoverySucceeded}.");
181 bool DestroyContractsClient =
false;
185 DestroyContractsClient =
true;
191 if (legalIdDefinition is not
null)
194 await contractsClient.ImportKeys(legalIdDefinition);
197 LegalIdentity[] identities = await contractsClient.GetLegalIdentitiesAsync();
203 if ((
string.IsNullOrEmpty(legalIdentityJid) ||
string.Compare(legalIdentityJid, identity.
Id, StringComparison.OrdinalIgnoreCase) == 0) &&
209 ApprovedIdentity = identity;
213 CreatedIdentity ??= identity;
216 catch (Exception ex2)
222 LegalIdentity? SelectedIdentity = ApprovedIdentity ?? CreatedIdentity;
224 if (SelectedIdentity is not
null)
226 ServiceRef.
LogService.LogInformational($
"Selected identity '{SelectedIdentity.Id}' (State={SelectedIdentity.State}).");
228 SelectedId = SelectedIdentity.
Id;
235 SelectedId =
string.Empty;
238 if (!
string.IsNullOrEmpty(pin))
241 ServiceRef.TagProfile.LocalPassword = pin;
246 if (identity.
Id == SelectedId)
249 switch (identity.
State)
254 await contractsClient.ObsoleteLegalIdentityAsync(identity.
Id);
261 if (DestroyContractsClient)
264 contractsClient.Dispose();
271 ServiceRef.
LogService.LogInformational($
"Connecting to XMPP account Host='{HostName}' Port={PortNumber} IsIp={IsIp}.");
272 (
bool Succeeded,
string? ErrorMessage,
string[]?
_) = await
ServiceRef.
XmppService.TryConnectAndConnectToAccount(
281 typeof(
App).Assembly,
283 ServiceRef.
LogService.LogInformational($
"XMPP connect result: {(Succeeded ? "Succeeded
" : "Failed
")} Error='{ErrorMessage}'.");
284 if (!Succeeded && !
string.IsNullOrEmpty(ErrorMessage))
298 this.BuildActiveSequence();
299 OnboardingStep? Candidate = this.FindStepInDirection(step, NavigationDirection.Forward);
300 if (!Candidate.HasValue)
305 return this.ResolveStepWithSkipping(Candidate.Value, NavigationDirection.Forward,
false);
310 if (!this.completedSteps.Add(step))
316 new KeyValuePair<string, object?>(
"Step", step.ToString()),
317 new KeyValuePair<string, object?>(
"Scenario",
this.scenario.ToString()));
319 this.BuildActiveSequence();
329 throw new InvalidOperationException($
"Step '{step}' has not been registered.");
334 await base.OnInitializeAsync().ConfigureAwait(
false);
336 this.BuildActiveSequence();
337 if (this.allStepsSkippable)
339 await this.MoveToStepAsync(
OnboardingStep.Finalize, NavigationDirection.Forward).ConfigureAwait(
false);
340 await this.CompleteOnboardingAsync(
false,
true).ConfigureAwait(
false);
344 if (this.activeSequence.Count == 0)
350 await this.MoveToStepAsync(StartStep, NavigationDirection.Forward).ConfigureAwait(
false);
352 if (!
string.IsNullOrEmpty(this.navigationArgs.PendingIntentUri))
357 await WelcomeVM.HandleInviteCodeFromIntent(this.navigationArgs.PendingIntentUri);
368 private async Task GoToNext()
372 bool CanAdvance = await CurrentStepViewModel.OnNextAsync().ConfigureAwait(
false);
379 this.BuildActiveSequence();
380 OnboardingStep? NextStep = this.FindStepInDirection(this.CurrentStep, NavigationDirection.Forward);
381 if (NextStep is
null)
383 await this.CompleteOnboardingAsync().ConfigureAwait(
false);
387 await this.MoveToStepAsync(NextStep.Value, NavigationDirection.Forward).ConfigureAwait(
false);
399 if (IsBackRestrictedStep(this.CurrentStep))
406 await CurrentStepViewModel.OnBackAsync().ConfigureAwait(
false);
409 this.BuildActiveSequence();
410 OnboardingStep? PreviousStep = this.FindStepInDirection(this.CurrentStep, NavigationDirection.Backward);
411 if (PreviousStep is
null)
416 await this.MoveToStepAsync(PreviousStep.Value, NavigationDirection.Backward).ConfigureAwait(
false);
429 NavigationDirection Direction = this.DetermineDirection(this.CurrentStep, Step);
430 if (Direction == NavigationDirection.Backward && IsBackRestrictedStep(this.CurrentStep))
435 await this.MoveToStepAsync(Step, Direction).ConfigureAwait(
false);
439 private async Task ChangeLanguage()
446 private async Task ExistingAccount()
461 private void InitializeDescriptors()
463 this.descriptorOrder.Clear();
464 this.descriptorMap.Clear();
465 this.scenarioSequence.Clear();
467 List<OnboardingStep> BaseSequence = this.GetBaseSequence(this.scenario);
470 this.scenarioSequence.Add(Step);
473 HashSet<OnboardingStep> ScenarioSet =
new HashSet<OnboardingStep>(this.scenarioSequence);
504 private void AddDescriptor(
OnboardingStep step, Func<bool> include, Func<bool> skip)
506 StepDescriptor descriptor =
new StepDescriptor(step, include, skip);
507 this.descriptorOrder.Add(descriptor);
508 this.descriptorMap[step] = descriptor;
519 private void BuildActiveSequence()
521 List<OnboardingStep> PreviousSequence = this.activeSequence;
522 List<OnboardingStep> NewSequence =
new List<OnboardingStep>();
523 bool HasVisibleSteps =
false;
525 foreach (StepDescriptor Descriptor
in this.descriptorOrder)
527 if (!Descriptor.Include())
533 NewSequence.Add(Step);
540 bool Skip = Descriptor.Skip();
543 HasVisibleSteps =
true;
547 if (NewSequence.Count == 0)
552 this.activeSequence = NewSequence;
553 this.allStepsSkippable = !HasVisibleSteps;
555 bool SequencesEqual = this.AreSequencesEqual(PreviousSequence, NewSequence);
556 if (!this.hasLoggedSequence || !SequencesEqual)
558 this.hasLoggedSequence =
true;
559 this.LogSequence(
"Built onboarding sequence.");
572 List<OnboardingStep>
Sequence =
new List<OnboardingStep>();
573 switch (onboardingScenario)
616 private bool TryGetSkipReason(
OnboardingStep Step, out
string Reason)
619 bool HasEstablishedIdentity = this.HasEstablishedIdentity(Profile);
620 if (this.completedSteps.Contains(Step))
633 (
this.transferContext?.HasLegalIdentity ==
true || HasEstablishedIdentity))
635 Reason =
"IdentityAlreadyEstablished";
642 Reason =
"TemporaryOtpUsed";
646 (
this.transferContext?.HasLegalIdentity ==
true || HasEstablishedIdentity))
648 Reason =
"IdentityAlreadyEstablished";
654 (!
string.IsNullOrEmpty(Profile.
Account) ||
this.transferContext is not
null))
656 Reason = this.transferContext is not
null ?
"AccountTransferred" :
"AccountAlreadySelected";
663 bool HasTransferIdentity = this.transferContext?.HasLegalIdentity ==
true;
665 if (HasTransferIdentity || HasApprovedIdentity)
667 Reason = HasTransferIdentity ?
"AccountTransferred" :
"IdentityAlreadyPresent";
675 Reason =
"PasswordAlreadyDefined";
682 Reason =
"BiometricsAlreadyEnabled";
687 Reason =
"BiometricsUnavailable";
697 private bool HasEstablishedIdentity(
ITagProfile Profile)
701 return Identity.State == IdentityState.Approved || Identity.State ==
IdentityState.Created;
710 bool HasIdentity = this.HasEstablishedIdentity(Profile);
711 bool HasAccount = !
string.IsNullOrEmpty(Profile.
Account);
712 switch (this.scenario)
717 if (HasAccount && !HasIdentity)
719 ServiceRef.
LogService.LogInformational(
"Resuming FullSetup with existing account. Restarting 2FA.",
720 new KeyValuePair<string, object?>(
"Account", Profile.
Account));
729 return this.activeSequence.Count > 0 ? this.activeSequence[0] :
OnboardingStep.Finalize;
733 private async Task MoveToStepAsync(
OnboardingStep Step, NavigationDirection Direction)
735 this.BuildActiveSequence();
737 OnboardingStep TargetStep = this.ResolveStepWithSkipping(Step, Direction);
738 this.CurrentStep = TargetStep;
742 this.isUpdatingSelection =
true;
743 this.SelectedStateKey = TargetStep.ToStateKey();
747 this.isUpdatingSelection =
false;
750 this.HeaderTitle = this.GetStepTitle(TargetStep);
751 this.CanGoBack = !IsBackRestrictedStep(TargetStep) && this.FindStepInDirection(TargetStep, NavigationDirection.Backward).HasValue;
755 await StepViewModel.OnActivatedAsync().ConfigureAwait(
false);
766 NavigationDirection EffectiveDirection = Direction == NavigationDirection.None ? NavigationDirection.Forward : Direction;
768 int Guard = this.descriptorOrder.Count;
772 if (!this.descriptorMap.TryGetValue(TargetStep, out StepDescriptor? Descriptor))
777 bool Include = Descriptor.Include();
778 bool Skip = Descriptor.Skip();
785 if (Skip && logSkips && this.TryGetSkipReason(TargetStep, out
string Reason) && !
string.IsNullOrEmpty(Reason) && !this.loggedSkips.Contains(TargetStep))
787 this.loggedSkips.Add(TargetStep);
789 new KeyValuePair<string, object?>(
"Step", TargetStep.ToString()),
790 new KeyValuePair<string, object?>(
"Reason", Reason));
793 OnboardingStep? NextStep = this.FindStepInDirection(TargetStep, EffectiveDirection);
794 if (!NextStep.HasValue)
799 TargetStep = NextStep.Value;
808 NavigationDirection EffectiveDirection = Direction == NavigationDirection.None ? NavigationDirection.Forward : Direction;
810 if (this.activeSequence.Count == 0)
815 int ActiveIndex = this.activeSequence.IndexOf(ReferenceStep);
816 if (ActiveIndex >= 0)
818 if (EffectiveDirection == NavigationDirection.Forward)
820 if (ActiveIndex >= this.activeSequence.Count - 1)
825 return this.activeSequence[ActiveIndex + 1];
828 if (ActiveIndex == 0)
833 return this.activeSequence[ActiveIndex - 1];
836 int CanonicalIndex = this.GetCanonicalIndex(ReferenceStep);
837 if (CanonicalIndex < 0)
842 if (EffectiveDirection == NavigationDirection.Forward)
844 for (
int i = CanonicalIndex + 1; i < this.descriptorOrder.Count; i++)
846 StepDescriptor Descriptor = this.descriptorOrder[i];
847 if (!Descriptor.Include())
852 if (Descriptor.Skip() && Descriptor.Step !=
OnboardingStep.Finalize)
857 if (this.activeSequence.Contains(Descriptor.Step))
859 return Descriptor.Step;
865 for (
int i = CanonicalIndex - 1; i >= 0; i--)
867 StepDescriptor Descriptor = this.descriptorOrder[i];
868 if (!Descriptor.Include())
873 if (Descriptor.Skip() && Descriptor.Step !=
OnboardingStep.Finalize)
878 if (this.activeSequence.Contains(Descriptor.Step))
880 return Descriptor.Step;
890 for (
int i = 0; i < this.descriptorOrder.Count; i++)
892 if (this.descriptorOrder[i].Step == Step)
903 if (OriginStep == TargetStep)
905 return NavigationDirection.None;
908 int OriginIndex = this.scenarioSequence.IndexOf(OriginStep);
909 int TargetIndex = this.scenarioSequence.IndexOf(TargetStep);
910 if (OriginIndex >= 0 && TargetIndex >= 0)
912 return TargetIndex >= OriginIndex ? NavigationDirection.Forward : NavigationDirection.Backward;
915 int CanonicalOriginIndex = this.GetCanonicalIndex(OriginStep);
916 int CanonicalTargetIndex = this.GetCanonicalIndex(TargetStep);
917 if (CanonicalOriginIndex >= 0 && CanonicalTargetIndex >= 0)
919 return CanonicalTargetIndex >= CanonicalOriginIndex ? NavigationDirection.Forward : NavigationDirection.Backward;
922 return NavigationDirection.None;
927 if (this.stepViewModels.TryGetValue(Step, out
BaseOnboardingStepViewModel? StepViewModel) && StepViewModel is not
null && !
string.IsNullOrWhiteSpace(StepViewModel.Title))
929 return StepViewModel.Title;
932 return Step.ToString();
935 private async Task CompleteOnboardingAsync(
bool EnsureFinalizeVisible =
true,
bool AllStepsSkipped =
false)
937 if (this.isCompleting)
942 this.isCompleting =
true;
944 if (EnsureFinalizeVisible && this.CurrentStep !=
OnboardingStep.Finalize)
946 await this.MoveToStepAsync(
OnboardingStep.Finalize, NavigationDirection.Forward).ConfigureAwait(
false);
950 new KeyValuePair<string, object?>(
"Scenario", this.scenario.ToString()),
951 new KeyValuePair<string, object?>(
"Path", AllStepsSkipped ?
"AllStepsSkipped" :
"Standard"));
953 await Task.Delay(TimeSpan.FromMilliseconds(300)).ConfigureAwait(
false);
957 private void LogSequence(
string Message)
959 string StepsValue =
string.Join(
",", this.activeSequence);
961 new KeyValuePair<string, object?>(
"Scenario", this.scenario.ToString()),
962 new KeyValuePair<string, object?>(
"Steps", StepsValue));
965 private bool AreSequencesEqual(List<OnboardingStep> Left, List<OnboardingStep> Right)
967 if (ReferenceEquals(Left, Right))
972 if (Left.Count != Right.Count)
977 for (
int i = 0; i < Left.Count; i++)
979 if (Left[i] != Right[i])
988 partial
void OnSelectedStateKeyChanged(
string value)
990 if (this.isUpdatingSelection)
995 if (Enum.TryParse(value, out
OnboardingStep ParsedStep) && ParsedStep !=
this.CurrentStep &&
this.activeSequence.Contains(ParsedStep))
997 NavigationDirection Direction = this.DetermineDirection(this.CurrentStep, ParsedStep);
998 if (Direction == NavigationDirection.Backward && IsBackRestrictedStep(this.CurrentStep))
1003 _ = this.MoveToStepAsync(ParsedStep, Direction);
1007 partial
void OnSelectedLanguageChanged(
LanguageInfo value)
1020 private enum NavigationDirection
1029 return step == OnboardingStep.DefinePassword ||
1030 step == OnboardingStep.Biometrics ||
Represents an instance of the Neuro-Access app.
static LanguageInfo SelectedLanguage
Gets the selected language.
static readonly LanguageInfo[] SupportedLanguages
Supported languages.
const string Default
The default language code.
A set of never changing property constants and helpful values.
Base class that references services in the app.
static ILogService LogService
Log service.
static INetworkService NetworkService
Network service.
static INavigationService NavigationService
The navigation service for navigating between pages.
static IPopupService PopupService
Popup service for presenting application popups.
static ITagProfile TagProfile
TAG Profile service.
static IXmppService XmppService
The XMPP service for XMPP communication.
static IPlatformSpecific PlatformSpecific
Localization service
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.
OnboardingScenario Scenario
Onboarding scenario.
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.
bool HasClientPublicKey
If the identity has a client public key
DateTime From
From what point in time the legal identity is valid.
DateTime To
To what point in time the legal identity is valid.
IdentityState State
Current state of identity
string Id
ID of the legal identity
bool ValidateClientSignature()
Validates the client signature of the legal identity
bool HasClientSignature
If the identity has a client signature
Manages an XMPP client connection. Implements XMPP, as defined in https://tools.ietf....
string PasswordHashMethod
Password hash method.
bool TryGetExtension(Type Type, out IXmppExtension Extension)
Tries to get a registered extension of a specific type from the client.
string PasswordHash
Hash value of password. Depends on method used to authenticate user.
The TAG Profile is the heart of the digital identity for a specific user/device. Use this instance to...
AuthenticationMethod AuthenticationMethod
How the user authenticates itself with the App.
string? Account
The account name for this profile
void SetXmppPasswordNeedsUpdating(bool Value)
Sets the local flag for if xmpp password needs updating.
string? LegalJid
The Jabber Legal JID for this user/profile.
bool HasLocalPassword
Indicates if the user has a LocalPassword.
DateTime? TestOtpTimestamp
Returns a timestamp if the user used a Test OTP Code.
LegalIdentity? LegalIdentity
The legal identity of the current user/profile.
bool NeedsUpdating()
Returns true if the current ITagProfile needs to have its values updated, false otherwise.
void SetAccount(string AccountName, string ClientPasswordHash, string ClientPasswordHashMethod)
Set the account name and password for a new account.
string? Domain
The domain this profile is connected to.
Task SetAccountAndLegalIdentity(string AccountName, string ClientPasswordHash, string ClientPasswordHashMethod, LegalIdentity Identity)
Set the account name and password for an existing account.
class Sequence(Array Elements, byte[] SubSection)
A generic sequence class used if dedicated security objects cannot be found.
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.