Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
NewContractViewModel.cs
1using CommunityToolkit.Maui.Layouts;
2using CommunityToolkit.Mvvm.ComponentModel;
3using CommunityToolkit.Mvvm.Input;
13using System.Collections.ObjectModel;
14using System.ComponentModel;
15using System.Text;
16using Waher.Content;
18using Waher.Script;
20using System.Linq;
21
22using Timer = System.Timers.Timer;
23
25{
26 public partial class NewContractViewModel : BaseViewModel, ILinkableView, IDisposable
27 {
28 #region Constructors
29
34 {
35 this.args = ServiceRef.NavigationService.PopLatestArgs<NewContractNavigationArgs>();
36
37 this.SelectedContractVisibilityItem = this.ContractVisibilityItems[0];
38 }
39
40 #endregion
41
42 #region Fields
43
44 private readonly NewContractNavigationArgs? args;
45 private System.Timers.Timer? debounceValidationTimer;
46 private readonly object debounceLock = new();
47 private Task? latestValidationTask;
48 private bool suppressParameterValidation;
49 private TaskCompletionSource<Contract?>? postCreateCompletion;
50
51
52 #endregion
53
54 #region Properties
55 // If roles were preselected via args, the user cannot change their own role selections
56 [ObservableProperty]
57 private bool areRolesLockedForMe;
58
59 [ObservableProperty]
60 private ObservableContract? contract;
61
62 [ObservableProperty]
63 [NotifyCanExecuteChangedFor(nameof(GoToParametersCommand))]
64 [NotifyCanExecuteChangedFor(nameof(BackCommand))]
65 private bool canStateChange;
66
67 [ObservableProperty]
68 private string currentState = nameof(NewContractStep.Loading);
69
70 partial void OnCurrentStateChanged(string value)
71 {
72 this.OnPropertyChanged(nameof(this.IsOnRolesStep));
73 this.OnPropertyChanged(nameof(this.ShowNoRolesWarning));
74 this.OnPropertyChanged(nameof(this.CanGoBack));
75 }
76
77 // Wizard steps (Phase 1 skeleton)
78 public ObservableCollection<StepDescriptor> Steps { get; } = new();
79
80 [ObservableProperty]
81 private StepDescriptor? currentStep;
82
83 [ObservableProperty]
84 private bool isCurrentStepValid;
85
86 [ObservableProperty]
87 private bool isOnPreviewStep;
88
89 [ObservableProperty]
90 private bool isValidatingParameters;
91
92 // Removed final state concept
93
94 [ObservableProperty]
95 private bool isTransientPreview;
96
97 partial void OnIsTransientPreviewChanged(bool value)
98 {
99 this.OnPropertyChanged(nameof(this.CanGoBack));
100 }
101
102 private Contract? lastCreatedContract;
103
107 [ObservableProperty]
108 [NotifyPropertyChangedFor(nameof(CanCreate))]
109 [NotifyCanExecuteChangedFor(nameof(CreateCommand))]
110 private bool isValidationDisabled = false;
111
112 public string ProgressText => this.CurrentStep is null ? string.Empty : ServiceRef.Localizer[nameof(AppResources.ContractWizardStepFormat), this.CurrentStep.Index + 1, this.Steps.Count] ?? string.Empty;
113
114 public string PrimaryActionText =>
115 (this.CurrentStep is not null && this.Steps.Count > 0 && this.CurrentStep.Index >= this.Steps.Count - 1)
116 ? (ServiceRef.Localizer[nameof(AppResources.Create)] ?? "Create")
118
119 public bool CanGoBack =>
120 (this.CurrentStep?.Index ?? 0) > 0 ||
121 (this.IsTransientPreview && this.CurrentState == nameof(NewContractStep.Preview));
122
123 partial void OnCurrentStepChanged(StepDescriptor? oldValue, StepDescriptor? newValue)
124 {
125 if (oldValue is not null)
126 oldValue.IsCurrent = false;
127 if (newValue is not null)
128 {
129 newValue.IsCurrent = true;
130 newValue.IsVisited = true;
131 this.IsOnPreviewStep = newValue.Key == nameof(NewContractStep.Preview);
132 this.OnPropertyChanged(nameof(this.ProgressText));
133 this.OnPropertyChanged(nameof(this.PrimaryActionText));
134 this.OnPropertyChanged(nameof(this.CanGoBack));
135 this.NavigateStateForStep(newValue);
136 _ = this.UpdateCurrentStepValidityAsync();
137 this.OnPropertyChanged(nameof(this.IsOnRolesStep));
138 this.OnPropertyChanged(nameof(this.ShowNoRolesWarning));
139 }
140 }
141
142 [ObservableProperty]
143 private string contractName = string.Empty;
144
145 [ObservableProperty]
146 [NotifyPropertyChangedFor(nameof(HasHumanReadableText))]
147 private VerticalStackLayout? humanReadableText;
148
149 public ObservableCollection<ObservableRole> AvailableRoles { get; set; } = [];
150
155 {
156 get
157 {
158 string? MyId = ServiceRef.TagProfile.LegalIdentity?.Id;
159 if (this.Contract is null || string.IsNullOrEmpty(MyId))
160 return false;
161 foreach (ObservableRole Role in this.Contract.Roles)
162 {
163 if (Role.Parts.Any(p => p.LegalId == MyId))
164 return true;
165 }
166 return false;
167 }
168 }
169
170 public bool IsOnRolesStep => this.CurrentState == nameof(NewContractStep.Roles);
171
172 public bool ShowNoRolesWarning => this.IsOnRolesStep && !this.HasSelectedRoles;
173
174 public ObservableCollection<ObservableParameter> EditableParameters { get; set; } = [];
175
179 public bool HasHumanReadableText => this.HumanReadableText is not null;
180
184 public BindableObject? StateObject { get; set; }
185
189 public ObservableCollection<ContractVisibilityModel> ContractVisibilityItems { get; } =
190 [
195 ];
196
200 [ObservableProperty]
201 [NotifyPropertyChangedFor(nameof(CanCreate))]
202 private ContractVisibilityModel? selectedContractVisibilityItem;
203
204
205 public bool HasRoles => this.Contract is not null && this.Contract.Roles.Count > 0;
206
207 public bool HasParameters => this.Contract is not null && this.EditableParameters.Count > 0;
208
212 [ObservableProperty]
213 [NotifyPropertyChangedFor(nameof(CanCreate))]
214 [NotifyCanExecuteChangedFor(nameof(CreateCommand))]
215 private bool isParametersOk;
216
220 [ObservableProperty]
221 [NotifyPropertyChangedFor(nameof(CanCreate))]
222 [NotifyCanExecuteChangedFor(nameof(CreateCommand))]
223 private bool isRolesOk;
224
228 [ObservableProperty]
229 [NotifyPropertyChangedFor(nameof(CanCreate))]
230 [NotifyCanExecuteChangedFor(nameof(CreateCommand))]
231 private bool isContractOk;
232
236
237 public bool CanCreate =>
238 (this.IsParametersOk && this.IsRolesOk && this.IsContractOk
239 && this.SelectedContractVisibilityItem is not null)
240 || this.IsValidationDisabled;
241
242 partial void OnIsContractOkChanged(bool value)
243 {
244 if (this.CurrentStep?.Key == nameof(NewContractStep.Preview))
245 _ = this.UpdateCurrentStepValidityAsync();
246 }
247
248 #endregion
249
250 #region Methods
252 public override async Task OnInitializeAsync()
253 {
254
255 await base.OnInitializeAsync();
256
257 if (this.args is null || this.args?.Template is null)
258 {
263 await this.GoBack();
264 return;
265 }
266
267 try
268 {
269 this.Contract = await ObservableContract.CreateAsync(this.args.Template);
270 this.Contract.ParameterChanged += this.Parameter_PropertyChanged;
271 this.Contract.PartChanged += (_, __) =>
272 {
273 this.OnPropertyChanged(nameof(this.HasSelectedRoles));
274 this.OnPropertyChanged(nameof(this.ShowNoRolesWarning));
275 _ = this.UpdateCurrentStepValidityAsync();
276 };
277
278
279 TaskCompletionSource<bool> HasInitializedParameters = new();
280
281 MainThread.BeginInvokeOnMainThread(async () =>
282 {
283 if (this.args.ParameterValues is not null)
284 {
285 // Set the parameter values
287 {
288 if (this.args.ParameterValues.TryGetValue(Parameter.Parameter.Name, out object? Value))
289 Parameter.Value = Value;
290 }
291
292 lock (this.debounceLock)
293 {
294 if (this.debounceValidationTimer is not null)
295 {
296 this.debounceValidationTimer.Stop();
297 this.debounceValidationTimer.Dispose();
298 this.debounceValidationTimer = null;
299 }
300 }
301 // Set Role values
302 foreach (ObservableRole RoleItem in this.Contract.Roles)
303 {
304 if (this.args.ParameterValues.TryGetValue(RoleItem.Role.Name, out object? RoleValue))
305 {
306 if (RoleValue is string LegalID)
307 await RoleItem.AddPart(LegalID, PresetFromArgs: true);
308 }
309 }
310 // If my own ID was preselected for any role, lock role selection
311 string? MyIdInit = ServiceRef.TagProfile.LegalIdentity?.Id;
312 if (!string.IsNullOrEmpty(MyIdInit))
313 {
314 this.AreRolesLockedForMe = this.Contract.Roles.Any(r => r.Parts.Any(p => p.LegalId == MyIdInit));
315 }
316 }
318 {
319 if (Parameter.Parameter is BooleanParameter
320 || Parameter.Parameter is StringParameter
321 || Parameter.Parameter is NumericalParameter
322 || Parameter.Parameter is DateParameter
323 || Parameter.Parameter is DateTimeParameter
324 || Parameter.Parameter is TimeParameter
325 || Parameter.Parameter is DurationParameter
327 || Parameter.Parameter is GeoParameter)
328 {
329 this.EditableParameters.Add(Parameter);
330 }
331 }
332 this.OnPropertyChanged(nameof(this.HasRoles));
333 this.OnPropertyChanged(nameof(this.HasParameters));
334
335 HasInitializedParameters.SetResult(true);
336 });
337 await HasInitializedParameters.Task;
338 // One-time validation after presets, no debounce
339 await this.ValidateParametersAsync();
340 this.InitializeSteps();
341
342 // Multi-select: do not auto-select any role. Keep user in control.
343
344 await this.GoToState(NewContractStep.Intro);
345 this.CurrentStep = this.Steps.FirstOrDefault(Step => Step.Key == nameof(NewContractStep.Intro));
346 }
347 catch (Exception Ex4)
348 {
349 ServiceRef.LogService.LogException(Ex4);
354 await this.GoBack();
355 // TODO: Handle error, perhaps change to an error state
356 }
357 }
358
360 public override async Task OnDisposeAsync()
361 {
362 if (this.Contract is not null)
363 {
364 this.Contract.ParameterChanged -= this.Parameter_PropertyChanged;
365 }
366 await base.OnDisposeAsync();
367 }
368
375 private async Task GoToState(NewContractStep newStep)
376 {
377 if (this.StateObject is null)
378 return;
379
380 string NewState = newStep.ToString();
381
382 if (NewState == this.CurrentState)
383 return;
384
385 while (!this.CanStateChange)
386 await Task.Delay(100);
387
388 await MainThread.InvokeOnMainThreadAsync(async () =>
389 {
390 await StateContainer.ChangeStateWithAnimation(this.StateObject, NewState);
391 });
392 }
393
394 private void InitializeSteps()
395 {
396 if (this.Steps.Count > 0)
397 return;
398
399 string[] Order = [nameof(NewContractStep.Intro), nameof(NewContractStep.Parameters), nameof(NewContractStep.Roles), nameof(NewContractStep.Preview)];
400 int I = 0;
401 foreach (string Key in Order)
402 {
403 Func<Task<bool>>? ValidateFunc = null;
404 if (Key == nameof(NewContractStep.Intro))
405 {
406 // Intro is informational; always valid
407 ValidateFunc = () => Task.FromResult(true);
408 }
409 else if (Key == nameof(NewContractStep.Parameters))
410 {
411 ValidateFunc = () =>
412 {
413 if (this.IsValidationDisabled)
414 return Task.FromResult(true);
415 bool Ok = true;
416 foreach (ObservableParameter Param in this.EditableParameters)
417 {
418 if (Param.Value is null || !Param.IsValid)
419 {
420 Ok = false;
421 break;
422 }
423 }
424 this.IsParametersOk = Ok;
425 return Task.FromResult(Ok);
426 };
427 }
428 else if (Key == nameof(NewContractStep.Roles))
429 {
430 ValidateFunc = () => Task.FromResult(this.IsValidationDisabled || this.CheckRolesValid());
431 }
432 else if (Key == nameof(NewContractStep.Preview))
433 {
434 ValidateFunc = () => Task.FromResult(this.IsValidationDisabled || this.IsContractOk);
435 }
436
437 this.Steps.Add(new StepDescriptor
438 {
439 Key = Key,
440 Title = Key,
441 Index = I++,
442 ValidateAsync = ValidateFunc
443 });
444 }
445 }
446
447 private bool CheckRolesValid()
448 {
449 if (this.Contract is null)
450 return false;
451
452 // Must have at least one role selected by the current user
453 bool HasMySelection = this.HasSelectedRoles;
454
455 // All roles must meet their minimum part requirements
456 bool MinCountsOk = true;
457 foreach (ObservableRole Role in this.Contract.Roles)
458 {
459 if (Role.Parts.Count < Role.MinCount)
460 {
461 MinCountsOk = false;
462 break;
463 }
464 }
465
466 bool Ok = HasMySelection && MinCountsOk;
467 this.IsRolesOk = Ok;
468 return Ok;
469 }
470
471 private async Task UpdateCurrentStepValidityAsync()
472 {
473 if (this.CurrentStep?.ValidateAsync is null)
474 {
475 this.IsCurrentStepValid = true;
476 return;
477 }
478 if (this.IsValidationDisabled)
479 {
480 this.IsCurrentStepValid = true;
481 this.CurrentStep.IsComplete = true;
482 return;
483 }
484 bool Ok = false;
485 try
486 {
487 Ok = await this.CurrentStep.ValidateAsync();
488 }
489 catch (Exception Ex)
490 {
491 ServiceRef.LogService.LogException(Ex);
492 }
493 this.IsCurrentStepValid = Ok;
494 this.CurrentStep.IsComplete = Ok;
495 }
496
497 partial void OnIsValidationDisabledChanged(bool value)
498 {
499 _ = this.UpdateCurrentStepValidityAsync();
500 }
501
502 private async void NavigateStateForStep(StepDescriptor Step)
503 {
504 try
505 {
506 NewContractStep Target = (NewContractStep)Enum.Parse(typeof(NewContractStep), Step.Key);
507 if (Target != NewContractStep.Loading && this.CurrentState != Step.Key)
508 {
509 if (Target == NewContractStep.Preview)
510 {
511 // Ensure preview is generated when entering preview step
512 await this.GoToPreview();
513 }
514 else if (Target == NewContractStep.Parameters)
515 {
516 // Suppress validation noise caused by initial UI bindings when entering the Parameters step
517 lock (this.debounceLock)
518 {
519 if (this.debounceValidationTimer is not null)
520 {
521 this.debounceValidationTimer.Stop();
522 this.debounceValidationTimer.Dispose();
523 this.debounceValidationTimer = null;
524 }
525 }
526 this.suppressParameterValidation = true;
527
528 await this.GoToState(Target);
529
530 // Clear suppression on the next UI tick after controls have bound
531 MainThread.BeginInvokeOnMainThread(async () =>
532 {
533 await Task.Delay(50);
534 this.suppressParameterValidation = false;
535 });
536 }
537 else if (Target == NewContractStep.Roles)
538 {
539 // Auto-select the only available role if exactly one can be chosen
540 if (this.Contract is not null && !this.HasSelectedRoles)
541 {
542 string? MyId = ServiceRef.TagProfile.LegalIdentity?.Id;
543 if (!string.IsNullOrEmpty(MyId))
544 {
545 List<ObservableRole> Joinable = this.Contract.Roles
546 .Where(r => r.Parts.Count < r.MaxCount && !r.Parts.Any(p => p.LegalId == MyId))
547 .ToList();
548 if (Joinable.Count == 1)
549 {
550 try
551 {
552 await Joinable[0].AddPart(MyId, false);
553 }
554 catch (Exception Ex)
555 {
556 ServiceRef.LogService.LogException(Ex);
557 }
558 }
559 }
560 }
561
562 await this.GoToState(Target);
563 }
564 else
565 {
566 await this.GoToState(Target);
567 }
568 }
569 }
570 catch (Exception Ex)
571 {
572 ServiceRef.LogService.LogException(Ex);
573 }
574 }
575
576 [RelayCommand]
577 private void GoNextStep()
578 {
579 if (this.CurrentStep is null)
580 return;
581 int NextIndex = this.CurrentStep.Index + 1;
582 if (NextIndex < this.Steps.Count)
583 {
584 this.CurrentStep = this.Steps[NextIndex];
585 }
586 else if (this.CurrentStep.Key == nameof(NewContractStep.Preview))
587 {
588 _ = this.CreateAsync();
589 }
590 }
591
592 [RelayCommand]
593 private void GoPreviousStep()
594 {
595 if (this.CurrentStep is null)
596 return;
597 // Special case: transient preview -> always return to Intro regardless of CurrentStep pointer
598 if (this.IsTransientPreview && this.CurrentState == nameof(NewContractStep.Preview))
599 {
600 this.IsTransientPreview = false;
601 // Don't advance step progression; just go back to Intro state
602 _ = this.GoToState(NewContractStep.Intro);
603 return;
604 }
605
606 int PrevIndex = this.CurrentStep.Index - 1;
607 if (PrevIndex >= 0)
608 this.CurrentStep = this.Steps[PrevIndex];
609 }
610
611 [RelayCommand]
612 private void GoToStep(string? stepKey)
613 {
614 if (string.IsNullOrEmpty(stepKey))
615 return;
616 StepDescriptor? Target = this.Steps.FirstOrDefault(S => S.Key == stepKey);
617 if (Target is null)
618 return;
619
620 // Only allow navigating to steps up to the first incomplete one
621 int FirstIncomplete = this.Steps.TakeWhile(S => S.IsComplete || S.IsCurrent).Count();
622 if (Target.Index <= FirstIncomplete)
623 this.CurrentStep = Target;
624 }
625
626 [RelayCommand]
627 private void GoToStepDescriptor(StepDescriptor? step)
628 {
629 if (step is null)
630 return;
631 int FirstIncomplete = this.Steps.TakeWhile(S => S.IsComplete || S.IsCurrent).Count();
632 if (step.Index <= FirstIncomplete)
633 this.CurrentStep = step;
634 }
635
639 public async Task<bool> CheckCanCreateAsync()
640 {
641 if (this.Contract is null)
642 return false;
643
644 await this.FlushValidationAsync();
645
646 bool ParametersOk = true;
647 foreach (ObservableParameter ParamItem in this.EditableParameters)
648 {
649 if (ParamItem.Value is null || !ParamItem.IsValid)
650 {
651 ParametersOk = false;
652 break;
653 }
654 }
655
656 bool RolesOk = this.HasSelectedRoles;
657 if (RolesOk)
658 {
659 foreach (ObservableRole Role in this.Contract.Roles)
660 {
661 if (Role.Parts.Count < Role.MinCount)
662 {
663 RolesOk = false;
664 break;
665 }
666 }
667 }
668
669 MainThread.BeginInvokeOnMainThread(() =>
670 {
671 this.IsParametersOk = ParametersOk;
672 this.IsRolesOk = RolesOk;
673 });
674 return this.CanCreate;
675 }
676
680 private async Task ValidateParametersAsync()
681 {
682 if (this.Contract is null)
683 return;
684
685 try
686 {
687 // Step 1: Get the variables and prepare the parameters to validate
688 Variables Variables = [];
689
690 Variables["Duration"] = this.Contract.Contract.Duration;
691
692 DateTime? FirstSignature = this.Contract.Contract.FirstSignatureAt;
693 if (FirstSignature.HasValue)
694 {
695 Variables["Now"] = FirstSignature.Value.ToLocalTime();
696 Variables["NowUtc"] = FirstSignature.Value.ToUniversalTime();
697 }
698
699 foreach (ObservableParameter ParamLoop in this.Contract.Parameters)
700 ParamLoop.Parameter.Populate(Variables);
701
702 // Step 2: Prepare to collect validation results
703 List<(ObservableParameter Param, bool IsValid, string ValidationText)> ValidationResults = [];
704
706 try
707 {
708 ContractsClient = ServiceRef.XmppService.ContractsClient;
709 }
710 catch (Exception)
711 {
712 // Ignore, client might not be available currently
713 }
714
715 Task<(ObservableParameter Param, bool IsValid, string ValidationText)>[] ValidationTasks = this.Contract.Parameters.Select(async ParamToValidate =>
716 {
717 bool IsValid = false;
718 string ValidationText = string.Empty;
719 try
720 {
721 if (await ParamToValidate.Parameter.IsParameterValid(Variables, ServiceRef.XmppService.ContractsClient).ConfigureAwait(false))
722 {
723 IsValid = true;
724 ValidationText = string.Empty;
725 }
726 else if(ParamToValidate.Value is null)
727 {
728 ValidationText = string.Empty;
729 }
730 else
731 {
732 IsValid = IsValid || ParamToValidate.Parameter.ErrorText == ContractStatus.ClientIdentityInvalid.ToString();
733 ValidationText = ParamToValidate.Parameter.ErrorText;
734 }
735
736 // Optional: keep debug noise low when simply entering the Parameters step
737 if (!this.suppressParameterValidation)
738 ServiceRef.LogService.LogDebug($"Parameter '{ParamToValidate.Parameter.Name}' validation result: {IsValid}, Error: {ParamToValidate.Parameter.ErrorReason} - {ValidationText}");
739 }
740 catch (Exception Ex2)
741 {
742 ServiceRef.LogService.LogException(Ex2);
743 IsValid = true;
744 }
745 return (Param: ParamToValidate, IsValid, ValidationText);
746 }).ToArray();
747
748 (ObservableParameter Param, bool IsValid, string ValidationText)[] Results = await Task.WhenAll(ValidationTasks);
749
750 // Update UI in a batch on the main thread:
751 await MainThread.InvokeOnMainThreadAsync(() =>
752 {
753 foreach ((ObservableParameter Param, bool IsValid, string ValidationText) Result in Results)
754 {
755 Result.Param.IsValid = Result.IsValid;
756 Result.Param.ValidationText = Result.ValidationText;
757 }
758 // Reflect aggregate state for step gating without triggering another validation run
759 this.IsParametersOk = this.EditableParameters.All(p => p.Value is not null && p.IsValid);
760 });
761 }
762 catch (Exception Ex)
763 {
764 ServiceRef.LogService.LogException(Ex);
765 }
766 }
767
768 private void DebounceValidateParameters()
769 {
770 lock (this.debounceLock)
771 {
772 if (this.debounceValidationTimer is not null)
773 {
774 this.debounceValidationTimer.Stop();
775 this.debounceValidationTimer.Dispose();
776 this.debounceValidationTimer = null;
777 }
778
779 this.debounceValidationTimer = new Timer(700); // e.g., 1.5 seconds
780 this.debounceValidationTimer.Elapsed += async (s, e) =>
781 {
782 lock (this.debounceLock)
783 {
784 this.debounceValidationTimer?.Stop();
785 this.debounceValidationTimer?.Dispose();
786 this.debounceValidationTimer = null;
787 }
788
789 // Run and store validation task
790 Task ValidationTask = MainThread.InvokeOnMainThreadAsync(async () =>
791 {
792 await this.ValidateParametersAsync();
793 });
794
795 // Store the task for possible awaiting later
796 this.latestValidationTask = ValidationTask;
797 await ValidationTask;
798 // After validation finishes, update step validity based on current flags (no re-validation)
799 await MainThread.InvokeOnMainThreadAsync(async () =>
800 {
801 await this.UpdateCurrentStepValidityAsync();
802 });
803 };
804 this.debounceValidationTimer.AutoReset = false;
805 this.debounceValidationTimer.Start();
806 }
807 }
808 private async Task FlushValidationAsync()
809 {
810 Task? ValidationTask = null;
811 this.IsValidatingParameters = true;
812
813 lock (this.debounceLock)
814 {
815 if (this.debounceValidationTimer is not null)
816 {
817 this.debounceValidationTimer.Stop();
818 this.debounceValidationTimer.Dispose();
819 this.debounceValidationTimer = null;
820 ValidationTask = MainThread.InvokeOnMainThreadAsync(this.ValidateParametersAsync);
821 this.latestValidationTask = ValidationTask;
822 }
823 else if (this.latestValidationTask is not null)
824 {
825 ValidationTask = this.latestValidationTask;
826 }
827 else
828 {
829 ValidationTask = MainThread.InvokeOnMainThreadAsync(this.ValidateParametersAsync);
830 this.latestValidationTask = ValidationTask;
831 }
832 }
833
834 if (ValidationTask is not null)
835 await ValidationTask;
836 this.IsValidatingParameters = false;
837 }
838
839
840
841 #endregion
842
843 #region Commands
844
845 [RelayCommand(CanExecute = nameof(CanCreate), AllowConcurrentExecutions = false)]
846 private async Task CreateAsync()
847 {
848 if (this.Contract is null)
849 return;
850
851 await this.GoToState(NewContractStep.Loading);
852
853 if (!await ServiceRef.AuthenticationService.AuthenticateUserAsync(AuthenticationPurpose.SignContract, true))
854 {
855 await this.GoToState(NewContractStep.Preview);
856 return;
857 }
858
859 ContractsClient Client = ServiceRef.XmppService.ContractsClient;
860
861 Contract? CreatedContract = null;
862 List<Part> Parts = [];
863 foreach (ObservableRole Role in this.Contract.Roles)
864 {
865 foreach (ObservablePart Part in Role.Parts)
866 {
867 Parts.Add(Part.Part);
868 }
869 }
870
871 try
872 {
873 CreatedContract = await Client.CreateContractAsync(
874 this.Contract.Contract.ContractId,
875 [.. Parts],
876 this.Contract.Contract.Parameters,
877 this.SelectedContractVisibilityItem?.Visibility ?? this.Contract.Visibility,
878 ContractParts.ExplicitlyDefined,
879 this.Contract.Contract.Duration ?? Duration.FromYears(1),
880 this.Contract.Contract.ArchiveRequired ?? Duration.FromYears(5),
881 this.Contract.Contract.ArchiveOptional ?? Duration.FromYears(5),
882 null, null, false);
883 this.lastCreatedContract = CreatedContract;
884 await this.OpenCreatedContract();
885 // Sign for all selected roles (could be none)
886 string? MyId = ServiceRef.TagProfile.LegalIdentity?.Id;
887 if (!string.IsNullOrEmpty(MyId))
888 {
889 foreach (ObservableRole Role in this.Contract.Roles)
890 {
891 if (Role.Parts.Any(p => p.LegalId == MyId))
892 {
893 CreatedContract = await ServiceRef.XmppService.SignContract(CreatedContract, Role.Name, false);
894 }
895 }
896 }
897
898 foreach (Part Part in Parts)
899 {
900 if (this.args?.SuppressedProposalLegalIds is not null && Array.IndexOf<CaseInsensitiveString>(this.args.SuppressedProposalLegalIds, Part.LegalId) >= 0)
901 continue;
902
903 if (Part.LegalId == ServiceRef.TagProfile.LegalIdentity?.Id)
904 continue;
905
907 if (Info is null || string.IsNullOrEmpty(Info.BareJid))
908 continue;
909 await ServiceRef.XmppService.ContractsClient.AuthorizeAccessToContractAsync(CreatedContract.ContractId, Info.BareJid, true);
910
915
916 if (!string.IsNullOrEmpty(Proposal))
917 await ServiceRef.XmppService.SendContractProposal(CreatedContract, Part.Role, Info.BareJid, Proposal);
918 else
919 await ServiceRef.XmppService.SendContractProposal(CreatedContract, Part.Role, Info.BareJid, ServiceRef.Localizer[nameof(AppResources.ProposalDefaultMessage)]);
920 }
921 }
923 {
926 Ex.Message,
928 }
929 catch (Exception Ex)
930 {
931 ServiceRef.LogService.LogException(Ex);
932
933 //Todo: display contract errors
934
935 }
936
937 if (CreatedContract is null)
938 {
943 await this.GoToState(NewContractStep.Parameters);
944 return;
945 }
946
947 // Directly open created contract (no Final step)
948 this.lastCreatedContract = CreatedContract;
949 // Complete the post-create TCS so ViewContract can update signing UI
950 this.postCreateCompletion?.TrySetResult(CreatedContract);
951 this.postCreateCompletion = null;
952
953 }
954
955
960 [RelayCommand(CanExecute = nameof(CanStateChange))]
961 public async Task Back()
962 {
963 try
964 {
965 NewContractStep CurrentStep = (NewContractStep)Enum.Parse(typeof(NewContractStep), this.CurrentState);
966
967 switch (CurrentStep)
968 {
969 case NewContractStep.Loading:
970 await base.GoBack();
971 break;
972 case NewContractStep.Preview when this.IsTransientPreview:
973 this.IsTransientPreview = false;
974 await this.GoToState(NewContractStep.Intro);
975 break;
976 default:
977 if (this.CanGoBack)
978 {
979 this.GoPreviousStep();
980 }
981 else
982 {
983 await base.GoBack();
984 }
985 break;
986 }
987 }
988 catch (Exception Ex3)
989 {
990 ServiceRef.LogService.LogException(Ex3);
991 }
992 }
993
994 public override async Task GoBack()
995 {
996 await this.Back();
997 }
998
999 [RelayCommand]
1000 private async Task OpenCreatedContract()
1001 {
1002 if (this.lastCreatedContract is null)
1003 return;
1004 TaskCompletionSource<Contract?> Tcs = new TaskCompletionSource<Contract?>();
1005 ViewContractNavigationArgs Args = new(this.lastCreatedContract, false, null, string.Empty, null, Tcs);
1007 this.postCreateCompletion = Tcs;
1008 }
1009
1013 [RelayCommand(CanExecute = nameof(CanStateChange))]
1014 private async Task GoToParameters()
1015 {
1016 await this.GoToState(NewContractStep.Loading);
1017
1018 // Suppress initial validation during entry
1019 lock (this.debounceLock)
1020 {
1021 if (this.debounceValidationTimer is not null)
1022 {
1023 this.debounceValidationTimer.Stop();
1024 this.debounceValidationTimer.Dispose();
1025 this.debounceValidationTimer = null;
1026 }
1027 }
1028 this.suppressParameterValidation = true;
1029
1030 await this.GoToState(NewContractStep.Parameters);
1031
1032 MainThread.BeginInvokeOnMainThread(async () =>
1033 {
1034 await Task.Delay(50);
1035 this.suppressParameterValidation = false;
1036 });
1037 }
1038
1042 [RelayCommand(CanExecute = nameof(CanStateChange))]
1043 private async Task GoToRoles()
1044 {
1045 await this.GoToState(NewContractStep.Loading);
1046
1047 // If there's exactly one available role (not at MaxCount) for me, select it by default
1048 if (this.Contract is not null && !this.HasSelectedRoles)
1049 {
1050 string? MyId = ServiceRef.TagProfile.LegalIdentity?.Id;
1051 if (!string.IsNullOrEmpty(MyId))
1052 {
1053 List<ObservableRole> Joinable = this.Contract.Roles
1054 .Where(r => r.Parts.Count < r.MaxCount && !r.Parts.Any(p => p.LegalId == MyId))
1055 .ToList();
1056
1057 if (Joinable.Count == 1)
1058 {
1059 try
1060 {
1061 await Joinable[0].AddPart(MyId, false);
1062 }
1063 catch (Exception Ex)
1064 {
1065 ServiceRef.LogService.LogException(Ex);
1066 }
1067 }
1068 }
1069 }
1070
1071 await this.GoToState(NewContractStep.Roles);
1072
1073 }
1074
1079 [RelayCommand(CanExecute = nameof(CanStateChange))]
1080 private async Task GoToPreview()
1081 {
1082 if (this.Contract is null)
1083 return;
1084
1085 await this.GoToState(NewContractStep.Loading);
1086
1087 foreach (Parameter Param in this.Contract.Contract.Parameters)
1088 {
1089 try
1090 {
1091 if (string.IsNullOrEmpty(Param.StringValue))
1092 Param.StringValue = null;
1093 }
1094 catch (Exception Ex)
1095 {
1096 // Ignore
1097 }
1098 }
1099
1100 await this.ValidateParametersAsync(); // Populate All Parameters
1101
1102 foreach (Parameter Param in this.Contract.Contract.Parameters)
1103 {
1104 try
1105 {
1106 if (string.IsNullOrEmpty(Param.StringValue))
1107 Param.StringValue = null;
1108 }
1109 catch (Exception Ex)
1110 {
1111 // Ignore
1112 }
1113 }
1114 VerticalStackLayout? HumanReadableText = await this.Contract.Contract.ToMaui(this.Contract.Contract.DeviceLanguage());
1115
1116 await MainThread.InvokeOnMainThreadAsync(() =>
1117 {
1118 this.HumanReadableText = HumanReadableText;
1119 });
1120
1121 await this.GoToState(NewContractStep.Preview);
1122 }
1123
1124 [RelayCommand(CanExecute = nameof(CanStateChange))]
1125 private async Task ShowPreviewFromIntro()
1126 {
1127 this.IsTransientPreview = true;
1128 await this.GoToPreview();
1129 }
1130
1131 #endregion
1132
1133 #region Event Handlers
1134
1139 private void Parameter_PropertyChanged(object? sender, PropertyChangedEventArgs e)
1140 {
1141 if (e.PropertyName == nameof(ObservableParameter.Value))
1142 {
1143 if (this.suppressParameterValidation)
1144 return;
1145 this.DebounceValidateParameters();
1146 }
1147 }
1148
1149 partial void OnSelectedContractVisibilityItemChanged(ContractVisibilityModel? oldValue, ContractVisibilityModel? newValue)
1150 {
1151 //Fixes losing value when switching view
1152 if (newValue is null)
1153 {
1154 this.SelectedContractVisibilityItem = oldValue;
1155 return;
1156 }
1157 }
1158
1159 #endregion
1160
1161 #region Interface Implementations
1162
1164 public bool IsLinkable => true;
1165
1167 public bool EncodeAppLinks => true;
1168
1170 public string Link
1171 {
1172 get
1173 {
1174 StringBuilder Url = new();
1175 //bool first = true;
1176
1177 Url.Append(Constants.UriSchemes.IotSc);
1178 Url.Append(':');
1179 // url.Append(this.template?.ContractId);
1180
1181 // TODO: Define and initialize 'parametersByName' if necessary
1182 // foreach (KeyValuePair<CaseInsensitiveString, ParameterInfo> p in this.parametersByName)
1183 // {
1184 // if (first)
1185 // {
1186 // first = false;
1187 // url.Append('&');
1188 // }
1189 // else
1190 // {
1191 // url.Append('?');
1192 // }
1193
1194 // url.Append(p.Key);
1195 // url.Append('=');
1196
1197 // if (p.Value.Control is Entry entry)
1198 // url.Append(entry.Text);
1199 // else if (p.Value.Control is CheckBox checkBox)
1200 // url.Append(checkBox.IsChecked ? '1' : '0');
1201 // else if (p.Value.Control is ExtendedDatePicker picker)
1202 // {
1203 // if (p.Value.Parameter is DateParameter)
1204 // url.Append(XML.Encode(picker.Date, true));
1205 // else
1206 // url.Append(XML.Encode(picker.Date, false));
1207 // }
1208 // else
1209 // {
1210 // url.Append(p.Value.Parameter.ObjectValue?.ToString());
1211 // }
1212 // }
1213
1214 return Url.ToString();
1215 }
1216 }
1217
1219 public Task<string> Title => ContractModel.GetName(this.Contract?.Contract);
1220
1222 public bool HasMedia => false;
1223
1225 public byte[]? Media => null;
1226
1228 public string? MediaContentType => null;
1229
1230
1231 private bool disposedValue;
1232
1233 protected virtual void Dispose(bool disposing)
1234 {
1235 if (!this.disposedValue)
1236 {
1237 if (disposing)
1238 {
1239 // Dispose managed state (managed objects)
1240 lock (this.debounceLock)
1241 {
1242 this.debounceValidationTimer?.Stop();
1243 this.debounceValidationTimer?.Dispose();
1244 this.debounceValidationTimer = null;
1245 }
1246 }
1247
1248 this.disposedValue = true;
1249 }
1250 }
1251
1252 public void Dispose()
1253 {
1254 // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
1255 this.Dispose(disposing: true);
1256 GC.SuppressFinalize(this);
1257 }
1258
1259 #endregion
1260 }
1261}
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 ContractWizardNext
Looks up a localized string similar to Next.
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 Create
Looks up a localized string similar to Create.
static string ContractWizardStepFormat
Looks up a localized string similar to Step {0} of {1}.
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 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 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 Ok
Looks up a localized string similar to OK.
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 Error
Looks up a localized string similar to Error.
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
CaseInsensitiveString BareJid
Bare JID of contact.
Definition: ContactInfo.cs:63
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 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
A base class for all view models, inheriting from the BindableObject. NOTE: using this class requir...
static async Task< string > GetName(Contract? Contract)
Gets a displayable name for a contract.
async Task< bool > CheckCanCreateAsync()
Checks if the contract can be created based on the validity of parameters and roles.
override async Task GoBack()
Method called when user wants to navigate to the previous screen.
NewContractViewModel()
Initializes a new instance of the NewContractViewModel class.
override async Task OnInitializeAsync()
Method called when view is initialized for the first time. Use this method to implement registration ...
BindableObject? StateObject
The state object containing all views. Is set by the view.
ObservableCollection< ContractVisibilityModel > ContractVisibilityItems
A list of valid visibility items to choose from for this contract.
bool HasSelectedRoles
True if the current user has selected at least one role to sign as.
override async Task OnDisposeAsync()
Method called when the view is disposed, and will not be used more. Use this method to unregister eve...
async Task Back()
A custom back command, similar to inherited GoBack with Views in mind
override string ToString()
Returns the string representation, i.e. name, of this ContractVisibilityModel.
Describes a wizard step in the new contract flow.
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.
Contains the definition of a contract
Definition: Contract.cs:22
Parameter[] Parameters
Defined parameters for the smart contract.
Definition: Contract.cs:267
Role[] Roles
Roles defined in the smart contract.
Definition: Contract.cs:240
string ContractId
Contract identity
Definition: Contract.cs:65
Contract()
Contains the definition of a contract
Definition: Contract.cs:57
Adds support for legal identities, smart contracts and signatures to an XMPP client.
Task< Contract > CreateContractAsync(XmlElement ForMachines, HumanReadableText[] ForHumans, Role[] Roles, Part[] Parts, Parameter[] Parameters, ContractVisibility Visibility, ContractParts PartsMode, Duration? Duration, Duration? ArchiveRequired, Duration? ArchiveOptional, DateTime? SignAfter, DateTime? SignBefore, bool CanActAsTemplate)
Creates a new contract.
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.
abstract string StringValue
String representation of value.
Definition: Parameter.cs:110
Class defining a part in a contract
Definition: Part.cs:30
string LegalId
Legal identity of part
Definition: Part.cs:38
string Role
Role of the part in the contract
Definition: Part.cs:57
Class defining a role
Definition: Role.cs:7
int MinCount
Smallest amount of signatures of this role required for a legally binding contract.
Definition: Role.cs:26
string Name
Name of the role.
Definition: Role.cs:17
String-valued contractual parameter
Base class of XMPP exceptions
Represents a case-insensitive string.
Collection of variables.
Definition: Variables.cs:25
Task GoToAsync(string Route)
Navigates to the specified route and pushes the page onto the navigation stack.
Task< string?> DisplayPrompt(string Title, string Message, string? Accept, string? Cancel)
Prompts the user for some input.
Task< bool > DisplayAlert(string Title, string Message, string? Accept=null, string? Cancel=null)
Displays an alert/message box to the user.
Interface for linkable views.
Definition: ILinkableView.cs:7
Definition: ImplTypes.g.cs:58
BackMethod
Navigation Back Method
Definition: BackMethod.cs:7
NewContractStep
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
ContractVisibility
Visibility types for contracts.
Definition: Enumerations.cs:56
Definition: App.xaml.cs:4
Represents a duration value, as defined by the xsd:duration data type: http://www....
Definition: Duration.cs:14
static Duration FromYears(int Years)
Creates a Duration object from a given number of years.
Definition: Duration.cs:584