Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
CreateAccountOnboardingStepViewModel.cs
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4using CommunityToolkit.Mvvm.ComponentModel;
5using CommunityToolkit.Mvvm.Input;
6using Microsoft.Extensions.DependencyInjection;
7using Microsoft.Maui.ApplicationModel;
17
19{
24 {
25 private bool hasAppliedForIdentity;
26 private bool callbacksRegistered;
27
28 public CreateAccountOnboardingStepViewModel() : base(OnboardingStep.CreateAccount) { }
29
30 [ObservableProperty]
31 private bool isBusy;
32
33 [ObservableProperty]
34 private string errorMessage = string.Empty;
35
36 [ObservableProperty]
37 private string statusMessage = string.Empty;
38
39 public bool HasError => !string.IsNullOrEmpty(this.ErrorMessage);
40 public bool HasStatusMessage => !string.IsNullOrEmpty(this.StatusMessage);
41 public bool IsXmppConnected => ServiceRef.XmppService.State == XmppState.Connected;
42 public bool IsAccountCreated => !string.IsNullOrEmpty(ServiceRef.TagProfile.Account);
43 public bool IsLegalIdentityCreated => ServiceRef.TagProfile.LegalIdentity is LegalIdentity identity && identity.State == IdentityState.Approved;
44 public bool IsIdentityPendingApproval => ServiceRef.TagProfile.LegalIdentity is LegalIdentity identity && identity.State == IdentityState.Created;
45 public bool IsWaitingForIdentity => this.IsBusy || this.IsIdentityPendingApproval || (this.hasAppliedForIdentity && !this.IsLegalIdentityCreated);
46 public bool CanCreateIdentity => this.IsAccountCreated && ServiceRef.TagProfile.LegalIdentity is null && !this.IsBusy;
47
48 public override async Task OnInitializeAsync()
49 {
50 await base.OnInitializeAsync();
51
52 if (!this.callbacksRegistered)
53 {
54 ServiceRef.XmppService.ConnectionStateChanged += this.XmppService_ConnectionStateChanged;
55 ServiceRef.XmppService.LegalIdentityChanged += this.XmppContracts_LegalIdentityChanged;
56 this.callbacksRegistered = true;
57 }
58
59 if (this.CoordinatorViewModel?.Scenario == OnboardingScenario.ReverifyIdentity &&
61 existingIdentity.State == IdentityState.Approved)
62 {
63 ServiceRef.LogService.LogInformational("Clearing approved identity prior to reverify onboarding step.");
65 }
66
67 await ServiceRef.XmppService.WaitForConnectedState(TimeSpan.FromSeconds(10));
68 await this.TryAutoApplyAsync();
69 await this.CheckApplicationStateAsync();
70 }
71
72 internal override async Task OnActivatedAsync()
73 {
74 await base.OnActivatedAsync();
75 await this.TryAutoApplyAsync();
76 await this.CheckApplicationStateAsync();
77 }
78
79 public override async Task OnDisposeAsync()
80 {
81 if (this.callbacksRegistered)
82 {
83 ServiceRef.XmppService.ConnectionStateChanged -= this.XmppService_ConnectionStateChanged;
84 ServiceRef.XmppService.LegalIdentityChanged -= this.XmppContracts_LegalIdentityChanged;
85 this.callbacksRegistered = false;
86 }
87
88 await base.OnDisposeAsync();
89 }
90
91 private Task XmppService_ConnectionStateChanged(object _, XmppState newState)
92 {
93 if (newState == XmppState.Connected)
94 {
95 MainThread.BeginInvokeOnMainThread(async () =>
96 {
97 await this.TryAutoApplyAsync();
98 await this.CheckApplicationStateAsync();
99 });
100 }
101
102 return Task.CompletedTask;
103 }
104
105 private Task XmppContracts_LegalIdentityChanged(object _, LegalIdentityEventArgs e)
106 {
107 MainThread.BeginInvokeOnMainThread(async () =>
108 {
109 this.OnPropertyChanged(nameof(this.IsLegalIdentityCreated));
110 this.OnPropertyChanged(nameof(this.IsIdentityPendingApproval));
111 this.OnPropertyChanged(nameof(this.IsWaitingForIdentity));
112 await this.CheckApplicationStateAsync();
113 });
114
115 return Task.CompletedTask;
116 }
117
118 private async Task TryAutoApplyAsync()
119 {
120 // Guard: Must have created account and active XMPP connection before applying for identity.
121 if (!this.IsAccountCreated || ServiceRef.XmppService.State != XmppState.Connected)
122 return;
123
124 // Guard: Prevent duplicate attempts while already applied or busy.
125 if (!this.CanCreateIdentity || this.hasAppliedForIdentity)
126 return;
127
128 // Ensure required XMPP services are discovered before attempting identity creation.
130 {
131 try
132 {
133 XmppClient? client = ServiceRef.XmppService.State == XmppState.Connected ? ServiceRef.XmppService.GetType().GetProperty("ContractsClient") is null ? null : null : null; // placeholder to avoid reflection; discovery uses default client
134 await ServiceRef.XmppService.DiscoverServices();
135 }
136 catch (Exception ex)
137 {
138 ServiceRef.LogService.LogException(ex);
139 return; // Abort until services available.
140 }
141 }
142
143 this.SetHasAppliedForIdentity(true);
144 this.ErrorMessage = string.Empty;
145 this.StatusMessage = string.Empty;
146 this.IsBusy = true;
147
148 using CancellationTokenSource timerCts = new(TimeSpan.FromSeconds(Constants.Timeouts.GenericRequest.Seconds));
149 bool appliedSuccessfully = false;
150 LegalIdentity? identity = null;
151
152 while (!timerCts.Token.IsCancellationRequested)
153 {
154 try
155 {
156 RegisterIdentityModel model = this.CreateRegisterModel();
157 LegalIdentityAttachment[] photos = []; // No photos during onboarding
158 (bool succeeded, LegalIdentity? addedIdentity) = await ServiceRef.NetworkService.TryRequest(() => ServiceRef.XmppService.AddLegalIdentity(model, true, photos));
159 if (succeeded && addedIdentity is not null)
160 {
161 identity = addedIdentity;
162 await ServiceRef.TagProfile.SetLegalIdentity(identity, true);
163 this.RegisterIdentityNotificationIgnoreFilter(identity.Id);
164 appliedSuccessfully = true;
166 break;
167 }
168 }
169 catch (Exception ex)
170 {
171 ServiceRef.LogService.LogException(ex);
172 }
173
174 try
175 {
176 await Task.Delay(TimeSpan.FromSeconds(5), timerCts.Token);
177 }
178 catch (TaskCanceledException)
179 {
180 break;
181 }
182 }
183
184 if (!appliedSuccessfully)
185 this.ErrorMessage = ServiceRef.Localizer[nameof(AppResources.PleaseTryAgain)];
186
187 this.IsBusy = false;
188 this.OnPropertyChanged(nameof(this.IsLegalIdentityCreated));
189 this.OnPropertyChanged(nameof(this.IsIdentityPendingApproval));
190 this.OnPropertyChanged(nameof(this.IsWaitingForIdentity));
191
192 if (!appliedSuccessfully)
193 {
195 ServiceRef.LogService.LogWarning("Legal identity application failed during onboarding.");
196 this.SetHasAppliedForIdentity(false); // Allow future retry.
197 }
198 }
199
200 private async Task CheckApplicationStateAsync()
201 {
202 if (ServiceRef.TagProfile.LegalIdentity is not LegalIdentity legalIdentity)
203 {
204 if (!this.IsAccountCreated)
205 return;
206 if (!this.hasAppliedForIdentity)
207 await this.TryAutoApplyAsync();
208 return;
209 }
210
211 switch (legalIdentity.State)
212 {
213 case IdentityState.Approved:
214 this.ErrorMessage = string.Empty;
215 this.StatusMessage = string.Empty;
216 if (this.CoordinatorViewModel is not null)
217 await this.CoordinatorViewModel.GoToStepCommand.ExecuteAsync(OnboardingStep.DefinePassword);
218 break;
219 case IdentityState.Created:
220 this.ErrorMessage = string.Empty;
222 break;
223 case IdentityState.Rejected:
224 case IdentityState.Obsoleted:
225 case IdentityState.Compromised:
226 await this.HandleIdentityFailureAsync(legalIdentity.State);
227 break;
228 default:
229 if (legalIdentity.IsDiscarded())
230 {
231 await this.HandleIdentityFailureAsync(legalIdentity.State);
232 }
233 break;
234 }
235
236 this.OnPropertyChanged(nameof(this.IsLegalIdentityCreated));
237 this.OnPropertyChanged(nameof(this.IsIdentityPendingApproval));
238 this.OnPropertyChanged(nameof(this.IsWaitingForIdentity));
239 }
240
241 private async Task HandleIdentityFailureAsync(IdentityState state)
242 {
243 this.StatusMessage = string.Empty;
244 switch (state)
245 {
246 case IdentityState.Compromised:
248 break;
249 case IdentityState.Obsoleted:
251 break;
252 default:
253 this.ErrorMessage = ServiceRef.Localizer[nameof(AppResources.YourApplicationWasRejected)];
254 break;
255 }
256
258 this.SetHasAppliedForIdentity(false);
259 this.OnPropertyChanged(nameof(this.IsLegalIdentityCreated));
260 this.OnPropertyChanged(nameof(this.IsIdentityPendingApproval));
261
262 if (this.CoordinatorViewModel is not null)
263 await this.CoordinatorViewModel.GoToStepCommand.ExecuteAsync(OnboardingStep.ValidatePhone);
264 }
265
266 private void SetHasAppliedForIdentity(bool value)
267 {
268 if (this.hasAppliedForIdentity == value)
269 return;
270
271 this.hasAppliedForIdentity = value;
272 this.OnPropertyChanged(nameof(this.IsWaitingForIdentity));
273 }
274
275 private RegisterIdentityModel CreateRegisterModel()
276 {
277 RegisterIdentityModel model = new();
278 string value;
279 if (!string.IsNullOrWhiteSpace(value = ServiceRef.TagProfile?.PhoneNumber?.Trim() ?? string.Empty))
280 model.PhoneNr = value;
281 if (!string.IsNullOrWhiteSpace(value = ServiceRef.TagProfile?.EMail?.Trim() ?? string.Empty))
282 model.EMail = value;
283 if (!string.IsNullOrWhiteSpace(value = ServiceRef.TagProfile?.SelectedCountry?.Trim() ?? string.Empty))
284 model.CountryCode = value;
285 return model;
286 }
287
288 private void RegisterIdentityNotificationIgnoreFilter(string IdentityId)
289 {
290 if (string.IsNullOrWhiteSpace(IdentityId))
291 return;
292
294 string IgnoredIdentityId = IdentityId;
295 NotificationService.AddIgnoreFilter((NotificationIntent Intent) =>
296 {
297 if (!string.Equals(Intent.Channel, NeuroAccessMaui.Constants.PushChannels.Identities, StringComparison.OrdinalIgnoreCase))
299
300 if (!string.Equals(Intent.EntityId, IgnoredIdentityId, StringComparison.OrdinalIgnoreCase))
302
303 return new NotificationFilterDecision(true, true, true);
304 });
305 }
306
307 partial void OnStatusMessageChanged(string value)
308 {
309 this.OnPropertyChanged(nameof(this.HasStatusMessage));
310 }
311
312 partial void OnIsBusyChanged(bool value)
313 {
314 this.OnPropertyChanged(nameof(this.IsWaitingForIdentity));
315 }
316 }
317}
const string Identities
Identities channel
Definition: Constants.cs:749
static readonly TimeSpan GenericRequest
Generic request timeout
Definition: Constants.cs:692
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 YourLegalIdentityHasBeenCompromised
Looks up a localized string similar to Your identity has been marked compromised. You have therefore ...
static string IdApplicationSentDescription
Looks up a localized string similar to An application for a new ID has been sent. Waiting for the app...
static string PleaseTryAgain
Looks up a localized string similar to Please Try Again.
static string YourLegalIdentityHasBeenObsoleted
Looks up a localized string similar to Your identity has been marked obsolete. You have therefore bee...
static string YourApplicationWasRejected
Looks up a localized string similar to Your application was rejected..
Represents a filter decision for notification handling.
static NotificationFilterDecision None
Gets a decision that does not ignore any operation.
Platform-neutral intent describing how to route a notification.
string? EntityId
Gets or sets an entity identifier associated with the action.
string? Channel
Gets or sets the push channel identifier.
Base class that references services in the app.
Definition: ServiceRef.cs:43
static IServiceProvider Provider
The service provider for the app. This is set before the app is started, and will be used to resolve ...
Definition: ServiceRef.cs:48
static ILogService LogService
Log service.
Definition: ServiceRef.cs:214
static INetworkService NetworkService
Network service.
Definition: ServiceRef.cs:226
static ITagProfile TagProfile
TAG Profile service.
Definition: ServiceRef.cs:202
static IReportingStringLocalizer Localizer
Localization service
Definition: ServiceRef.cs:370
static IXmppService XmppService
The XMPP service for XMPP communication.
Definition: ServiceRef.cs:190
Mirrors registration create-account behaviour for onboarding: waits for XMPP, applies for identity,...
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 ...
Manages an XMPP client connection. Implements XMPP, as defined in https://tools.ietf....
Definition: XmppClient.cs:58
Interface for the redesigned notification service.
Task SetLegalIdentity(LegalIdentity? Identity, bool RemoveOldAttachments)
Sets the legal identity of the profile.
string? Account
The account name for this profile
Definition: ITagProfile.cs:102
string? EMail
Verified e-mail address.
Definition: ITagProfile.cs:97
Task ClearLegalIdentity()
Revert the Set LegalIdentity
string? SelectedCountry
Selected country. Some countries have the same phone code, so we want to save the selected country
Definition: ITagProfile.cs:72
string? PhoneNumber
Verified phone number.
Definition: ITagProfile.cs:92
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.
Definition: ImplTypes.g.cs:58
OnboardingScenario
Distinct onboarding scenarios.
OnboardingStep
Unified onboarding steps matching the registration journey.
IdentityState
Lists recognized legal identity states.
XmppState
State of XMPP connection.
Definition: XmppState.cs:7