1using CommunityToolkit.Maui.Layouts;
2using CommunityToolkit.Mvvm.ComponentModel;
3using CommunityToolkit.Mvvm.Input;
13using System.Collections.ObjectModel;
14using System.ComponentModel;
22using Timer =
System.Timers.Timer;
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;
57 private bool areRolesLockedForMe;
63 [NotifyCanExecuteChangedFor(nameof(GoToParametersCommand))]
64 [NotifyCanExecuteChangedFor(nameof(BackCommand))]
65 private bool canStateChange;
70 partial
void OnCurrentStateChanged(
string value)
72 this.OnPropertyChanged(nameof(this.IsOnRolesStep));
73 this.OnPropertyChanged(nameof(this.ShowNoRolesWarning));
74 this.OnPropertyChanged(nameof(this.CanGoBack));
78 public ObservableCollection<StepDescriptor> Steps {
get; } =
new();
84 private bool isCurrentStepValid;
87 private bool isOnPreviewStep;
90 private bool isValidatingParameters;
95 private bool isTransientPreview;
97 partial
void OnIsTransientPreviewChanged(
bool value)
99 this.OnPropertyChanged(nameof(this.CanGoBack));
102 private Contract? lastCreatedContract;
108 [NotifyPropertyChangedFor(nameof(
CanCreate))]
109 [NotifyCanExecuteChangedFor(nameof(CreateCommand))]
110 private bool isValidationDisabled =
false;
114 public string PrimaryActionText =>
115 (this.CurrentStep is not
null && this.Steps.Count > 0 && this.CurrentStep.Index >= this.Steps.Count - 1)
119 public bool CanGoBack =>
120 (this.CurrentStep?.Index ?? 0) > 0 ||
121 (this.IsTransientPreview && this.CurrentState == nameof(
NewContractStep.Preview));
125 if (oldValue is not
null)
126 oldValue.IsCurrent =
false;
127 if (newValue is not
null)
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));
143 private string contractName =
string.Empty;
147 private VerticalStackLayout? humanReadableText;
149 public ObservableCollection<ObservableRole> AvailableRoles {
get;
set; } = [];
159 if (this.
Contract is
null ||
string.IsNullOrEmpty(MyId))
163 if (
Role.Parts.Any(p => p.LegalId == MyId))
170 public bool IsOnRolesStep => this.CurrentState == nameof(
NewContractStep.Roles);
172 public bool ShowNoRolesWarning => this.IsOnRolesStep && !this.
HasSelectedRoles;
174 public ObservableCollection<ObservableParameter> EditableParameters {
get;
set; } = [];
201 [NotifyPropertyChangedFor(nameof(
CanCreate))]
205 public bool HasRoles => this.Contract is not
null && this.Contract.Roles.Count > 0;
207 public bool HasParameters => this.Contract is not
null && this.EditableParameters.Count > 0;
213 [NotifyPropertyChangedFor(nameof(
CanCreate))]
214 [NotifyCanExecuteChangedFor(nameof(CreateCommand))]
215 private bool isParametersOk;
221 [NotifyPropertyChangedFor(nameof(
CanCreate))]
222 [NotifyCanExecuteChangedFor(nameof(CreateCommand))]
223 private bool isRolesOk;
229 [NotifyPropertyChangedFor(nameof(
CanCreate))]
230 [NotifyCanExecuteChangedFor(nameof(CreateCommand))]
231 private bool isContractOk;
238 (this.IsParametersOk && this.IsRolesOk && this.IsContractOk
239 && this.SelectedContractVisibilityItem is not
null)
240 || this.IsValidationDisabled;
242 partial
void OnIsContractOkChanged(
bool value)
245 _ = this.UpdateCurrentStepValidityAsync();
255 await base.OnInitializeAsync();
257 if (this.args is
null || this.args?.Template is
null)
270 this.Contract.ParameterChanged += this.Parameter_PropertyChanged;
271 this.Contract.PartChanged += (
_, __) =>
274 this.OnPropertyChanged(nameof(this.ShowNoRolesWarning));
275 _ = this.UpdateCurrentStepValidityAsync();
279 TaskCompletionSource<bool> HasInitializedParameters =
new();
281 MainThread.BeginInvokeOnMainThread(async () =>
283 if (this.args.ParameterValues is not
null)
288 if (this.args.ParameterValues.TryGetValue(
Parameter.Parameter.
Name, out
object? Value))
289 Parameter.Value = Value;
292 lock (this.debounceLock)
294 if (this.debounceValidationTimer is not
null)
296 this.debounceValidationTimer.Stop();
297 this.debounceValidationTimer.Dispose();
298 this.debounceValidationTimer =
null;
304 if (this.args.ParameterValues.TryGetValue(RoleItem.
Role.
Name, out
object? RoleValue))
306 if (RoleValue is
string LegalID)
307 await RoleItem.
AddPart(LegalID, PresetFromArgs:
true);
312 if (!
string.IsNullOrEmpty(MyIdInit))
314 this.AreRolesLockedForMe = this.
Contract.
Roles.Any(r => r.Parts.Any(p => p.LegalId == MyIdInit));
332 this.OnPropertyChanged(nameof(this.HasRoles));
333 this.OnPropertyChanged(nameof(this.HasParameters));
335 HasInitializedParameters.SetResult(
true);
337 await HasInitializedParameters.Task;
339 await this.ValidateParametersAsync();
340 this.InitializeSteps();
345 this.CurrentStep = this.Steps.FirstOrDefault(Step => Step.Key == nameof(
NewContractStep.Intro));
347 catch (Exception Ex4)
364 this.Contract.ParameterChanged -= this.Parameter_PropertyChanged;
366 await base.OnDisposeAsync();
380 string NewState = newStep.ToString();
382 if (NewState == this.CurrentState)
385 while (!this.CanStateChange)
386 await Task.Delay(100);
388 await MainThread.InvokeOnMainThreadAsync(async () =>
390 await StateContainer.ChangeStateWithAnimation(this.
StateObject, NewState);
394 private void InitializeSteps()
396 if (this.Steps.Count > 0)
401 foreach (
string Key
in Order)
403 Func<Task<bool>>? ValidateFunc =
null;
407 ValidateFunc = () => Task.FromResult(
true);
413 if (this.IsValidationDisabled)
414 return Task.FromResult(
true);
418 if (Param.Value is
null || !Param.
IsValid)
424 this.IsParametersOk = Ok;
425 return Task.FromResult(Ok);
430 ValidateFunc = () => Task.FromResult(
this.IsValidationDisabled ||
this.CheckRolesValid());
434 ValidateFunc = () => Task.FromResult(
this.IsValidationDisabled ||
this.IsContractOk);
442 ValidateAsync = ValidateFunc
447 private bool CheckRolesValid()
456 bool MinCountsOk =
true;
466 bool Ok = HasMySelection && MinCountsOk;
471 private async Task UpdateCurrentStepValidityAsync()
473 if (this.CurrentStep?.ValidateAsync is
null)
475 this.IsCurrentStepValid =
true;
478 if (this.IsValidationDisabled)
480 this.IsCurrentStepValid =
true;
481 this.CurrentStep.IsComplete =
true;
487 Ok = await this.CurrentStep.ValidateAsync();
493 this.IsCurrentStepValid = Ok;
494 this.CurrentStep.IsComplete = Ok;
497 partial
void OnIsValidationDisabledChanged(
bool value)
499 _ = this.UpdateCurrentStepValidityAsync();
507 if (Target !=
NewContractStep.Loading &&
this.CurrentState != Step.Key)
512 await this.GoToPreview();
517 lock (this.debounceLock)
519 if (this.debounceValidationTimer is not
null)
521 this.debounceValidationTimer.Stop();
522 this.debounceValidationTimer.Dispose();
523 this.debounceValidationTimer =
null;
526 this.suppressParameterValidation =
true;
528 await this.GoToState(Target);
531 MainThread.BeginInvokeOnMainThread(async () =>
533 await Task.Delay(50);
534 this.suppressParameterValidation =
false;
543 if (!
string.IsNullOrEmpty(MyId))
545 List<ObservableRole> Joinable = this.Contract.Roles
546 .Where(r => r.Parts.Count < r.MaxCount && !r.Parts.Any(p => p.LegalId == MyId))
548 if (Joinable.Count == 1)
552 await Joinable[0].AddPart(MyId,
false);
562 await this.GoToState(Target);
566 await this.GoToState(Target);
577 private void GoNextStep()
579 if (this.CurrentStep is
null)
581 int NextIndex = this.CurrentStep.Index + 1;
582 if (NextIndex < this.Steps.Count)
584 this.CurrentStep = this.Steps[NextIndex];
588 _ = this.CreateAsync();
593 private void GoPreviousStep()
595 if (this.CurrentStep is
null)
598 if (this.IsTransientPreview && this.CurrentState == nameof(
NewContractStep.Preview))
600 this.IsTransientPreview =
false;
606 int PrevIndex = this.CurrentStep.Index - 1;
608 this.CurrentStep = this.Steps[PrevIndex];
612 private void GoToStep(
string? stepKey)
614 if (
string.IsNullOrEmpty(stepKey))
616 StepDescriptor? Target = this.Steps.FirstOrDefault(S => S.Key == stepKey);
621 int FirstIncomplete = this.Steps.TakeWhile(S => S.IsComplete || S.IsCurrent).Count();
622 if (Target.Index <= FirstIncomplete)
623 this.CurrentStep = Target;
631 int FirstIncomplete = this.Steps.TakeWhile(S => S.IsComplete || S.IsCurrent).Count();
632 if (step.Index <= FirstIncomplete)
633 this.CurrentStep = step;
644 await this.FlushValidationAsync();
646 bool ParametersOk =
true;
649 if (ParamItem.Value is
null || !ParamItem.
IsValid)
651 ParametersOk =
false;
669 MainThread.BeginInvokeOnMainThread(() =>
671 this.IsParametersOk = ParametersOk;
672 this.IsRolesOk = RolesOk;
680 private async Task ValidateParametersAsync()
693 if (FirstSignature.HasValue)
695 Variables[
"Now"] = FirstSignature.Value.ToLocalTime();
696 Variables[
"NowUtc"] = FirstSignature.Value.ToUniversalTime();
703 List<(
ObservableParameter Param,
bool IsValid,
string ValidationText)> ValidationResults = [];
717 bool IsValid =
false;
718 string ValidationText =
string.Empty;
724 ValidationText =
string.Empty;
726 else if(ParamToValidate.Value is
null)
728 ValidationText =
string.Empty;
732 IsValid = IsValid || ParamToValidate.Parameter.ErrorText ==
ContractStatus.ClientIdentityInvalid.ToString();
733 ValidationText = ParamToValidate.Parameter.ErrorText;
737 if (!this.suppressParameterValidation)
738 ServiceRef.
LogService.LogDebug($
"Parameter '{ParamToValidate.Parameter.Name}' validation result: {IsValid}, Error: {ParamToValidate.Parameter.ErrorReason} - {ValidationText}");
740 catch (Exception Ex2)
745 return (Param: ParamToValidate, IsValid, ValidationText);
748 (
ObservableParameter Param,
bool IsValid,
string ValidationText)[] Results = await Task.WhenAll(ValidationTasks);
751 await MainThread.InvokeOnMainThreadAsync(() =>
753 foreach ((
ObservableParameter Param,
bool IsValid,
string ValidationText) Result in Results)
755 Result.Param.IsValid = Result.IsValid;
756 Result.Param.ValidationText = Result.ValidationText;
759 this.IsParametersOk = this.EditableParameters.All(p => p.Value is not
null && p.IsValid);
768 private void DebounceValidateParameters()
770 lock (this.debounceLock)
772 if (this.debounceValidationTimer is not
null)
774 this.debounceValidationTimer.Stop();
775 this.debounceValidationTimer.Dispose();
776 this.debounceValidationTimer =
null;
779 this.debounceValidationTimer =
new Timer(700);
780 this.debounceValidationTimer.Elapsed += async (s, e) =>
782 lock (this.debounceLock)
784 this.debounceValidationTimer?.Stop();
785 this.debounceValidationTimer?.Dispose();
786 this.debounceValidationTimer =
null;
790 Task ValidationTask = MainThread.InvokeOnMainThreadAsync(async () =>
792 await this.ValidateParametersAsync();
796 this.latestValidationTask = ValidationTask;
797 await ValidationTask;
799 await MainThread.InvokeOnMainThreadAsync(async () =>
801 await this.UpdateCurrentStepValidityAsync();
804 this.debounceValidationTimer.AutoReset =
false;
805 this.debounceValidationTimer.Start();
808 private async Task FlushValidationAsync()
810 Task? ValidationTask =
null;
811 this.IsValidatingParameters =
true;
813 lock (this.debounceLock)
815 if (this.debounceValidationTimer is not
null)
817 this.debounceValidationTimer.Stop();
818 this.debounceValidationTimer.Dispose();
819 this.debounceValidationTimer =
null;
820 ValidationTask = MainThread.InvokeOnMainThreadAsync(this.ValidateParametersAsync);
821 this.latestValidationTask = ValidationTask;
823 else if (this.latestValidationTask is not
null)
825 ValidationTask = this.latestValidationTask;
829 ValidationTask = MainThread.InvokeOnMainThreadAsync(this.ValidateParametersAsync);
830 this.latestValidationTask = ValidationTask;
834 if (ValidationTask is not
null)
835 await ValidationTask;
836 this.IsValidatingParameters =
false;
845 [RelayCommand(CanExecute = nameof(CanCreate), AllowConcurrentExecutions =
false)]
846 private async Task CreateAsync()
862 List<Part> Parts = [];
867 Parts.Add(
Part.Part);
876 this.Contract.Contract.Parameters,
877 this.SelectedContractVisibilityItem?.Visibility ??
this.Contract.Visibility,
883 this.lastCreatedContract = CreatedContract;
884 await this.OpenCreatedContract();
887 if (!
string.IsNullOrEmpty(MyId))
891 if (
Role.Parts.Any(p => p.LegalId == MyId))
907 if (Info is
null ||
string.IsNullOrEmpty(Info.
BareJid))
916 if (!
string.IsNullOrEmpty(Proposal))
937 if (CreatedContract is
null)
948 this.lastCreatedContract = CreatedContract;
950 this.postCreateCompletion?.TrySetResult(CreatedContract);
951 this.postCreateCompletion =
null;
960 [RelayCommand(CanExecute = nameof(CanStateChange))]
972 case NewContractStep.Preview when this.IsTransientPreview:
973 this.IsTransientPreview =
false;
979 this.GoPreviousStep();
988 catch (Exception Ex3)
1000 private async Task OpenCreatedContract()
1002 if (this.lastCreatedContract is
null)
1004 TaskCompletionSource<Contract?> Tcs =
new TaskCompletionSource<Contract?>();
1005 ViewContractNavigationArgs Args =
new(this.lastCreatedContract,
false,
null,
string.Empty,
null, Tcs);
1007 this.postCreateCompletion = Tcs;
1013 [RelayCommand(CanExecute = nameof(CanStateChange))]
1014 private async Task GoToParameters()
1019 lock (this.debounceLock)
1021 if (this.debounceValidationTimer is not
null)
1023 this.debounceValidationTimer.Stop();
1024 this.debounceValidationTimer.Dispose();
1025 this.debounceValidationTimer =
null;
1028 this.suppressParameterValidation =
true;
1032 MainThread.BeginInvokeOnMainThread(async () =>
1034 await Task.Delay(50);
1035 this.suppressParameterValidation =
false;
1042 [RelayCommand(CanExecute = nameof(CanStateChange))]
1043 private async Task GoToRoles()
1048 if (this.
Contract is not
null && !this.HasSelectedRoles)
1051 if (!
string.IsNullOrEmpty(MyId))
1053 List<ObservableRole> Joinable = this.Contract.Roles
1054 .Where(r => r.Parts.Count < r.MaxCount && !r.Parts.Any(p => p.LegalId == MyId))
1057 if (Joinable.Count == 1)
1061 await Joinable[0].AddPart(MyId,
false);
1063 catch (Exception Ex)
1079 [RelayCommand(CanExecute = nameof(CanStateChange))]
1080 private async Task GoToPreview()
1092 Param.StringValue =
null;
1094 catch (Exception Ex)
1100 await this.ValidateParametersAsync();
1107 Param.StringValue =
null;
1109 catch (Exception Ex)
1116 await MainThread.InvokeOnMainThreadAsync(() =>
1118 this.HumanReadableText = HumanReadableText;
1124 [RelayCommand(CanExecute = nameof(CanStateChange))]
1125 private async Task ShowPreviewFromIntro()
1127 this.IsTransientPreview =
true;
1128 await this.GoToPreview();
1133 #region Event Handlers
1139 private void Parameter_PropertyChanged(
object? sender, PropertyChangedEventArgs e)
1143 if (this.suppressParameterValidation)
1145 this.DebounceValidateParameters();
1152 if (newValue is
null)
1154 this.SelectedContractVisibilityItem = oldValue;
1161 #region Interface Implementations
1164 public bool IsLinkable =>
true;
1167 public bool EncodeAppLinks =>
true;
1174 StringBuilder Url =
new();
1222 public bool HasMedia =>
false;
1225 public byte[]? Media =>
null;
1228 public string? MediaContentType =>
null;
1231 private bool disposedValue;
1233 protected virtual void Dispose(
bool disposing)
1235 if (!this.disposedValue)
1240 lock (this.debounceLock)
1242 this.debounceValidationTimer?.Stop();
1243 this.debounceValidationTimer?.Dispose();
1244 this.debounceValidationTimer =
null;
1248 this.disposedValue =
true;
1252 public void Dispose()
1255 this.Dispose(disposing:
true);
1256 GC.SuppressFinalize(
this);
const string IotSc
The IoT Smart Contract URI Scheme (iotsc)
A set of never changing property constants and helpful values.
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.
Base class that references services in the app.
static IAuthenticationService AuthenticationService
Authentication service.
static ILogService LogService
Log service.
static IUiService UiService
Service serializing and managing UI-related tasks.
static INavigationService NavigationService
The navigation service for navigating between pages.
static ITagProfile TagProfile
TAG Profile service.
static IReportingStringLocalizer Localizer
Localization service
static IXmppService XmppService
The XMPP service for XMPP communication.
A base class for all view models, inheriting from the BindableObject. NOTE: using this class requir...
The data model for a contract.
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.
bool CanCreate
If Contract can be created
Task< string > Title
Title of the current view
bool HasHumanReadableText
If HumanReadableText is not empty
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
The data model for contract visibility.
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....
Parameter Parameter
The wrapped parameter object
bool IsValid
If the parameter is avlid
An observable object that wraps a Waher.Networking.XMPP.Contracts.Role object. This allows for easier...
Role Role
The wrapped Role object
async Task AddPart(string LegalId, bool Notify=true, bool AutoPetition=true, bool PresetFromArgs=false)
Adds a part with a given LegalId to the role.
A page that displays a specific contract.
Boolean contractual parameter
Contains the definition of a contract
Parameter[] Parameters
Defined parameters for the smart contract.
Role[] Roles
Roles defined in the smart contract.
string ContractId
Contract identity
Contract()
Contains the definition of a contract
Contract-reference parameter
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.
Date contractual parameter
Date and Time contractual parameter
Duration contractual parameter
Geo-spatial contractual parameter
Numerical contractual parameter
Abstract base class for contractual parameters
abstract void Populate(Variables Variables)
Populates a variable collection with the value of the parameter.
abstract string StringValue
String representation of value.
string Name
Parameter name
Class defining a part in a contract
string LegalId
Legal identity of part
string Role
Role of the part in the contract
int MinCount
Smallest amount of signatures of this role required for a legally binding contract.
string Name
Name of the role.
String-valued contractual parameter
Time contractual parameter
Base class of XMPP exceptions
Represents a case-insensitive string.
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.
BackMethod
Navigation Back Method
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.
ContractStatus
Validation Status of smart contract
ContractVisibility
Visibility types for contracts.
Represents a duration value, as defined by the xsd:duration data type: http://www....
static Duration FromYears(int Years)
Creates a Duration object from a given number of years.