6using System.Threading.Tasks;
8using CommunityToolkit.Mvvm.ComponentModel;
9using CommunityToolkit.Mvvm.Input;
10using Microsoft.Maui.ApplicationModel;
29 private CancellationTokenSource? inviteCodeCts;
30 private const int InviteCodeDebounceMs = 500;
31 private bool autoAdvanced;
39 [NotifyPropertyChangedFor(nameof(
IsBusy))]
40 private bool isLoading =
true;
46 private string? inviteCode;
52 private bool inviteCodeIsValid =
true;
58 [NotifyPropertyChangedFor(nameof(
IsBusy))]
59 private bool isProcessingInvite;
79 public bool IsBusy => this.IsLoading || this.IsProcessingInvite;
81 internal override async Task OnActivatedAsync()
84 await base.OnActivatedAsync();
88 await Task.Delay(800).ConfigureAwait(
false);
89 MainThread.BeginInvokeOnMainThread(() =>
91 this.IsLoading =
false;
97 internal override Task<bool> OnNextAsync() => Task.FromResult(
true);
99 partial
void OnInviteCodeChanged(
string? value)
101 ServiceRef.
LogService.LogDebug($
"Invite code changed. RawValue='{value ?? string.Empty}' AutoAdvanced={this.autoAdvanced}.");
102 if (this.autoAdvanced)
108 this.CancelPendingInviteProcessing();
110 if (
string.IsNullOrWhiteSpace(value))
112 this.InviteCodeIsValid =
true;
117 bool IsValid = BasicValidateInviteCode(value, out
string Trimmed);
118 this.InviteCodeIsValid = IsValid;
119 ServiceRef.
LogService.LogInformational($
"Invite code validation result: Valid={IsValid} Trimmed='{Trimmed}'.");
126 CancellationTokenSource DebounceCts =
new CancellationTokenSource();
127 this.inviteCodeCts = DebounceCts;
128 ServiceRef.
LogService.LogDebug($
"Debounce scheduled ({InviteCodeDebounceMs} ms) for invitation processing.");
130 _ = Task.Run(async () =>
134 await Task.Delay(InviteCodeDebounceMs, DebounceCts.Token).ConfigureAwait(
false);
135 if (DebounceCts.IsCancellationRequested)
137 ServiceRef.LogService.LogDebug(
"Debounce cancelled before execution.");
140 if (!
string.Equals(this.InviteCode, value, StringComparison.Ordinal))
142 ServiceRef.LogService.LogDebug(
"Invite code changed during debounce; aborting processing.");
146 this.SetInviteProcessingState(
true);
149 bool Processed = await this.ProcessInvitationAsync(Trimmed).ConfigureAwait(false);
152 MainThread.BeginInvokeOnMainThread(this.AdvanceAfterInvite);
157 this.SetInviteProcessingState(
false);
160 catch (OperationCanceledException)
170 if (ReferenceEquals(this.inviteCodeCts, DebounceCts))
172 this.inviteCodeCts =
null;
175 DebounceCts.Dispose();
177 }, DebounceCts.Token);
180 public async Task HandleInviteCodeFromIntent(
string inviteUrl)
184 bool IsValid = BasicValidateInviteCode(inviteUrl, out
string Trimmed);
185 this.InviteCodeIsValid = IsValid;
186 ServiceRef.
LogService.LogInformational($
"Invite code validation result: Valid={IsValid} Trimmed='{Trimmed}'.");
196 bool Processed = await this.ProcessInvitationAsync(Trimmed).ConfigureAwait(
false);
199 MainThread.BeginInvokeOnMainThread(this.AdvanceAfterInvite);
204 this.SetInviteProcessingState(
false);
208 private void AdvanceAfterInvite()
210 if (this.autoAdvanced)
215 this.autoAdvanced =
true;
216 ServiceRef.
LogService.LogInformational(
"Advancing to next onboarding step after invitation processing.");
217 if (this.CoordinatorViewModel is not
null)
218 this.CoordinatorViewModel.GoToNextCommand.Execute(
null);
222 private async Task PasteInviteCode()
225 if (!Clipboard.HasText)
230 string? ClipboardText = await Clipboard.GetTextAsync();
231 if (
string.IsNullOrWhiteSpace(ClipboardText))
237 await Clipboard.SetTextAsync(
null);
238 this.InviteCode = ClipboardText;
242 private async Task ScanQrCode()
246 if (
string.IsNullOrWhiteSpace(Url))
258 MainThread.BeginInvokeOnMainThread(() => this.InviteCode = Url);
262 private async Task SelectForMe()
266 this.CancelPendingInviteProcessing();
268 if (!await this.PrepareAutomaticProviderSelectionAsync().ConfigureAwait(
false))
270 ServiceRef.
LogService.LogWarning(
"Automatic provider preparation failed. Staying on Welcome step.");
274 bool NavigationSucceeded =
false;
277 this.autoAdvanced =
true;
279 await MainThread.InvokeOnMainThreadAsync(() => this.InviteCode =
null);
281 if (this.CoordinatorViewModel is
null)
287 ServiceRef.
LogService.LogInformational(
"Advancing directly to ValidatePhone step after automatic provider selection.");
288 await this.CoordinatorViewModel.GoToStepCommand.ExecuteAsync(
OnboardingStep.ValidatePhone).ConfigureAwait(
false);
289 NavigationSucceeded =
true;
293 if (!NavigationSucceeded)
295 this.autoAdvanced =
false;
300 private async Task<bool> PrepareAutomaticProviderSelectionAsync()
308 if (HadExistingDomain)
310 ServiceRef.
LogService.LogInformational(
"Clearing previously selected domain to enable automatic provider selection.");
314 ServiceRef.
LogService.LogInformational(
"No domain selected; ready for automatic provider selection.");
323 await MainThread.InvokeOnMainThreadAsync(async () =>
335 internal override Task OnBackAsync()
338 this.CancelPendingInviteProcessing();
339 return Task.CompletedTask;
342 private void SetInviteProcessingState(
bool isProcessing)
344 if (MainThread.IsMainThread)
346 this.IsProcessingInvite = isProcessing;
350 MainThread.BeginInvokeOnMainThread(() => this.IsProcessingInvite = isProcessing);
354 private void CancelPendingInviteProcessing()
356 CancellationTokenSource? existing = Interlocked.Exchange(ref this.inviteCodeCts,
null);
357 if (existing is
null)
366 catch (ObjectDisposedException)
372 private void MarkInviteAsInvalid(
string messageResourceKey)
374 MainThread.BeginInvokeOnMainThread(async () =>
376 this.InviteCodeIsValid =
false;
384 private static bool BasicValidateInviteCode(
string code, out
string trimmed)
386 trimmed = code.Trim();
387 if (
string.IsNullOrEmpty(trimmed))
391 string[] Parts = trimmed.Split(
':', StringSplitOptions.RemoveEmptyEntries);
392 if (Parts.Length != 5)
394 string Domain = Parts[1];
395 string CodePart = Parts[2];
396 string KeyPart = Parts[3];
397 string IvPart = Parts[4];
398 if (
string.IsNullOrWhiteSpace(Domain) || Domain.Contains(
' '))
400 if (
string.IsNullOrWhiteSpace(CodePart))
404 _ = Convert.FromBase64String(KeyPart);
405 _ = Convert.FromBase64String(IvPart);
407 catch {
return false; }
411 private async Task<bool> ProcessInvitationAsync(
string inviteUrl)
414 string[] Parts = inviteUrl.Split(
':', StringSplitOptions.RemoveEmptyEntries);
415 if (Parts.Length != 5)
421 string Domain = Parts[1];
422 string Code = Parts[2];
423 string KeyStr = Parts[3];
424 string IvStr = Parts[4];
429 DomainInfoUri =
new Uri(
"https://" + Domain +
"/Onboarding/GetInfo");
442 Encoding.ASCII.GetBytes(Code),
446 new KeyValuePair<string, string>(
"Accept",
"text/plain"));
450 EncryptedStr = (string)Decoded.
Decoded;
460 XmlElement? LegalIdDefinition =
null;
461 string? transferredAccount =
null;
462 string? transferredPassword =
null;
463 string? transferredPasswordMethod =
null;
464 bool domainSelectedFromInvite =
false;
468 byte[] Key = Convert.FromBase64String(KeyStr);
469 byte[] Iv = Convert.FromBase64String(IvStr);
470 byte[] Encrypted = Convert.FromBase64String(EncryptedStr);
471 using Aes Aes = Aes.Create();
474 Aes.Mode = CipherMode.CBC;
475 Aes.Padding = PaddingMode.PKCS7;
476 using ICryptoTransform Decryptor = Aes.CreateDecryptor(Key, Iv);
477 byte[] Decrypted = Decryptor.TransformFinalBlock(Encrypted, 0, Encrypted.Length);
478 string XmlStr = Encoding.UTF8.GetString(Decrypted);
479 XmlDocument Doc =
new XmlDocument { PreserveWhitespace =
true };
482 throw new Exception(
"Invalid Invitation XML");
484 LinkedList<XmlElement> ToProcess =
new LinkedList<XmlElement>();
485 ToProcess.AddLast(Doc.DocumentElement);
486 while (ToProcess.First is not
null)
488 XmlElement E = ToProcess.First.Value;
489 ToProcess.RemoveFirst();
497 if(
string.IsNullOrEmpty(ApiDomain))
499 ApiDomain = await
ServiceRef.
UiService.DisplayPrompt(
"Please enter the IP of your provider",
"This is for developers and testers",
"Enter",
"Cancel");
502 await SelectDomain(ApiDomain, ApiKey, Secret).ConfigureAwait(
false);
503 domainSelectedFromInvite =
true;
508 string PasswordMethod =
XML.
Attribute(E,
"passwordMethod");
510 ServiceRef.
LogService.LogInformational($
"Selecting domain (Account) '{AccDomain}'. Capturing transfer for user '{UserName}'.");
511 await SelectDomain(AccDomain,
string.Empty,
string.Empty).ConfigureAwait(
false);
512 transferredAccount = UserName;
513 transferredPassword = Password;
514 transferredPasswordMethod = PasswordMethod;
517 LegalIdDefinition = E;
521 foreach (XmlNode N
in E.ChildNodes)
522 if (N is XmlElement E2)
523 ToProcess.AddLast(E2);
532 bool advancedManually =
false;
533 bool shouldAdvance =
false;
534 if (!
string.IsNullOrEmpty(transferredAccount) && transferredPassword is not
null && transferredPasswordMethod is not
null && this.CoordinatorViewModel is not
null)
536 OnboardingTransferContext Context =
new OnboardingTransferContext(transferredAccount, transferredPassword, transferredPasswordMethod, LegalIdDefinition);
537 await this.CoordinatorViewModel.ApplyTransferContextAsync(Context).ConfigureAwait(
false);
538 shouldAdvance =
true;
540 bool Connected = await this.CoordinatorViewModel.TryFinalizeTransferAsync(
false).ConfigureAwait(
false);
542 new KeyValuePair<string, object?>(
"Success", Connected),
543 new KeyValuePair<string, object?>(
"HasLegalId", Context.HasLegalIdentity));
544 if (Connected && Context.HasLegalIdentity)
546 advancedManually =
true;
547 MainThread.BeginInvokeOnMainThread(async () =>
549 await this.CoordinatorViewModel.GoToStepCommand.ExecuteAsync(
OnboardingStep.DefinePassword);
555 if (domainSelectedFromInvite)
557 shouldAdvance =
true;
566 if (!advancedManually)
568 return shouldAdvance;
582 private static async Task SelectDomain(
string Domain,
string Key,
string Secret)
585 bool DefaultConnectivity;
590 ServiceRef.
LogService.LogInformational($
"Domain resolution: Host='{HostName}' Port={PortNumber} DefaultConnectivity={DefaultConnectivity}.");
595 DefaultConnectivity =
false;
Represents an instance of the Neuro-Access app.
static void ValidateCertificateCallback(object? Sender, RemoteCertificateEventArgs Args)
Callback for validating SSL certificates. This method is called when a remote certificate is received...
const string Onboarding
Onboarding URI Scheme (obinfo)
static ? string GetScheme(string Url)
Gets the predefined scheme from an IoT Code
A set of never changing property constants and helpful values.
A strongly-typed resource class, for looking up localized strings, etc.
static string UnableToAccessInvitation
Looks up a localized string similar to Unable to access invitation information..
static string ActivateYourDigitalIdentity
Looks up a localized string similar to Select your ID provider.
static string ScanQRCode
Looks up a localized string similar to Scan QR Code.
static string InvalidInvitationCode
Looks up a localized string similar to Invalid invitation code..
static string Ok
Looks up a localized string similar to OK.
static string SomethingWentWrong
Looks up a localized string similar to Something went wrong.
static string ErrorTitle
Looks up a localized string similar to An error has occurred.
Base class that references services in the app.
static ILogService LogService
Log service.
static INetworkService NetworkService
Network service.
static IUiService UiService
Service serializing and managing UI-related tasks.
static ITagProfile TagProfile
TAG Profile service.
static IReportingStringLocalizer Localizer
Localization service
Helper class to perform scanning of QR Codes by displaying the UI and handling async results.
static async Task< string?> ScanQrCode(string? QrTitle, string[] AllowedSchemas)
Navigates to the Scan QR Code Page, waits for scan to complete, and returns the result....
Provides state and logic for the welcome onboarding step, including invite and QR handling.
override string NextButtonText
Gets the text displayed on the (unused) next button.
bool CanContinue
Gets a value indicating whether manual continuation is allowed.
bool IsBusy
Gets a value indicating whether the welcome step is initializing or processing invite data.
override string Title
Gets the localized title displayed on the welcome step.
Contains information about a binary response to a content request.
string ContentType
Internet Content-Type of encoded object.
void AssertOk()
Asserts response is OK.
byte[] Encoded
Encoded object.
Contains information about a response to a content request.
object Decoded
Decoded object.
void AssertOk()
Asserts response is OK.
Static class managing encoding and decoding of internet content.
static Task< ContentResponse > PostAsync(Uri Uri, object Data, params KeyValuePair< string, string >[] Headers)
Posts to a resource, using a Uniform Resource Identifier (or Locator).
static Task< ContentResponse > DecodeAsync(string ContentType, byte[] Data, Encoding Encoding, KeyValuePair< string, string >[] Fields, Uri BaseUri)
Decodes an object.
Helps with common XML-related tasks.
static string Attribute(XmlElement E, string Name)
Gets the value of an XML attribute.
Adds support for legal identities, smart contracts and signatures to an XMPP client.
const string NamespaceOnboarding
http://waher.se/schema/Onboarding/v1.xsd
Class containing credentials for an XMPP client connection.
const int DefaultPort
Default XMPP Server port.
OnboardingStep
Unified onboarding steps matching the registration journey.