Invitations screen changes. Scrollable list. Details. And role selection on import
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { AppendInvitationParameters, Engine, FindSuitableResourcesParameters } from '@xo-cash/engine';
|
||||
import { hasInvitationExpired } from '@xo-cash/engine';
|
||||
import type { XOInvitation, XOInvitationCommit, XOInvitationInput, XOInvitationOutput, XOInvitationVariable } from '@xo-cash/types';
|
||||
import type { UnspentOutputData } from '@xo-cash/state';
|
||||
|
||||
@@ -86,6 +87,21 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
*/
|
||||
private storage: Storage;
|
||||
|
||||
/**
|
||||
* True after we have successfully called sign() on this invitation (session-only, not persisted).
|
||||
*/
|
||||
private _weHaveSigned = false;
|
||||
|
||||
/**
|
||||
* True after we have successfully called broadcast() on this invitation (session-only, not persisted).
|
||||
*/
|
||||
private _broadcasted = false;
|
||||
|
||||
/**
|
||||
* The status of the invitation (last emitted word: pending, actionable, signed, ready, complete, expired, unknown).
|
||||
*/
|
||||
public status: string = 'unknown';
|
||||
|
||||
/**
|
||||
* Create an invitation and start the SSE Session required for it.
|
||||
*/
|
||||
@@ -108,28 +124,25 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
|
||||
async start(): Promise<void> {
|
||||
// Connect to the sync server and get the invitation (in parallel)
|
||||
console.time(`connectAndGetInvitation-${this.data.invitationIdentifier}`);
|
||||
const [_, invitation] = await Promise.all([
|
||||
this.syncServer.connect(),
|
||||
this.syncServer.getInvitation(this.data.invitationIdentifier),
|
||||
]);
|
||||
console.timeEnd(`connectAndGetInvitation-${this.data.invitationIdentifier}`);
|
||||
|
||||
// There is a chance we get SSE messages before the invitation is returned, so we want to combine any commits
|
||||
const sseCommits = this.data.commits;
|
||||
|
||||
console.time(`mergeCommits-${this.data.invitationIdentifier}`);
|
||||
// Merge the commits
|
||||
const combinedCommits = this.mergeCommits(sseCommits, invitation?.commits ?? []);
|
||||
console.timeEnd(`mergeCommits-${this.data.invitationIdentifier}`);
|
||||
|
||||
console.time(`setInvitationData-${this.data.invitationIdentifier}`);
|
||||
// Set the invitation data with the combined commits
|
||||
this.data = { ...this.data, ...invitation, commits: combinedCommits };
|
||||
|
||||
// Store the invitation in the storage
|
||||
await this.storage.set(this.data.invitationIdentifier, this.data);
|
||||
console.timeEnd(`setInvitationData-${this.data.invitationIdentifier}`);
|
||||
|
||||
// Compute and emit initial status
|
||||
await this.updateStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,8 +168,8 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
// Set the new commits
|
||||
this.data = { ...this.data, commits: newCommits };
|
||||
|
||||
// Calculate the new status of the invitation
|
||||
this.updateStatus();
|
||||
// Calculate the new status of the invitation (fire-and-forget; handler is sync)
|
||||
this.updateStatus().catch(() => {});
|
||||
|
||||
// Emit the updated event
|
||||
this.emit('invitation-updated', this.data);
|
||||
@@ -185,12 +198,64 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
// Return the merged commits
|
||||
return Array.from(initialMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of the invitation based on the filled in information
|
||||
* Compute the invitation status as a single word: expired | complete | ready | signed | actionable | unknown.
|
||||
*/
|
||||
private updateStatus(): void {
|
||||
// Calculate the status of the invitation
|
||||
this.emit('invitation-status-changed', 'pending - this is temporary');
|
||||
private async computeStatus(): Promise<string> {
|
||||
try {
|
||||
return await this.computeStatusInternal();
|
||||
} catch (err) {
|
||||
return `error (${err instanceof Error ? err.message : String(err)})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal status computation: returns a single word.
|
||||
* - expired: any commit has expired
|
||||
* - complete: we have broadcast this invitation
|
||||
* - ready: no missing requirements and we have signed (ready to broadcast)
|
||||
* - signed: we have signed but there are still missing parts (waiting for others)
|
||||
* - actionable: you can provide data (missing requirements and/or you can sign)
|
||||
* - unknown: template/action not found or error
|
||||
*/
|
||||
private async computeStatusInternal(): Promise<string> {
|
||||
if (hasInvitationExpired(this.data)) {
|
||||
return 'expired';
|
||||
}
|
||||
if (this._broadcasted) {
|
||||
return 'complete';
|
||||
}
|
||||
|
||||
let missingReqs;
|
||||
try {
|
||||
missingReqs = await this.engine.listMissingRequirements(this.data);
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const hasMissing =
|
||||
(missingReqs.variables?.length ?? 0) > 0 ||
|
||||
(missingReqs.inputs?.length ?? 0) > 0 ||
|
||||
(missingReqs.outputs?.length ?? 0) > 0 ||
|
||||
(missingReqs.roles !== undefined && Object.keys(missingReqs.roles).length > 0);
|
||||
|
||||
if (!hasMissing && this._weHaveSigned) {
|
||||
return 'ready';
|
||||
}
|
||||
if (hasMissing && this._weHaveSigned) {
|
||||
return 'signed';
|
||||
}
|
||||
return 'actionable';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the status of the invitation and emit the new single-word status.
|
||||
*/
|
||||
private async updateStatus(): Promise<void> {
|
||||
const status = await this.computeStatus();
|
||||
this.status = status;
|
||||
this.emit('invitation-status-changed', status);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,7 +269,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
this.syncServer.publishInvitation(this.data);
|
||||
|
||||
// Update the status of the invitation
|
||||
this.updateStatus();
|
||||
await this.updateStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,26 +285,26 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
// Store the signed invitation in the storage
|
||||
await this.storage.set(this.data.invitationIdentifier, signedInvitation);
|
||||
|
||||
this.data = signedInvitation;
|
||||
this._weHaveSigned = true;
|
||||
|
||||
// Update the status of the invitation
|
||||
this.updateStatus();
|
||||
await this.updateStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast the invitation
|
||||
*/
|
||||
async broadcast(): Promise<void> {
|
||||
// Broadcast the invitation
|
||||
const broadcastedInvitation = await this.engine.executeAction(this.data, {
|
||||
// Broadcast the transaction (executeAction returns transaction hash when broadcastTransaction: true)
|
||||
await this.engine.executeAction(this.data, {
|
||||
broadcastTransaction: true,
|
||||
});
|
||||
|
||||
// Store the broadcasted invitation in the storage
|
||||
await this.storage.set(this.data.invitationIdentifier, broadcastedInvitation);
|
||||
|
||||
// TODO: Am I supposed to do something here?
|
||||
this._broadcasted = true;
|
||||
|
||||
// Update the status of the invitation
|
||||
this.updateStatus();
|
||||
await this.updateStatus();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -260,7 +325,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
await this.storage.set(this.data.invitationIdentifier, this.data);
|
||||
|
||||
// Update the status of the invitation
|
||||
this.updateStatus();
|
||||
await this.updateStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +360,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
|
||||
const { unspentOutputs } = await this.engine.findSuitableResources(this.data, options);
|
||||
|
||||
// Update the status of the invitation
|
||||
this.updateStatus();
|
||||
await this.updateStatus();
|
||||
|
||||
// Return the suitable resources
|
||||
return unspentOutputs;
|
||||
|
||||
Reference in New Issue
Block a user