Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
NameEntryOnboardingStepViewModel.cs
1using System;
3using System.Globalization;
4using System.Linq;
5using System.Text;
6using System.Text.RegularExpressions;
7using System.Threading;
8using System.Threading.Tasks;
9using CommunityToolkit.Mvvm.ComponentModel;
10using CommunityToolkit.Mvvm.Input;
11using NeuroAccessMaui;
16using Waher.Networking.XMPP.StanzaErrors; // Added for InternalServerErrorException
17
19{
21 {
22 private CancellationTokenSource? cooldownCts;
23 private bool disposed;
24
25 [ObservableProperty]
26 [NotifyPropertyChangedFor(nameof(IsInCooldown))]
27 [NotifyPropertyChangedFor(nameof(LocalizedContinueText))]
28 private int cooldownSecondsLeft;
29
30 [ObservableProperty]
31 private string? username;
32
33 [ObservableProperty]
34 [NotifyCanExecuteChangedFor(nameof(CreateAccountCommand))]
35 private bool usernameIsValid;
36
37 [ObservableProperty]
38 private string? alternativeName;
39
40 [ObservableProperty]
41 private string? localizedValidationMessage;
42
43 public NameEntryOnboardingStepViewModel() : base(OnboardingStep.NameEntry)
44 {
45 }
46
47 public override async Task OnInitializeAsync()
48 {
49 await base.OnInitializeAsync();
50
51 ServiceRef.XmppService.ConnectionStateChanged += this.XmppService_ConnectionStateChanged;
52
53 if (!string.IsNullOrEmpty(ServiceRef.TagProfile.Account) && this.CoordinatorViewModel is not null)
54 {
55 await this.CoordinatorViewModel.GoToStepCommand.ExecuteAsync(OnboardingStep.CreateAccount);
56 }
57 }
58
59 public override async Task OnDisposeAsync()
60 {
61 ServiceRef.XmppService.ConnectionStateChanged -= this.XmppService_ConnectionStateChanged;
62 this.Dispose();
63
64 await base.OnDisposeAsync();
65 }
66
67 private Task XmppService_ConnectionStateChanged(object? _, XmppState __)
68 {
69 this.CreateAccountCommand.NotifyCanExecuteChanged();
70 return Task.CompletedTask;
71 }
72
73 public bool IsInCooldown => this.CooldownSecondsLeft > 0;
74
75 public bool IsXmppConnected => ServiceRef.XmppService.State == XmppState.Connected;
76
77 public string LocalizedContinueText
78 {
79 get
80 {
81 if (this.CooldownSecondsLeft > 0)
82 return ServiceRef.Localizer[nameof(AppResources.ContinueSecondsFormat), this.CooldownSecondsLeft];
83
85 }
86 }
87
88 public bool CanCreateAccount => this.UsernameIsValid &&
89 !string.IsNullOrEmpty(this.Username) &&
90 string.IsNullOrEmpty(this.AlternativeName) &&
91 !this.IsBusy &&
92 !this.IsInCooldown;
93
94 partial void OnUsernameChanged(string? value)
95 {
96 this.UsernameIsValid = IsValidUsernameString(value);
97 if (this.UsernameIsValid)
98 {
99 this.AlternativeName = string.Empty;
100 this.LocalizedValidationMessage = string.Empty;
101 }
102 else
103 {
104 this.LocalizedValidationMessage = ServiceRef.Localizer[nameof(AppResources.InvalidUsernameCharacters)];
105 this.AlternativeName = this.GenerateUsername(value);
106 }
107
108 this.CreateAccountCommand.NotifyCanExecuteChanged();
109 }
110
111 partial void OnAlternativeNameChanged(string? value)
112 {
113 this.CreateAccountCommand.NotifyCanExecuteChanged();
114 }
115
116 [RelayCommand]
117 private void SelectName(string? name)
118 {
119 if (string.IsNullOrEmpty(name))
120 return;
121
122 this.Username = name;
123 this.AlternativeName = string.Empty;
124 }
125
126 [RelayCommand(CanExecute = nameof(CanCreateAccount))]
127 private async Task CreateAccount()
128 {
129 this.SetIsBusy(true);
130 try
131 {
132 string? Account = this.Username;
133 if (string.IsNullOrEmpty(Account))
134 return;
135
136 string Password = ServiceRef.CryptoService.CreateRandomPassword();
137
138 if (string.IsNullOrWhiteSpace(ServiceRef.TagProfile.Domain))
139 {
141 return;
142 }
143
144 (string HostName, int PortNumber, bool IsIpAddress) = await ServiceRef.NetworkService.LookupXmppHostnameAndPort(ServiceRef.TagProfile.Domain!);
145
146 async Task OnConnected(XmppClient Client)
147 {
149 await ServiceRef.XmppService.DiscoverServices(Client);
150
152
153 if (this.CoordinatorViewModel is not null)
154 await this.CoordinatorViewModel.GoToStepCommand.ExecuteAsync(OnboardingStep.CreateAccount);
155 }
156
157 int Attempt = 0;
158 bool InternalServerErrorRetried = false;
159 while (Attempt < 2)
160 {
161 Attempt++;
162 try
163 {
164 (bool Succeeded, string? ErrorMessage, string[]? Alternatives) = await ServiceRef.XmppService.TryConnectAndCreateAccount(
166 IsIpAddress,
167 HostName,
168 PortNumber,
169 Account,
170 Password,
172 ServiceRef.TagProfile.ApiKey ?? string.Empty,
173 ServiceRef.TagProfile.ApiSecret ?? string.Empty,
174 typeof(App).Assembly,
175 OnConnected);
176
177 if (Alternatives is not null && Alternatives.Length > 0)
178 {
179 Random Rnd = new Random();
180 this.UsernameIsValid = false;
181 this.AlternativeName = Alternatives[Rnd.Next(0, Alternatives.Length)];
182 this.LocalizedValidationMessage = ServiceRef.Localizer[nameof(AppResources.UsernameNameAlreadyTaken)];
183 }
184 else if (!Succeeded && !string.IsNullOrEmpty(ErrorMessage))
185 {
186 await ServiceRef.UiService.DisplayAlert(
187 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)], ErrorMessage,
189 }
190
191 // Exit loop after first successful attempt or handled failure.
192 break;
193 }
195 {
196 if (!InternalServerErrorRetried)
197 {
198 InternalServerErrorRetried = true;
199 ServiceRef.LogService.LogWarning("Internal server error during account creation. Retrying once.");
200 continue; // Immediate retry
201 }
202 ServiceRef.LogService.LogException(Ex);
203 await ServiceRef.UiService.DisplayAlert(
204 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)], Ex.Message,
206 break;
207 }
208 catch (Exception Ex)
209 {
210 ServiceRef.LogService.LogException(Ex);
211 await ServiceRef.UiService.DisplayAlert(
212 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)], Ex.Message,
214 break;
215 }
216 }
217 }
218 finally
219 {
220 await this.StartCooldownAsync();
221 this.SetIsBusy(false);
222 }
223 }
224
225 public override void SetIsBusy(bool isBusy)
226 {
227 bool changed = this.IsBusy != isBusy;
228 base.SetIsBusy(isBusy);
229 if (changed)
230 this.CreateAccountCommand.NotifyCanExecuteChanged();
231 }
232
233 private async Task StartCooldownAsync(int seconds = 3)
234 {
235 this.cooldownCts?.Cancel();
236 this.cooldownCts?.Dispose();
237 this.cooldownCts = new CancellationTokenSource();
238
239 this.CooldownSecondsLeft = seconds;
240 this.CreateAccountCommand.NotifyCanExecuteChanged();
241
242 try
243 {
244 while (this.CooldownSecondsLeft > 0)
245 {
246 await Task.Delay(1000, this.cooldownCts.Token);
247 this.CooldownSecondsLeft--;
248 }
249 }
250 catch (TaskCanceledException)
251 {
252 // Ignore
253 }
254 finally
255 {
256 if (this.CooldownSecondsLeft != 0)
257 {
258 this.CooldownSecondsLeft = 0;
259 }
260 this.CreateAccountCommand.NotifyCanExecuteChanged();
261 }
262 }
263
264 private static bool IsValidUsernameString(string? input)
265 {
266 if (string.IsNullOrEmpty(input))
267 return true;
268
269 foreach (char c in input)
270 {
271 switch (c)
272 {
273 case '"':
274 case '&':
275 case '\'':
276 case '/':
277 case ':':
278 case '<':
279 case '>':
280 case '@':
281 case ' ':
282 case '|':
283 case '\0':
284 case '\u0001':
285 case '\u0002':
286 case '\u0003':
287 case '\u0004':
288 case '\u0005':
289 case '\u0006':
290 case '\a':
291 case '\b':
292 case '\t':
293 case '\n':
294 case '\v':
295 case '\f':
296 case '\r':
297 case '\u000e':
298 case '\u000f':
299 case '\u0010':
300 case '\u0011':
301 case '\u0012':
302 case '\u0013':
303 case '\u0014':
304 case '\u0015':
305 case '\u0016':
306 case '\u0017':
307 case '\u0018':
308 case '\u0019':
309 case '\u001a':
310 case '\u001b':
311 case '\u001c':
312 case '\u001d':
313 case '\u001e':
314 case '\u001f':
315 case '*':
316 case '?':
317 case '\\':
318 return false;
319 }
320 }
321
322 return true;
323 }
324
325 private string? GenerateUsername(string? source)
326 {
327 if (string.IsNullOrWhiteSpace(source))
328 return string.Empty;
329
330 string normalizedSource = source.Normalize(NormalizationForm.FormC).ToLowerInvariant();
331
332 IEnumerable<string?> processedParts = normalizedSource
333 .Split(' ', StringSplitOptions.RemoveEmptyEntries)
334 .Select(ReplaceInvalidUsernameCharacters)
335 .Where(processedPart => !string.IsNullOrEmpty(processedPart));
336
337 if (!processedParts.Any())
338 return string.Empty;
339
340 string result = string.Join(".", processedParts);
341
342 string randomDigits = new Random().Next(0, 10000).ToString("D4", CultureInfo.InvariantCulture);
343 result += randomDigits;
344
345 result = System.Text.RegularExpressions.Regex.Replace(result, @"\.+", ".");
346
347 return result;
348 }
349
350 private static string ReplaceInvalidUsernameCharacters(string input)
351 {
352 StringBuilder sb = new(input.Length);
353 foreach (char c in input)
354 {
355 switch (c)
356 {
357 case '"':
358 case '&':
359 case '\'':
360 case '/':
361 case ':':
362 case '<':
363 case '>':
364 case '@':
365 case ' ':
366 case '|':
367 case '\0':
368 case '\u0001':
369 case '\u0002':
370 case '\u0003':
371 case '\u0004':
372 case '\u0005':
373 case '\u0006':
374 case '\a':
375 case '\b':
376 case '\t':
377 case '\n':
378 case '\v':
379 case '\f':
380 case '\r':
381 case '\u000e':
382 case '\u000f':
383 case '\u0010':
384 case '\u0011':
385 case '\u0012':
386 case '\u0013':
387 case '\u0014':
388 case '\u0015':
389 case '\u0016':
390 case '\u0017':
391 case '\u0018':
392 case '\u0019':
393 case '\u001a':
394 case '\u001b':
395 case '\u001c':
396 case '\u001d':
397 case '\u001e':
398 case '\u001f':
399 case '*':
400 case '?':
401 case '\\':
402 break;
403 default:
404 sb.Append(c);
405 break;
406 }
407 }
408
409 return sb.ToString();
410 }
411
415 public void Dispose()
416 {
417 this.Dispose(true);
418 GC.SuppressFinalize(this);
419 }
420
425 protected virtual void Dispose(bool disposing)
426 {
427 if (this.disposed)
428 return;
429 this.disposed = true;
430
431 if (disposing)
432 {
433 this.cooldownCts?.Cancel();
434 this.cooldownCts?.Dispose();
435 this.cooldownCts = null;
436 }
437 }
438 }
439}
Represents an instance of the Neuro-Access app.
Definition: App.xaml.cs:125
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
A strongly-typed resource class, for looking up localized strings, etc.
static string UsernameNameAlreadyTaken
Looks up a localized string similar to Username already exists. Please try another....
static string ContinueSecondsFormat
Looks up a localized string similar to Continue ({0}).
static string Continue
Looks up a localized string similar to Continue.
static string Ok
Looks up a localized string similar to OK.
static string InvalidUsernameCharacters
Looks up a localized string similar to Username contains invalid characters..
static string UnableToConnect
Looks up a localized string similar to Unable to connect..
static string ErrorTitle
Looks up a localized string similar to An error has occurred.
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 ITagProfile TagProfile
TAG Profile service.
Definition: ServiceRef.cs:202
static ICryptoService CryptoService
Crypto service.
Definition: ServiceRef.cs:286
static IReportingStringLocalizer Localizer
Localization service
Definition: ServiceRef.cs:370
static IXmppService XmppService
The XMPP service for XMPP communication.
Definition: ServiceRef.cs:190
override void SetIsBusy(bool isBusy)
Sets the IsBusy property.
virtual void Dispose(bool disposing)
Disposes resources used by the NameEntryOnboardingStepViewModel.
void Dispose()
Disposes resources used by the NameEntryOnboardingStepViewModel.
override async Task OnDisposeAsync()
Method called when the view is disposed, and will not be used more. Use this method to unregister eve...
override async Task OnInitializeAsync()
Method called when view is initialized for the first time. Use this method to implement registration ...
The server has experienced a misconfiguration or other internal error that prevents it from processin...
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
string PasswordHash
Hash value of password. Depends on method used to authenticate user.
Definition: XmppClient.cs:3475
string? Account
The account name for this profile
Definition: ITagProfile.cs:102
string? ApiKey
API Key, for creating new account.
Definition: ITagProfile.cs:62
string? ApiSecret
API Secret, for creating new account.
Definition: ITagProfile.cs:67
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
Definition: ImplTypes.g.cs:58
OnboardingStep
Unified onboarding steps matching the registration journey.
XmppState
State of XMPP connection.
Definition: XmppState.cs:7