Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ViewContractViewModel.cs
1using System.Collections.ObjectModel;
2using System.Collections.Specialized;
3using System.ComponentModel;
4using System.Text;
5using CommunityToolkit.Maui.Layouts;
6using CommunityToolkit.Mvvm.ComponentModel;
7using CommunityToolkit.Mvvm.Input;
8using Microsoft.Maui.ApplicationModel; // For MainThread marshaling
19using Waher.Events;
26using Waher.Script;
27
29{
34 {
35 #region Fields
36
37 private readonly ViewContractNavigationArgs? args;
38
39 // Refresh coalescing state
40 private readonly object refreshLock = new();
41 private bool refreshInProgress;
42 private bool refreshQueued;
43 private Contract? pendingContractForRefresh;
44 private bool initialized;
45
46 // Awaiting post-create completion flag (bindable)
47 [ObservableProperty]
48 [NotifyPropertyChangedFor(nameof(CanShowSignBar))]
49 private bool isAwaitingPostCreateCompletion;
50
51 // Suppress RefreshView.Command when setting IsRefreshing programmatically
52 private bool suppressNextRefreshCommand;
53
54 #endregion
55
56 #region Constructor
57
62 {
63 this.args = ServiceRef.NavigationService.PopLatestArgs<ViewContractNavigationArgs>();
64
65 this.XmppUriClicked = this.CreateUriCommand(UriScheme.Xmpp);
66 this.IotIdUriClicked = this.CreateUriCommand(UriScheme.IotId);
67 this.IotScUriClicked = this.CreateUriCommand(UriScheme.IotSc);
68 this.NeuroFeatureUriClicked = this.CreateUriCommand(UriScheme.NeuroFeature);
69 this.IotDiscoUriClicked = this.CreateUriCommand(UriScheme.IotDisco);
70 this.EDalerUriClicked = this.CreateUriCommand(UriScheme.EDaler);
71 this.HyperlinkClicked = new Command(async p => await this.ExecuteHyperlinkClicked(p));
72
73 this.contractSignedHandler = new EventHandlerAsync<ContractSignedEventArgs>(this.OnContractSignedAsync);
74 this.contractUpdatedHandler = new EventHandlerAsync<ContractReferenceEventArgs>(this.OnContractUpdatedAsync);
75 }
76
77 #endregion
78
79 #region Initialization and Disposal
80
81 public override async Task OnInitializeAsync()
82 {
83 await base.OnInitializeAsync();
84
85 if (!this.ValidateArgs())
86 return;
87
88 this.SubscribeToEvents();
89
90 try
91 {
92 // Ensure args.Contract is loaded and displayed first
93 await this.LoadContractAsync();
94 await this.InitializeUIAsync();
95 await this.GoToStateAsync(ViewContractStep.Overview);
96 // If navigation provided a post-create completion, await it before enabling signing UI
97 if (this.args?.PostCreateCompletion is not null)
98 {
99 this.IsAwaitingPostCreateCompletion = true;
100 try
101 {
102 Contract? Completed = await this.args.PostCreateCompletion.Task.ConfigureAwait(false);
103 if (Completed is not null)
104 {
105 // Show the completed contract (e.g., signed)
106 await this.RefreshContractAsync(Completed);
107 }
108 }
109 catch (Exception Ex)
110 {
111 ServiceRef.LogService.LogException(Ex);
112 }
113 finally
114 {
115 this.IsAwaitingPostCreateCompletion = false;
116 }
117 }
118
119 // Mark initialized: allow future refreshes, but do not force a refresh here
120 this.initialized = true;
121 }
122 catch (Exception Ex)
123 {
124 ServiceRef.LogService.LogException(Ex);
125 await ServiceRef.UiService.DisplayAlert(
128 await this.GoBack();
129 }
130 }
131
132 public override async Task OnDisposeAsync()
133 {
134 this.UnsubscribeFromEvents();
135 await base.OnDisposeAsync();
136 }
137
138 #endregion
139
140 #region Properties
141
142 protected override void OnPropertyChanged(PropertyChangedEventArgs e)
143 {
144 base.OnPropertyChanged(e);
145 if (e.PropertyName == nameof(this.IsBusy))
146 this.OnPropertyChanged(nameof(this.CanSign));
147 }
148
149 [ObservableProperty]
150 [NotifyPropertyChangedFor(nameof(CanSign))]
151 [NotifyPropertyChangedFor(nameof(CanShowSignBar))]
152 private ObservableContract? contract;
153
154 [ObservableProperty]
155 private bool isRefreshing = false;
156
157 public BindableObject? StateObject { get; set; }
158
159 [ObservableProperty]
160 [NotifyCanExecuteChangedFor(nameof(GoToParametersCommand))]
161 [NotifyCanExecuteChangedFor(nameof(BackCommand))]
162 private bool canStateChange;
163
164 [ObservableProperty]
165 private string currentState = nameof(NewContractStep.Loading);
166
167 [ObservableProperty]
168 [NotifyPropertyChangedFor(nameof(HasHumanReadableText))]
169 private VerticalStackLayout? humanReadableText;
170
171 public bool HasHumanReadableText => this.HumanReadableText is not null;
172
173 public ObservableCollection<ObservableParameter> DisplayableParameters { get; } = new();
174
175 [ObservableProperty]
176 [NotifyPropertyChangedFor(nameof(ReadyToSign))]
177 [NotifyCanExecuteChangedFor(nameof(SignCommand))]
178 private bool isContractOk;
179
180 [ObservableProperty]
181 [NotifyPropertyChangedFor(nameof(HasProposalFriendlyName))]
182 [NotifyPropertyChangedFor(nameof(IsProposal))]
183 private string? proposalFriendlyName;
184
185 [ObservableProperty]
186 [NotifyPropertyChangedFor(nameof(HasProposalRole))]
187 private string? proposalRole;
188
189 [ObservableProperty]
190 [NotifyPropertyChangedFor(nameof(HasProposalMessage))]
191 [NotifyPropertyChangedFor(nameof(IsProposal))]
192 private string? proposalMessage;
193
194 public bool IsProposal =>
195 !string.IsNullOrEmpty(this.ProposalRole) ||
196 !string.IsNullOrEmpty(this.ProposalMessage) ||
197 !string.IsNullOrEmpty(this.ProposalFriendlyName);
198
199 public bool HasProposalFriendlyName => !string.IsNullOrEmpty(this.ProposalFriendlyName);
200 public bool HasProposalRole => !string.IsNullOrEmpty(this.ProposalRole);
201 public bool HasProposalMessage => !string.IsNullOrEmpty(this.ProposalMessage);
202
203 public string? Visibility => this.Contract?.Visibility switch
204 {
205 ContractVisibility.Public => ServiceRef.Localizer[nameof(AppResources.ContractVisibility_Public)],
206 ContractVisibility.CreatorAndParts => ServiceRef.Localizer[nameof(AppResources.ContractVisibility_CreatorAndParts)],
207 ContractVisibility.DomainAndParts => ServiceRef.Localizer[nameof(AppResources.ContractVisibility_DomainAndParts)],
208 ContractVisibility.PublicSearchable => ServiceRef.Localizer[nameof(AppResources.ContractVisibility_PublicSearchable)],
209 _ => string.Empty
210 };
211
212 [ObservableProperty]
213 private bool canDeleteContract;
214
215 [ObservableProperty]
216 private bool canObsoleteContract;
217
218 #endregion
219
220 #region Signing
221
222 public ObservableCollection<ObservableRole> SignableRoles { get; } = new();
223
224 private bool HasSignableRoles => this.SignableRoles.Count > 0;
225 private bool IsInSigningState => this.Contract?.ContractState is ContractState.Approved or ContractState.BeingSigned;
226 private bool AlreadySigned => this.Contract?.Roles.Any(r => r.Parts.Any(p => p.IsMe && p.HasSigned)) == true;
227
228 public bool CanSign => this.HasSignableRoles && this.IsInSigningState && !this.AlreadySigned;
229
230 // Public flag for bottom bar visibility; sign bar only when not awaiting post-create
231 public bool CanShowSignBar => !this.IsAwaitingPostCreateCompletion && this.CanSign;
232
233 public bool ReadyToSign => this.SelectedRole is not null && this.IsContractOk;
234
235 [ObservableProperty]
236 private ObservableRole? selectedRole;
237
238 partial void OnSelectedRoleChanged(ObservableRole? oldValue, ObservableRole? newValue)
239 {
240 var MyLegalId = ServiceRef.TagProfile.LegalIdentity?.Id;
241 if (string.IsNullOrEmpty(MyLegalId))
242 return;
243
244 oldValue?.RemovePart(MyLegalId);
245 _ = newValue?.AddPart(MyLegalId);
246
247 OnPropertyChanged(nameof(ReadyToSign));
248 SignCommand.NotifyCanExecuteChanged();
249 }
250
251 [RelayCommand(AllowConcurrentExecutions = false, CanExecute = nameof(ReadyToSign))]
252 public async Task SignAsync()
253 {
254 if (this.Contract is null || this.SelectedRole is null)
255 return;
256
257 await MainThread.InvokeOnMainThreadAsync(() => this.SetIsBusy(true));
258 try
259 {
260 Contract SignedContract = await ServiceRef.XmppService.SignContract(this.Contract.Contract, this.SelectedRole.Name, false);
261 await this.GoToStateAsync(ViewContractStep.Overview);
262 await this.RefreshContractAsync(SignedContract);
263 }
264 catch (Exception Ex)
265 {
266 ServiceRef.LogService.LogException(Ex);
267 await ServiceRef.UiService.DisplayAlert(
270 }
271 finally
272 {
273 await MainThread.InvokeOnMainThreadAsync(() => this.SetIsBusy(false));
274 }
275 }
276
277 #endregion
278
279 #region Navigation Commands
280
281 [RelayCommand(CanExecute = nameof(CanStateChange))]
282 public async Task BackAsync()
283 {
284 ViewContractStep Step = Enum.Parse<ViewContractStep>(this.CurrentState);
285 if (Step == ViewContractStep.Overview || Step == ViewContractStep.Loading)
286 await base.GoBack();
287 else
288 await this.GoToStateAsync(ViewContractStep.Overview);
289 }
290
291 public override async Task GoBack()
292 {
293 await this.BackAsync();
294 }
295
296 [RelayCommand(CanExecute = nameof(CanStateChange))]
297 private Task GoToParametersAsync() => this.GoToStepAsync(ViewContractStep.Parameters);
298
299 [RelayCommand(CanExecute = nameof(CanStateChange))]
300 private Task GoToRolesAsync() => this.GoToStepAsync(ViewContractStep.Roles);
301
302 [RelayCommand(CanExecute = nameof(CanStateChange))]
303 private async Task GoToSignAsync()
304 {
305 await this.GoToStepAsync(ViewContractStep.Sign);
306 await MainThread.InvokeOnMainThreadAsync(() =>
307 {
308 if (!string.IsNullOrEmpty(this.ProposalRole))
309 this.SelectedRole = this.SignableRoles.FirstOrDefault(r => r.Name == this.ProposalRole);
310 else if (this.SignableRoles.Count == 1)
311 this.SelectedRole = this.SignableRoles[0];
312 });
313 }
314
315 [RelayCommand(CanExecute = nameof(CanStateChange))]
316 private async Task GoToReviewAsync()
317 {
318 if (this.Contract is null)
319 return;
320
321 await this.GoToStepAsync(ViewContractStep.Review, Prepare: async () =>
322 {
323
324 await this.ValidateParametersAsync();
325 VerticalStackLayout? HumanReadableText = await this.Contract.Contract.ToMaui(this.Contract.Contract.DeviceLanguage());
326 this.HumanReadableText = HumanReadableText;
327 });
328 }
329
330 [RelayCommand]
331 private async Task OpenServerSignatureAsync()
332 {
333 if (this.Contract?.Contract is { } ContractObj)
334 await ServiceRef.NavigationService.GoToAsync(nameof(ServerSignaturePage), new ServerSignatureNavigationArgs(ContractObj), Services.UI.BackMethod.Pop);
335 }
336
337 #endregion
338
339 #region Proposals
340
341 [RelayCommand(AllowConcurrentExecutions = false)]
342 private async Task SendProposalToPartAsync(ObservablePart? part)
343 {
344 if (part is null || this.Contract is null)
345 return;
346
347 if (!part.CanSendProposal)
348 return;
349
350 try
351 {
352 ContactInfo? info = await ContactInfo.FindByLegalId(part.LegalId);
353 if (info is null || string.IsNullOrEmpty(info.BareJid))
354 {
355 await ServiceRef.UiService.DisplayAlert(
359 return;
360 }
361
362 await ServiceRef.XmppService.ContractsClient.AuthorizeAccessToContractAsync(
363 this.Contract.ContractId,
364 info.BareJid,
365 true);
366
367 string? friendlyTarget = info.FriendlyName;
368 if (string.IsNullOrEmpty(friendlyTarget))
369 friendlyTarget = part.FriendlyName ?? info.BareJid ?? part.LegalId;
370
371 string? proposal = await ServiceRef.UiService.DisplayPrompt(
372 ServiceRef.Localizer[nameof(AppResources.Proposal)] ?? string.Empty,
373 ServiceRef.Localizer[nameof(AppResources.EnterProposal), friendlyTarget] ?? string.Empty,
374 ServiceRef.Localizer[nameof(AppResources.Send)] ?? string.Empty,
375 ServiceRef.Localizer[nameof(AppResources.Cancel)] ?? string.Empty);
376
377 if(proposal is null) // Dont send if cancelled
378 return;
379 if (string.IsNullOrEmpty(proposal)) // Use default if empty
381
382 await ServiceRef.XmppService.SendContractProposal(
383 this.Contract.Contract,
384 part.Part.Role,
385 info.BareJid,
386 proposal);
387 }
388 catch (Exception Ex)
389 {
390 ServiceRef.LogService.LogException(Ex);
391 await ServiceRef.UiService.DisplayAlert(
392 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)] ?? string.Empty,
393 ServiceRef.Localizer[nameof(AppResources.SomethingWentWrong)] ?? string.Empty,
394 ServiceRef.Localizer[nameof(AppResources.Ok)] ?? string.Empty);
395 }
396 }
397
398 #endregion
399
400 #region Contract Management Commands
401
402 [RelayCommand]
403 private async Task ObsoleteContractAsync()
404 {
405 if (this.Contract is null)
406 return;
407
408 if (!await this.ConfirmAsync(nameof(AppResources.AreYouSureYouWantToObsoleteContract), AuthenticationPurpose.ObsoleteContract))
409 return;
410
411 await ServiceRef.XmppService.ObsoleteContract(this.Contract.ContractId);
412 await ServiceRef.UiService.DisplayAlert(
415 }
416
417 [RelayCommand]
418 private async Task DeleteContractAsync()
419 {
420 if (this.Contract is null)
421 return;
422
423 if (!await this.ConfirmAsync(nameof(AppResources.AreYouSureYouWantToDeleteContract), AuthenticationPurpose.DeleteContract))
424 return;
425
426 await ServiceRef.XmppService.DeleteContract(this.Contract.ContractId);
427 this.Contract.Contract.State = ContractState.Deleted;
428 await this.RefreshContractAsync(this.Contract.Contract);
429
430 await ServiceRef.UiService.DisplayAlert(
433 }
434
435 [RelayCommand]
436 private async Task ShowDetailsAsync()
437 {
438 if (this.Contract is null)
439 return;
440
441 byte[] Xml = Encoding.UTF8.GetBytes(this.Contract.Contract.ForMachines.OuterXml);
442 HttpFileUploadEventArgs Slot = await ServiceRef.XmppService.RequestUploadSlotAsync(
443 this.Contract.ContractId + ".xml", "text/xml; charset=utf-8", Xml.Length);
444
445 if (Slot.Ok)
446 {
447 await Slot.PUT(Xml, "text/xml", (int)Constants.Timeouts.UploadFile.TotalMilliseconds);
448 if (!await App.OpenUrlAsync(Slot.GetUrl, false))
449 await this.CopyAsync(Slot.GetUrl);
450 }
451 else
452 {
453 await ServiceRef.UiService.DisplayException(Slot.StanzaError ?? new Exception(Slot.ErrorText));
454 }
455 }
456
457 #endregion
458
459 #region Clipboard and Link Commands
460
461 [RelayCommand(AllowConcurrentExecutions = false)]
462 private async Task ShareAsync()
463 {
464 if (this.Contract is null)
465 return;
466
467 try
468 {
469 this.GenerateQrCode(this.Contract.Contract.ContractIdUriString);
470 await this.OpenQrPopup("");
471 }
472 catch (Exception Ex)
473 {
474 ServiceRef.LogService.LogException(Ex);
475 }
476
477 }
478
479 [RelayCommand]
480 private async Task CopyAsync(object Item)
481 {
482 this.SetIsBusy(true);
483 try
484 {
485 string Text = Item switch
486 {
487 string Id when Id == this.Contract?.ContractId
488 => $"{Constants.UriSchemes.IotSc}:{this.Contract.ContractId}",
489 string Other => Other,
490 _ => Item?.ToString() ?? string.Empty
491 };
492
493 await Clipboard.SetTextAsync(Text);
494
495 string Key = (Item is string Id2 && Id2 == this.Contract?.ContractId)
498
499 await ServiceRef.UiService.DisplayAlert(
501 ServiceRef.Localizer[Key]);
502 }
503 finally { this.SetIsBusy(false); }
504 }
505
506 [RelayCommand]
507 private static Task OpenContractAsync(object Item)
508 {
509 if (Item is string Id)
510 return App.OpenUrlAsync(Constants.UriSchemes.IotSc + ":" + Id);
511 return Task.CompletedTask;
512 }
513
514 [RelayCommand]
515 private async Task OpenLinkAsync(object Item)
516 {
517 if (Item is string Url && !await App.OpenUrlAsync(Url, false))
518 await this.CopyAsync(Url);
519 else
520 await this.CopyAsync(Item);
521 }
522
523 #endregion
524
525 #region State Navigation Helpers
526
527 private Command CreateUriCommand(UriScheme Scheme)
528 => new Command(async Parameter => await this.ExecuteUriClicked(Parameter, Scheme));
529
530 private async Task GoToStepAsync(ViewContractStep Step, Func<Task>? Prepare = null)
531 {
532 await this.GoToStateAsync(ViewContractStep.Loading);
533 if (Prepare is not null) await Prepare();
534 await this.GoToStateAsync(Step);
535 }
536
537 private async Task GoToStateAsync(ViewContractStep Step)
538 {
539 if (this.StateObject is null)
540 return;
541
542 string NewState = Step.ToString();
543 if (NewState == this.CurrentState)
544 return;
545
546 while (!this.CanStateChange)
547 await Task.Delay(100);
548
549 await MainThread.InvokeOnMainThreadAsync(async () =>
550 {
551 await StateContainer.ChangeStateWithAnimation(this.StateObject, NewState);
552 });
553 }
554
555 private Task SetCanStateChangeOnMainThreadAsync(bool value)
556 {
557 return MainThread.InvokeOnMainThreadAsync(() =>
558 {
559 this.CanStateChange = value;
560 });
561 }
562
563 #endregion
564
565 #region Event Wiring
566 private readonly EventHandlerAsync<ContractReferenceEventArgs> contractUpdatedHandler;
567 private readonly EventHandlerAsync<ContractSignedEventArgs> contractSignedHandler;
568
569 private void SubscribeToEvents()
570 {
571 ServiceRef.XmppService.ContractUpdated += this.contractUpdatedHandler;
572 ServiceRef.XmppService.ContractSigned += this.contractSignedHandler;
573 this.SignableRoles.CollectionChanged += this.OnSignableRolesChanged;
574 }
575
576 private void UnsubscribeFromEvents()
577 {
578 ServiceRef.XmppService.ContractUpdated -= this.contractUpdatedHandler;
579 ServiceRef.XmppService.ContractSigned -= this.contractSignedHandler;
580 this.SignableRoles.CollectionChanged -= this.OnSignableRolesChanged;
581 }
582
583 private void OnSignableRolesChanged(object? sender, NotifyCollectionChangedEventArgs e)
584 => MainThread.BeginInvokeOnMainThread(() =>
585 {
586 this.OnPropertyChanged(nameof(this.CanSign));
587 this.OnPropertyChanged(nameof(this.CanShowSignBar));
588 });
589
590 private async Task OnContractSignedAsync(object? sender, ContractSignedEventArgs e)
591 {
592 if (e.ContractId != this.Contract?.ContractId || e.LegalId == ServiceRef.TagProfile.LegalIdentity?.Id)
593 return;
594
595 // Prefer incoming contract if it's newer than what we have locally
596 Contract? Current = this.Contract?.Contract;
597 Contract? Incoming = e.Contract;
598 if (Current is null || Incoming is null)
599 return;
600
601 bool UseIncoming = false;
602 DateTime? CurrentTs = Current.ServerSignature?.Timestamp;
603 DateTime? IncomingTs = Incoming.ServerSignature?.Timestamp;
604
605 if (IncomingTs.HasValue && CurrentTs.HasValue)
606 {
607 UseIncoming = IncomingTs.Value > CurrentTs.Value;
608 }
609 else
610 {
611 DateTime? CurrentUpdated = Current.Updated;
612 DateTime? IncomingUpdated = Incoming.Updated;
613 if (IncomingUpdated.HasValue && CurrentUpdated.HasValue)
614 UseIncoming = IncomingUpdated.Value > CurrentUpdated.Value;
615 else if (IncomingUpdated.HasValue && !CurrentUpdated.HasValue)
616 UseIncoming = true; // Prefer newer info if local timestamp missing
617 else
618 UseIncoming = true; // If we can't compare reliably, accept incoming to be safe
619 }
620
621 if (UseIncoming)
622 this.RequestRefresh(Incoming);
623
624 await Task.CompletedTask;
625 }
626
627 private async Task OnContractUpdatedAsync(object? sender, ContractReferenceEventArgs e)
628 {
629 if (e.ContractId != this.Contract?.ContractId)
630 return;
631
632 // Coalesce refresh requests
633 this.RequestRefresh(null);
634 await Task.CompletedTask;
635 }
636
637 #endregion
638
639 #region Private Helpers
640
644 private async Task ValidateParametersAsync()
645 {
646 if (this.Contract is null)
647 return;
648
649 try
650 {
651 // Step 1: Get the variables and prepare the parameters to validate
652 Variables Variables = [];
653
654
655 Variables["Duration"] = this.Contract.Contract.Duration;
656
657 DateTime? FirstSignature = this.Contract.Contract.FirstSignatureAt;
658 if (FirstSignature.HasValue)
659 {
660 Variables["Now"] = FirstSignature.Value.ToLocalTime();
661 Variables["NowUtc"] = FirstSignature.Value.ToUniversalTime();
662 }
663
664 foreach (ObservableParameter ParamLoop in this.Contract.Parameters)
665 ParamLoop.Parameter.Populate(Variables);
666
667 // Step 2: Prepare to collect validation results
668 List<(ObservableParameter Param, bool IsValid, string ValidationText)> ValidationResults = [];
669
671 try
672 {
673 ContractsClient = ServiceRef.XmppService.ContractsClient;
674 }
675 catch (Exception)
676 {
677 // Ignore, client might not be available currently
678 }
679
680 Task<(ObservableParameter Param, bool IsValid, string ValidationText)>[] ValidationTasks = this.Contract.Parameters.Select(async ParamToValidate =>
681 {
682 bool IsValid = false;
683 string ValidationText = string.Empty;
684 try
685 {
686 IsValid = await ParamToValidate.Parameter.IsParameterValid(Variables, ServiceRef.XmppService.ContractsClient).ConfigureAwait(false);
687 IsValid = IsValid || ParamToValidate.Parameter.ErrorText == ContractStatus.ClientIdentityInvalid.ToString();
688 ValidationText = ParamToValidate.Parameter.ErrorText;
689 }
690 catch (Exception Ex2)
691 {
692 ServiceRef.LogService.LogException(Ex2);
693 IsValid = true;
694 }
695 return (Param: ParamToValidate, IsValid, ValidationText);
696 }).ToArray();
697
698 (ObservableParameter Param, bool IsValid, string ValidationText)[] Results = await Task.WhenAll(ValidationTasks);
699 }
700 catch (Exception Ex)
701 {
702 ServiceRef.LogService.LogException(Ex);
703 }
704 }
705 private bool ValidateArgs()
706 {
707 return this.args is not null && (this.args.Contract is not null || this.args.ContractRef is not null);
708 }
709
710 private async Task LoadContractAsync()
711 {
712 if (this.args!.ContractRef is null)
713 {
714 this.Contract = await ObservableContract.CreateAsync(this.args!.Contract!);
715 }
716 else
717 {
718 Contract? Contract = await this.args!.ContractRef!.GetContract();
719
720 if (this.args.ContractRef.ContractId is null)
721 {
722 await this.GoBack();
723 }
724
725 try
726 {
727 Contract ??= await ServiceRef.XmppService.GetContract(this.args!.ContractRef!.ContractId!);
728 }
730 {
731 if (await ServiceRef.UiService.DisplayAlert(
735 {
736 await Database.FindDelete<ContractReference>(new FilterFieldEqualTo("ContractId", this.args.ContractRef.ContractId));
737 await this.GoBack();
738 }
739
740 return;
741 }
742 catch (Exception Ex)
743 {
744 ServiceRef.LogService.LogException(Ex);
745 await ServiceRef.UiService.DisplayAlert(
746 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)], Ex.Message,
748
749 await this.GoBack();
750 }
751
752 this.Contract = await ObservableContract.CreateAsync(Contract!);
753 }
754 }
755
756 private async Task InitializeUIAsync()
757 {
758 await MainThread.InvokeOnMainThreadAsync(async () =>
759 {
760 this.ProposalFriendlyName = await this.ResolveProposalFriendlyNameAsync();
761 this.ProposalRole = this.args!.Role ?? string.Empty;
762 this.ProposalMessage = this.args!.Proposal ?? string.Empty;
763 });
764
765 await MainThread.InvokeOnMainThreadAsync(this.PrepareDisplayableParameters);
766 await MainThread.InvokeOnMainThreadAsync(this.PrepareSignableRoles);
767 await MainThread.InvokeOnMainThreadAsync(this.PreparePropertiesAsync);
768
769 MainThread.BeginInvokeOnMainThread(() =>
770 {
771 if (!string.IsNullOrEmpty(this.ProposalRole))
772 this.SelectedRole = this.SignableRoles.FirstOrDefault(r => r.Name == this.ProposalRole);
773 else if (this.SignableRoles.Count == 1)
774 this.SelectedRole = this.SignableRoles[0];
775 this.OnPropertyChanged(nameof(this.CanSign));
776 this.OnPropertyChanged(nameof(this.ReadyToSign));
777 this.OnPropertyChanged(nameof(this.CanShowSignBar));
778
779 });
780 }
781
782 private async Task<string> ResolveProposalFriendlyNameAsync()
783 {
784 if (string.IsNullOrEmpty(this.args!.Proposal) || string.IsNullOrEmpty(this.args.FromJID))
785 return string.Empty;
786
787 try
788 {
789 ContactInfo Info = await ContactInfo.FindByBareJid(this.args.FromJID);
790 return !string.IsNullOrEmpty(Info?.FriendlyName)
791 ? Info.FriendlyName
792 : this.args.FromJID;
793 }
794 catch
795 {
796 return string.Empty;
797 }
798 }
799
800 private void PrepareDisplayableParameters()
801 {
802 this.DisplayableParameters.Clear();
803 if (this.Contract?.Parameters is null)
804 return;
805
806 Variables Vars = [];
807
808 Vars["Duration"] = this.Contract.Contract.Duration;
809
810 DateTime? FirstSignature = this.Contract.Contract.FirstSignatureAt;
811 if (FirstSignature.HasValue)
812 {
813 Vars["Now"] = FirstSignature.Value.ToLocalTime();
814 Vars["NowUtc"] = FirstSignature.Value.ToUniversalTime();
815 }
816
817 foreach (ObservableParameter Parameter in this.Contract.Parameters)
818 {
819 Parameter.Parameter.Populate(Vars);
820
824 {
825 this.DisplayableParameters.Add(Parameter);
826 }
827 }
828/*
829 foreach (ObservableParameter P in this.DisplayableParameters)
830 {
831 P.Parameter.IsParameterValid(Vars, ServiceRef.XmppService.ContractsClient);
832 ServiceRef.LogService.LogDebug($"Parameter '{P.Parameter.Name}' validation result: {P.IsValid}, Error: {P.Parameter.ErrorReason} - {P.ValidationText}");
833 }
834*/
835 }
836
837 private void PrepareSignableRoles()
838 {
839 this.SignableRoles.Clear();
840 if (this.Contract is null)
841 return;
842
843 if (!string.IsNullOrEmpty(this.ProposalRole))
844 {
845 ObservableRole? Role = this.Contract.Roles.FirstOrDefault(r => r.Name == this.ProposalRole);
846 if (Role is not null)
847 this.SignableRoles.Add(Role);
848 }
849 else if (this.Contract.Contract.PartsMode == ContractParts.Open)
850 {
851 foreach (ObservableRole Role in this.Contract.Roles)
852 if (!Role.HasReachedMaxCount)
853 this.SignableRoles.Add(Role);
854 }
855 else
856 {
857 foreach (ObservableRole Role in this.Contract.Roles)
858 if (Role.Parts.Any(p => p.IsMe))
859 this.SignableRoles.Add(Role);
860 }
861 }
862
863 private async Task PreparePropertiesAsync()
864 {
865 if (this.Contract is null)
866 return;
867
868 foreach (ObservableRole? Role in this.Contract.Roles.Where(R => R.Parts.Any(P => P.IsMe)))
869 {
870 if (Role.Role.CanRevoke)
871 this.CanObsoleteContract = this.Contract.ContractState is ContractState.Approved or ContractState.BeingSigned or ContractState.Signed;
872 }
873
874 if (this.args is not null)
875 {
876 try
877 {
878 bool Binding = await this.Contract.Contract.IsLegallyBinding(true, ServiceRef.XmppService.ContractsClient);
879 MainThread.BeginInvokeOnMainThread(() =>
880 {
881 this.CanDeleteContract = !this.args.IsReadOnly && !Binding;
882 });
883 }
884 catch (Exception Ex)
885 {
886 this.CanDeleteContract = false;
887 ServiceRef.LogService.LogException(Ex);
888 }
889 }
890 }
891
892 [RelayCommand(AllowConcurrentExecutions = false)]
893 private Task RefreshContractAsync(Contract? newContract)
894 {
895 // If the command was triggered by our programmatic IsRefreshing change, ignore once
896 if (this.suppressNextRefreshCommand)
897 {
898 this.suppressNextRefreshCommand = false;
899 return Task.CompletedTask;
900 }
901
902 this.RequestRefresh(newContract);
903 return Task.CompletedTask;
904 }
905
906 // Coalesced refresh entry
907 private void RequestRefresh(Contract? newContract)
908 {
909 lock (this.refreshLock)
910 {
911 if (newContract is not null)
912 this.pendingContractForRefresh = newContract;
913
914 this.refreshQueued = true;
915
916 // Wait until initialized (first UI shown) before allowing refreshes to run
917 if (this.refreshInProgress)
918 return;
919
920 this.refreshInProgress = true;
921 }
922
923 _ = this.ProcessRefreshQueueAsync();
924 }
925
926 private async Task ProcessRefreshQueueAsync()
927 {
928 try
929 {
930 // Ensure initial contract displayed
931 while (!this.initialized || this.Contract is null)
932 await Task.Delay(50);
933
934 while (true)
935 {
936 Contract? ToUse;
937 lock (this.refreshLock)
938 {
939 if (!this.refreshQueued)
940 {
941 this.refreshInProgress = false;
942 return;
943 }
944
945 this.refreshQueued = false;
946 ToUse = this.pendingContractForRefresh;
947 this.pendingContractForRefresh = null;
948 }
949
950 await this.DoRefreshAsync(ToUse);
951 }
952 }
953 finally
954 {
955 lock (this.refreshLock)
956 {
957 this.refreshInProgress = false;
958 this.refreshQueued = false;
959 this.pendingContractForRefresh = null;
960 }
961 }
962 }
963
964 private async Task DoRefreshAsync(Contract? newContract)
965 {
966 if (this.Contract is null)
967 return;
968
969 ServiceRef.LogService.LogDebug($"RefreshContractAsync start for {this.Contract.ContractId}");
970 bool previousStateChange = this.CanStateChange;
971 await this.SetCanStateChangeOnMainThreadAsync(false); // Gate state transitions during refresh
972
973 await MainThread.InvokeOnMainThreadAsync(() =>
974 {
975 if (!this.IsRefreshing)
976 {
977 this.suppressNextRefreshCommand = true;
978 this.IsRefreshing = true;
979 }
980 });
981
982 try
983 {
984 newContract ??= await ServiceRef.XmppService.GetContract(this.Contract.ContractId);
985 }
986 catch (ForbiddenException)
987 {
988 if (await ServiceRef.UiService.DisplayAlert(
993 {
994 if (!await ServiceRef.NetworkService.TryRequest(
995 () => ServiceRef.XmppService.PetitionContract(
996 this.Contract.ContractId,
997 Guid.NewGuid().ToString(),
999 {
1000 MainThread.BeginInvokeOnMainThread(() =>
1001 {
1002 this.IsRefreshing = false;
1003 });
1004 return;
1005 }
1006 }
1007 ;
1008 }
1009 catch (ItemNotFoundException)
1010 {
1011 if (await ServiceRef.UiService.DisplayAlert(
1015 {
1016 await Database.FindDelete<ContractReference>(new FilterFieldEqualTo("ContractId", this.Contract.ContractId));
1017 }
1018 }
1019 catch
1020 {
1021 MainThread.BeginInvokeOnMainThread(() =>
1022 {
1023 this.IsRefreshing = false;
1024 });
1025 return; // Ignore other exceptions
1026 }
1027
1028 if (newContract is null || newContract.ServerSignature.Timestamp == this.Contract.Contract.ServerSignature.Timestamp)
1029 {
1030 MainThread.BeginInvokeOnMainThread(() =>
1031 {
1032 this.IsRefreshing = false;
1033 });
1034 await this.SetCanStateChangeOnMainThreadAsync(previousStateChange);
1035 ServiceRef.LogService.LogDebug("RefreshContractAsync skipped (no changes)");
1036 return;
1037 }
1038
1039 ObservableContract Wrapper = new ObservableContract(newContract);
1040 ViewContractStep CurrentStep = Enum.Parse<ViewContractStep>(this.CurrentState);
1041
1042 await MainThread.InvokeOnMainThreadAsync(async () =>
1043 {
1044 this.SelectedRole = null;
1045 this.Contract = Wrapper;
1046 await this.Contract.InitializeAsync();
1047 });
1048
1049 await MainThread.InvokeOnMainThreadAsync(async () =>
1050 {
1051 this.PrepareDisplayableParameters();
1052 this.PrepareSignableRoles();
1053 await this.PreparePropertiesAsync();
1054 this.OnPropertyChanged(nameof(this.CanSign));
1055 this.OnPropertyChanged(nameof(this.CanShowSignBar));
1056 });
1057
1058 ContractReference Ref = await Database.FindFirstDeleteRest<ContractReference>(
1059 new FilterFieldEqualTo("ContractId", this.Contract.ContractId));
1060
1061 if (Ref is not null)
1062 {
1063 await Ref.SetContract(newContract);
1064 await Database.Update(Ref);
1065 }
1066
1067 await this.SetCanStateChangeOnMainThreadAsync(previousStateChange);
1068 await this.GoToStateAsync(CurrentStep);
1069 ServiceRef.LogService.LogDebug($"RefreshContractAsync completed for {this.Contract.ContractId}");
1070
1071 MainThread.BeginInvokeOnMainThread(() =>
1072 {
1073 this.IsRefreshing = false;
1074 });
1075 }
1076
1077 private async Task<bool> ConfirmAsync(string resourceKey, AuthenticationPurpose purpose)
1078 {
1079 if (!await AreYouSure(ServiceRef.Localizer[resourceKey]))
1080 return false;
1081 return await ServiceRef.AuthenticationService.AuthenticateUserAsync(purpose, true);
1082 }
1083
1084 #endregion
1085
1086 #region ILinkableView Implementation
1087
1088 public override string? Link { get; }
1089 public override Task<string> Title => ContractModel.GetName(this.Contract?.Contract);
1090
1091 #endregion
1092
1093 #region Markdown Link Handlers
1094
1095 public Command XmppUriClicked { get; }
1096 public Command IotIdUriClicked { get; }
1097 public Command IotScUriClicked { get; }
1098 public Command NeuroFeatureUriClicked { get; }
1099 public Command IotDiscoUriClicked { get; }
1100 public Command EDalerUriClicked { get; }
1101 public Command HyperlinkClicked { get; }
1102
1103 private async Task ExecuteUriClicked(object? parameter, UriScheme scheme)
1104 {
1105 if (parameter is string Uri)
1106 await App.OpenUrlAsync(Uri);
1107 }
1108
1109 private async Task ExecuteHyperlinkClicked(object? parameter)
1110 {
1111 if (parameter is string Url)
1112 await App.OpenUrlAsync(Url);
1113 }
1114
1115 #endregion
1116 }
1117}
Represents an instance of the Neuro-Access app.
Definition: App.xaml.cs:125
static readonly TimeSpan UploadFile
Upload file timeout
Definition: Constants.cs:712
const string IotSc
The IoT Smart Contract URI Scheme (iotsc)
Definition: Constants.cs:163
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 Yes
Looks up a localized string similar to Yes.
static string ContractHasBeenObsoleted
Looks up a localized string similar to Contract has been obsoleted..
static string Cancel
Looks up a localized string similar to Cancel.
static string ContractVisibility_Public
Looks up a localized string similar to Public, not searchable.
static string ContractVisibility_PublicSearchable
Looks up a localized string similar to Public and searchable.
static string ContractVisibility_DomainAndParts
Looks up a localized string similar to Domain and parts.
static string RefreshContract_Forbidden_Description
Looks up a localized string similar to We couldn’t load the latest version of this contract....
static string ContractVisibility_CreatorAndParts
Looks up a localized string similar to Creator and parts.
static string ProposalDefaultMessage
Looks up a localized string similar to Hi, I would like to propose a contract with you....
static string ContractCouldNotBeFound
Looks up a localized string similar to Contract could not be found, it could have been deleted....
static string TagValueCopiedToClipboard
Looks up a localized string similar to Tag value copied to clipboard.
static string NetworkAddressOfContactUnknown
Looks up a localized string similar to Network address of contact unknown..
static string RefreshContract_Forbidden_Title
Looks up a localized string similar to Permission Required.
static string Send
Looks up a localized string similar to Send.
static string EnterProposal
Looks up a localized string similar to Enter the text to include in the proposal to {0},...
static string AreYouSureYouWantToDeleteContract
Looks up a localized string similar to Are you sure you want to delete this contract?...
static string ContractHasBeenDeleted
Looks up a localized string similar to Contract has been deleted..
static string AreYouSureYouWantToObsoleteContract
Looks up a localized string similar to Are you sure you want to obsolete this contract?...
static string ContractIdCopiedSuccessfully
Looks up a localized string similar to A link to the contract was copied to the clipboard....
static string RequestToAccessContract
Looks up a localized string similar to Request to access contract.
static string Ok
Looks up a localized string similar to OK.
static string No
Looks up a localized string similar to No.
static string Proposal
Looks up a localized string similar to Proposal.
static string SomethingWentWrong
Looks up a localized string similar to Something went wrong.
static string SuccessTitle
Looks up a localized string similar to Success.
static string ErrorTitle
Looks up a localized string similar to An error has occurred.
Contains information about a contact.
Definition: ContactInfo.cs:22
static async Task< ContactInfo?> FindByLegalId(string LegalId)
Finds information about a contact, given its Legal ID.
Definition: ContactInfo.cs:248
static Task< ContactInfo > FindByBareJid(string BareJid)
Finds information about a contact, given its Bare JID.
Definition: ContactInfo.cs:221
CaseInsensitiveString BareJid
Bare JID of contact.
Definition: ContactInfo.cs:63
Contains a local reference to a contract that the user has created or signed.
async Task SetContract(Contract Contract)
Sets a parsed contract.
Base class that references services in the app.
Definition: ServiceRef.cs:43
static IAuthenticationService AuthenticationService
Authentication service.
Definition: ServiceRef.cs:457
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 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
virtual void SetIsBusy(bool IsBusy)
Sets the IsBusy property.
static async Task< string > GetName(Contract? Contract)
Gets a displayable name for a contract.
An observable object that wraps a Contract object. This allows for easier binding in the UI....
static async Task< ObservableContract > CreateAsync(Contract contract)
Creates a new instance of ObservableContract and initializes the roles and parameters.
An observable object that wraps a Waher.Networking.XMPP.Contracts.Parameter object....
An observable object that wraps a Waher.Networking.XMPP.Contracts.Role object. This allows for easier...
async Task AddPart(string LegalId, bool Notify=true, bool AutoPetition=true, bool PresetFromArgs=false)
Adds a part with a given LegalId to the role.
void RemovePart(string LegalId, bool Notify=true)
Removes a part with a given LegalId from the role.
View model for displaying and managing a contract in the "View Contract" page.
ViewContractViewModel()
Default constructor. Retrieves navigation arguments and sets up commands.
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 GoBack()
Method called when user wants to navigate to the previous screen.
override async Task OnInitializeAsync()
Method called when view is initialized for the first time. Use this method to implement registration ...
A view model that holds the XMPP state.
async Task OpenQrPopup(string? Title)
Open QR Popup
void GenerateQrCode(string Uri)
Generates a QR-code
Holds navigation parameters specific to views displaying a server signature.
Calculation contractual parameter
Contains the definition of a contract
Definition: Contract.cs:22
Parameter[] Parameters
Defined parameters for the smart contract.
Definition: Contract.cs:267
DateTime Updated
When the contract was last updated
Definition: Contract.cs:139
Role[] Roles
Roles defined in the smart contract.
Definition: Contract.cs:240
string ContractId
Contract identity
Definition: Contract.cs:65
ServerSignature ServerSignature
Server signature attesting to the validity of the contents of the contract.
Definition: Contract.cs:327
ContractVisibility Visibility
Contrat Visibility
Definition: Contract.cs:184
Contract()
Contains the definition of a contract
Definition: Contract.cs:57
Adds support for legal identities, smart contracts and signatures to an XMPP client.
Geo-spatial contractual parameter
Definition: GeoParameter.cs:17
Abstract base class for contractual parameters
Definition: Parameter.cs:17
abstract void Populate(Variables Variables)
Populates a variable collection with the value of the parameter.
string Role
Role of the part in the contract
Definition: Part.cs:57
Class defining a role
Definition: Role.cs:7
bool CanRevoke
If parts having this role, can revoke their signature, once signed.
Definition: Role.cs:44
DateTime Timestamp
Timestamp of signature.
Definition: Signature.cs:18
String-valued contractual parameter
bool Ok
If the response is an OK result response (true), or an error response (false).
XmppException StanzaError
Any stanza error returned.
Event arguments for HTTP File Upload callback methods.
Task PUT(byte[] Content, string ContentType, int Timeout)
Uploads file content to the server.
The requesting entity does not possess the necessary permissions to perform an action that only certa...
The addressed JID or item requested cannot be found; the associated error type SHOULD be "cancel".
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static Task< IEnumerable< object > > FindDelete(string Collection, params string[] SortOrder)
Finds objects in a given collection and deletes them in the same atomic operation.
Definition: Database.cs:1442
static async Task Update(object Object)
Updates an object in the database.
Definition: Database.cs:1211
This filter selects objects that have a named field equal to a given value.
Collection of variables.
Definition: Variables.cs:25
Definition: ImplTypes.g.cs:58
NewContractStep
The different steps of creating a new contract.
ViewContractStep
The different steps of creating a new contract.
AuthenticationPurpose
Purpose for requesting the user to authenticate itself.
ContractParts
How the parts of the contract are defined.
Definition: Part.cs:9
ContractStatus
Validation Status of smart contract
ContractState
Recognized contract states
Definition: Enumerations.cs:7