Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ValidatePhoneOnboardingStepViewModel.cs
1using System;
3using System.ComponentModel;
4using System.Globalization;
5using System.Linq;
6using System.Threading.Tasks;
7using CommunityToolkit.Mvvm.ComponentModel;
8using CommunityToolkit.Mvvm.Input;
17using Waher.Content;
18
20{
25 {
26 private bool codeVerified;
27
32 {
33 // Initialize validation flags to true so UI does not show error state initially.
34 this.numberIsValid = true;
35 this.typeIsValid = true;
36 this.lengthIsValid = true;
37 }
38
40 public override async Task OnInitializeAsync()
41 {
42 await base.OnInitializeAsync();
43
44 LocalizationManager.Current.PropertyChanged += this.LocalizationManagerEventHandler;
45
46 if (App.Current is not null)
47 {
48 this.CountDownTimer = App.Current.Dispatcher.CreateTimer();
49 this.CountDownTimer.Interval = TimeSpan.FromMilliseconds(1000);
50 this.CountDownTimer.Tick += this.CountDownEventHandler;
51 }
52
53 string? ExistingNumber = ServiceRef.TagProfile.PhoneNumber;
54 if (string.IsNullOrEmpty(ExistingNumber))
55 {
56 try
57 {
59 new Uri("https://" + Constants.Domains.IdDomain + "/ID/CountryCode.ws"),
60 string.Empty,
61 null,
63 new KeyValuePair<string, string>("Accept", "application/json"));
64 Result.AssertOk();
65
66 if ((Result.Decoded is Dictionary<string, object> Response) &&
67 Response.TryGetValue("CountryCode", out object? CcObj) &&
68 CcObj is string CountryCodeStr &&
69 ISO_3166_1.TryGetCountryByCode(CountryCodeStr, out ISO_3166_Country? Country))
70 {
71 this.SelectedCountry = Country;
72 }
73 }
74 catch (Exception Ex)
75 {
76 ServiceRef.LogService.LogException(Ex);
77 }
78 }
79 else
80 {
81 this.ApplyStoredPhoneNumber(ExistingNumber);
82 }
83 }
84
86 public override async Task OnDisposeAsync()
87 {
88 LocalizationManager.Current.PropertyChanged -= this.LocalizationManagerEventHandler;
89
90 if (this.CountDownTimer is not null)
91 {
92 this.CountDownTimer.Stop();
93 this.CountDownTimer.Tick -= this.CountDownEventHandler;
94 this.CountDownTimer = null;
95 }
96
97 await base.OnDisposeAsync();
98 }
99
103 public void LocalizationManagerEventHandler(object? Sender, PropertyChangedEventArgs E)
104 {
105 this.OnPropertyChanged(nameof(this.Title));
106 this.OnPropertyChanged(nameof(this.Description));
107 this.OnPropertyChanged(nameof(this.LocalizedSendCodeText));
108 this.OnPropertyChanged(nameof(this.LocalizedValidationError));
109 this.OnPropertyChanged(nameof(this.ShowValidationError));
110 }
111
112 #region Observable Properties
113
117 [ObservableProperty]
118 private ISO_3166_Country selectedCountry = ISO_3166_1.DefaultCountry;
119
123 [ObservableProperty]
124 [NotifyCanExecuteChangedFor(nameof(SendCodeCommand))]
125 [NotifyPropertyChangedFor(nameof(ShowValidationError))]
126 private bool numberIsValid;
127
131 [ObservableProperty]
132 [NotifyPropertyChangedFor(nameof(LocalizedValidationError))]
133 [NotifyPropertyChangedFor(nameof(ShowValidationError))]
134 private bool typeIsValid = true;
135
139 [ObservableProperty]
140 [NotifyPropertyChangedFor(nameof(LocalizedValidationError))]
141 [NotifyPropertyChangedFor(nameof(ShowValidationError))]
142 private bool lengthIsValid = true;
143
147 [ObservableProperty]
148 [NotifyPropertyChangedFor(nameof(LocalizedSendCodeText))]
149 [NotifyCanExecuteChangedFor(nameof(SendCodeCommand))]
150 [NotifyCanExecuteChangedFor(nameof(ResendCodeCommand))]
151 private int countDownSeconds;
152
156 [ObservableProperty]
157 private IDispatcherTimer? countDownTimer;
158
162 [ObservableProperty]
163 [NotifyCanExecuteChangedFor(nameof(SendCodeCommand))]
164 [NotifyPropertyChangedFor(nameof(LocalizedValidationError))]
165 [NotifyPropertyChangedFor(nameof(ShowValidationError))]
166 private string phoneNumber = string.Empty;
167
171 [ObservableProperty]
172 private bool isPhoneReadOnly;
173
174 #endregion
175
176 #region Localization Helpers
177
178 public override string Title => ServiceRef.Localizer[nameof(AppResources.OnboardingPhonePageTitle)];
179 public override string Description => ServiceRef.Localizer[nameof(AppResources.OnboardingPhonePageDetails)];
180
181 public string LocalizedSendCodeText
182 {
183 get
184 {
185 if (this.CountDownSeconds > 0)
186 return ServiceRef.Localizer[nameof(AppResources.SendCodeSeconds), this.CountDownSeconds];
188 }
189 }
190
195 {
196 get
197 {
198 if (!this.TypeIsValid)
200 if (!this.LengthIsValid)
202 return string.Empty;
203 }
204 }
205
209 public bool ShowValidationError => !this.NumberIsValid && this.PhoneNumber.Length > 0;
210
211 #endregion
212
213 #region State & Derived
214
218 public bool CanSendCode => this.NumberIsValid && this.PhoneNumber.Length > 0 && this.CountDownSeconds <= 0 && !this.IsBusy;
219 public bool CanResendCode => this.CountDownSeconds <= 0;
220 public bool CanContinue => this.codeVerified;
221
222 #endregion
223
224 private void ApplyStoredPhoneNumber(string StoredNumber)
225 {
226 string Trimmed = StoredNumber.Trim();
227 ISO_3166_Country? ResolvedCountry = null;
228
230 {
231 ResolvedCountry = CountryFromProfile;
232 }
233 else if (ISO_3166_1.TryGetCountryByPhone(Trimmed, out ISO_3166_Country? CountryFromNumber))
234 {
235 ResolvedCountry = CountryFromNumber;
236 }
237
238 if (ResolvedCountry is not null)
239 {
240 this.SelectedCountry = ResolvedCountry;
241 string Digits = Trimmed;
242 if (Digits.StartsWith("+", StringComparison.Ordinal))
243 {
244 Digits = Digits[1..];
245 }
246
247 if (Digits.StartsWith(ResolvedCountry.DialCode, StringComparison.Ordinal))
248 {
249 Digits = Digits[ResolvedCountry.DialCode.Length..];
250 }
251
252 this.PhoneNumber = Digits;
253 }
254 else
255 {
256 this.PhoneNumber = Trimmed.TrimStart('+');
257 }
258 }
259
260 internal override Task OnActivatedAsync()
261 {
262 // Determine read-only state: ReverifyIdentity scenario or FullSetup with existing account.
263 OnboardingViewModel? Coordinator = this.CoordinatorViewModel;
264 if (Coordinator is not null)
265 {
267 bool HasAccount = !string.IsNullOrEmpty(Profile.Account);
268 if (Coordinator.Scenario == OnboardingScenario.ReverifyIdentity || (Coordinator.Scenario == OnboardingScenario.FullSetup && HasAccount))
269 {
270 this.IsPhoneReadOnly = true;
271 }
272 else
273 {
274 this.IsPhoneReadOnly = false;
275 }
276 }
277
278 if (string.IsNullOrEmpty(this.PhoneNumber) && !string.IsNullOrEmpty(ServiceRef.TagProfile.PhoneNumber))
279 {
280 this.ApplyStoredPhoneNumber(ServiceRef.TagProfile.PhoneNumber);
281 }
282
283 return base.OnActivatedAsync();
284 }
285
286 partial void OnPhoneNumberChanged(string value)
287 {
288 if (this.IsPhoneReadOnly)
289 {
290 // Ignore modifications when read-only; revert to stored value.
291 if (!string.IsNullOrEmpty(ServiceRef.TagProfile.PhoneNumber))
292 {
293 this.ApplyStoredPhoneNumber(ServiceRef.TagProfile.PhoneNumber);
294 }
295 return;
296 }
297 bool TypeValid = value.All(char.IsDigit);
298 bool LengthValid = value.Length >= 4;
299 this.TypeIsValid = TypeValid;
300 this.LengthIsValid = LengthValid;
301 this.NumberIsValid = TypeValid && LengthValid;
302 this.SendCodeCommand.NotifyCanExecuteChanged();
303 this.OnPropertyChanged(nameof(this.LocalizedValidationError));
304 this.OnPropertyChanged(nameof(this.ShowValidationError));
305 }
306
307 #region Commands
308
309 [RelayCommand]
310 private async Task SelectPhoneCode()
311 {
312 SelectPhoneCodePopup Popup = new();
313 await ServiceRef.PopupService.PushAsync(Popup);
314 ISO_3166_Country? Result = await Popup.Result;
315 if (Result is not null)
316 this.SelectedCountry = Result;
317 }
318
319 [RelayCommand(CanExecute = nameof(CanSendCode))]
320 private async Task SendCode()
321 {
322 if (!this.NumberIsValid)
323 return;
324
325 this.IsBusy = true;
326 try
327 {
328 if (!ServiceRef.NetworkService.IsOnline)
329 {
333 return;
334 }
335
336 string FullPhoneNumber = "+" + this.SelectedCountry.DialCode + this.PhoneNumber;
337 if (this.SelectedCountry.DialCode == "46")
338 FullPhoneNumber = "+" + this.SelectedCountry.DialCode + this.PhoneNumber.TrimStart('0');
339
340 ContentResponse SendResult = await InternetContent.PostAsync(
341 new Uri("https://" + Constants.Domains.IdDomain + "/ID/SendVerificationMessage.ws"),
342 new Dictionary<string, object>()
343 {
344 { "Nr", FullPhoneNumber },
345 { "AppName", Constants.Application.Name },
346 { "Language", CultureInfo.CurrentCulture.TwoLetterISOLanguageName }
347 },
348 null,
350 new KeyValuePair<string, string>("Accept", "application/json"));
351 SendResult.AssertOk();
352
353 if (SendResult.Decoded is Dictionary<string, object> SendResponse &&
354 SendResponse.TryGetValue("Status", out object? Obj) && Obj is bool SendStatus && SendStatus &&
355 SendResponse.TryGetValue("IsTemporary", out Obj) && Obj is bool SendIsTemporary)
356 {
357 if (!string.IsNullOrEmpty(ServiceRef.TagProfile.PhoneNumber) && (ServiceRef.TagProfile.TestOtpTimestamp is null) && SendIsTemporary)
358 {
362 }
363 else
364 {
365 this.StartTimer();
366 VerifyCodeNavigationArgs NavigationArgs = new(this, FullPhoneNumber);
368 string? Code = await NavigationArgs.VarifyCode!.Task;
369 if (!string.IsNullOrEmpty(Code))
370 {
371 await this.VerifyCodeAsync(Code, FullPhoneNumber).ConfigureAwait(false);
372 }
373 }
374 }
375 else
376 {
380 }
381 }
382 catch (Exception Ex)
383 {
384 ServiceRef.LogService.LogException(Ex);
388 }
389 finally
390 {
391 this.IsBusy = false;
392 }
393 }
394
395 [RelayCommand(CanExecute = nameof(CanResendCode))]
396 private async Task ResendCode()
397 {
398 try
399 {
400 if (!ServiceRef.NetworkService.IsOnline)
401 {
405 return;
406 }
407 string FullPhoneNumber = "+" + this.SelectedCountry.DialCode + this.PhoneNumber;
408 if (this.SelectedCountry.DialCode == "46")
409 FullPhoneNumber = "+" + this.SelectedCountry.DialCode + this.PhoneNumber.TrimStart('0');
410
411 ContentResponse SendResult = await InternetContent.PostAsync(
412 new Uri("https://" + Constants.Domains.IdDomain + "/ID/SendVerificationMessage.ws"),
413 new Dictionary<string, object>()
414 {
415 { "Nr", FullPhoneNumber },
416 { "AppName", Constants.Application.Name },
417 { "Language", CultureInfo.CurrentCulture.TwoLetterISOLanguageName }
418 },
419 null,
421 new KeyValuePair<string, string>("Accept", "application/json"));
422 SendResult.AssertOk();
423
424 if (SendResult.Decoded is Dictionary<string, object> SendResponse &&
425 SendResponse.TryGetValue("Status", out object? Obj) &&
426 Obj is bool SendStatus && SendStatus)
427 {
428 this.StartTimer();
429 }
430 else
431 {
435 }
436 }
437 catch (Exception Ex)
438 {
439 ServiceRef.LogService.LogException(Ex);
443 }
444 }
445
446 #endregion
447
448 #region Verification Logic
449
450 private async Task VerifyCodeAsync(string code, string fullPhoneNumber)
451 {
452 try
453 {
455 bool IsTest = Purpose == PurposeUse.Educational || Purpose == PurposeUse.Experimental;
456
457 ContentResponse VerifyResult = await InternetContent.PostAsync(
458 new Uri("https://" + Constants.Domains.IdDomain + "/ID/VerifyNumber.ws"),
459 new Dictionary<string, object>()
460 {
461 { "Nr", fullPhoneNumber },
462 { "Code", int.Parse(code, NumberStyles.None, CultureInfo.InvariantCulture) },
463 { "Test", IsTest }
464 },
465 null,
467 new KeyValuePair<string, string>("Accept", "application/json"));
468 VerifyResult.AssertOk();
469
470 if (VerifyResult.Decoded is Dictionary<string, object> VerifyResponse &&
471 VerifyResponse.TryGetValue("Status", out object? Obj) && Obj is bool VerifyStatus && VerifyStatus &&
472 VerifyResponse.TryGetValue("Domain", out Obj) && Obj is string VerifyDomain &&
473 VerifyResponse.TryGetValue("Key", out Obj) && Obj is string VerifyKey &&
474 VerifyResponse.TryGetValue("Secret", out Obj) && Obj is string VerifySecret &&
475 VerifyResponse.TryGetValue("Temporary", out Obj) && Obj is bool VerifyIsTemporary)
476 {
477 ServiceRef.TagProfile.SetPhone(this.SelectedCountry.Alpha2, fullPhoneNumber);
478 ServiceRef.TagProfile.SetPurpose(IsTest, Purpose);
479 ServiceRef.TagProfile.TestOtpTimestamp = VerifyIsTemporary ? DateTime.Now : null;
480
481 if (string.IsNullOrEmpty(ServiceRef.TagProfile.Domain))
482 {
483 bool DefaultConnectivity;
484 try
485 {
486 (string HostName, int PortNumber, bool IsIpAddress) = await ServiceRef.NetworkService.LookupXmppHostnameAndPort(VerifyDomain);
487 DefaultConnectivity = HostName == VerifyDomain && PortNumber == Waher.Networking.XMPP.XmppCredentials.DefaultPort;
488 }
489 catch (Exception)
490 {
491 DefaultConnectivity = false;
492 }
493 ServiceRef.TagProfile.SetDomain(VerifyDomain, DefaultConnectivity, VerifyKey, VerifySecret);
494 }
495
496 this.codeVerified = true;
497 this.OnPropertyChanged(nameof(this.CanContinue));
498
499 if (this.CoordinatorViewModel is not null)
500 {
501 OnboardingStep? NextStep = this.CoordinatorViewModel.GetNextActiveStep(this.Step);
502 OnboardingStep TargetStep = NextStep ?? OnboardingStep.Finalize;
503 await this.CoordinatorViewModel.GoToStepCommand.ExecuteAsync(TargetStep);
504 }
505 }
506 else
507 {
512 }
513 }
514 catch (Exception Ex)
515 {
516 ServiceRef.LogService.LogException(Ex);
521 }
522 }
523
524 #endregion
525
526 #region Timer
527
528 private void StartTimer()
529 {
530 if (this.CountDownTimer is not null)
531 {
532#if DEBUG
533 this.CountDownSeconds = 10;
534#else
535 this.CountDownSeconds = 300;
536#endif
537 if (!this.CountDownTimer.IsRunning)
538 this.CountDownTimer.Start();
539 }
540 }
541
542 private void CountDownEventHandler(object? Sender, EventArgs E)
543 {
544 if (this.CountDownTimer is not null)
545 {
546 if (this.CountDownSeconds > 0)
547 this.CountDownSeconds--;
548 else
549 this.CountDownTimer.Stop();
550 }
551 }
552
553 #endregion
554 }
555}
Represents an instance of the Neuro-Access app.
Definition: App.xaml.cs:125
static void ValidateCertificateCallback(object? Sender, RemoteCertificateEventArgs Args)
Callback for validating SSL certificates. This method is called when a remote certificate is received...
Definition: App.xaml.cs:1135
static new? App Current
Gets the current application instance.
Definition: App.xaml.cs:169
const string IdDomain
Neuro-Access domain.
Definition: Constants.cs:307
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 PhoneValidationDigits
Looks up a localized string similar to Only digits are allowed.
static string SwitchingToTestPhoneNumberNotAllowed
Looks up a localized string similar to Changing the phone number to a test one is not allowed....
static string OnboardingPhonePageTitle
Looks up a localized string similar to Please verify phone number.
static string OnboardingPhonePageDetails
Looks up a localized string similar to We’ll send you a verification code.
static string SendCode
Looks up a localized string similar to Send Code.
static string SendCodeSeconds
Looks up a localized string similar to Send Code ({0}).
static string NetworkSeemsToBeMissing
Looks up a localized string similar to Network seems to be missing. Please check your configuration a...
static string Ok
Looks up a localized string similar to OK.
static string UnableToVerifyCode
Looks up a localized string similar to The code was not correct..
static string PhoneValidationLength
Looks up a localized string similar to Minimum length 4 is required.
static string SomethingWentWrongWhenSendingPhoneCode
Looks up a localized string similar to Something went wrong when sending a verification code to this ...
static string ErrorTitle
Looks up a localized string similar to An error has occurred.
Conversion between Country Names and ISO-3166-1 country codes.
Definition: ISO_3166_1.cs:10
static bool TryGetCountryByCode(string? CountryCode, [NotNullWhen(true)] out ISO_3166_Country? Country)
Tries to get the country, given its country code.
Definition: ISO_3166_1.cs:71
static ISO_3166_Country DefaultCountry
This collection built from Wikipedia entry on ISO3166-1 on 9th Feb 2016
Definition: ISO_3166_1.cs:32
static bool TryGetCountryByPhone(string PhoneNumber, [NotNullWhen(true)] out ISO_3166_Country? Country)
Tries to get the country, given its country and phone codes.
Definition: ISO_3166_1.cs:45
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 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 IPopupService PopupService
Popup service for presenting application popups.
Definition: ServiceRef.cs:142
static ITagProfile TagProfile
TAG Profile service.
Definition: ServiceRef.cs:202
static IReportingStringLocalizer Localizer
Localization service
Definition: ServiceRef.cs:370
An base class holding page specific navigation parameters.
Onboarding step ViewModel for validating the phone number. Updated to mirror registration validation ...
override async Task OnInitializeAsync()
Method called when view is initialized for the first time. Use this method to implement registration ...
ValidatePhoneOnboardingStepViewModel()
Creates a new instance of the ValidatePhoneOnboardingStepViewModel class.
void LocalizationManagerEventHandler(object? Sender, PropertyChangedEventArgs E)
Handles localization manager property changes to refresh localized strings.
override async Task OnDisposeAsync()
Method called when the view is disposed, and will not be used more. Use this method to unregister eve...
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).
Class containing credentials for an XMPP client connection.
const int DefaultPort
Default XMPP Server port.
The TAG Profile is the heart of the digital identity for a specific user/device. Use this instance to...
Definition: ITagProfile.cs:18
string? Account
The account name for this profile
Definition: ITagProfile.cs:102
void SetDomain(string DomainName, bool DefaultXmppConnectivity, string Key, string Secret)
Set the domain name to connect to.
string? SelectedCountry
Selected country. Some countries have the same phone code, so we want to save the selected country
Definition: ITagProfile.cs:72
PurposeUse Purpose
Purpose for using the app.
Definition: ITagProfile.cs:212
void SetPhone(string Country, string PhoneNumber)
Sets the phone number used for contacting the user.
string? PhoneNumber
Verified phone number.
Definition: ITagProfile.cs:92
void SetPurpose(bool IsTest, PurposeUse Purpose)
Set if the user choose the educational or experimental purpose.
string? Domain
The domain this profile is connected to.
Definition: ITagProfile.cs:52
Task GoToAsync(string Route)
Navigates to the specified route and pushes the page onto the navigation stack.
Task< bool > DisplayAlert(string Title, string Message, string? Accept=null, string? Cancel=null)
Displays an alert/message box to the user.
class ISO_3166_Country(string Name, string Alpha2, string Alpha3, int NumericCode, string DialCode, EmojiInfo? EmojiInfo=null)
Representation of an ISO3166-1 Country
PurposeUse
For what purpose the app will be used
Definition: PurposeUse.cs:7
BackMethod
Navigation Back Method
Definition: BackMethod.cs:7
OnboardingScenario
Distinct onboarding scenarios.
OnboardingStep
Unified onboarding steps matching the registration journey.
Definition: App.xaml.cs:4