/** * Invitation utility functions. * * Pure functions for parsing and formatting invitation data. * These functions have no React dependencies and can be used * in both TUI and CLI contexts. */ import type { Invitation } from "../services/invitation.js"; import type { XOTemplate } from "@xo-cash/types"; /** * Color names for invitation states. * These are semantic color names that can be mapped to actual colors * by the consuming application (TUI or CLI). */ export type StateColorName = "info" | "warning" | "success" | "error" | "muted"; /** * Input data extracted from invitation commits. */ export interface InvitationInput { inputIdentifier?: string; roleIdentifier?: string; entityIdentifier: string; } /** * Output data extracted from invitation commits. */ export interface InvitationOutput { outputIdentifier?: string; roleIdentifier?: string; valueSatoshis?: bigint; entityIdentifier: string; } /** * Variable data extracted from invitation commits. */ export interface InvitationVariable { variableIdentifier: string; value: unknown; roleIdentifier?: string; entityIdentifier: string; } /** * Formatted invitation list item data. */ export interface FormattedInvitationItem { /** The display label for the invitation */ label: string; /** The current status of the invitation */ status: string; /** The semantic color name for the status */ statusColor: StateColorName; /** Whether the invitation data is valid */ isValid: boolean; } /** * Get the current state/status of an invitation. * * @param invitation - The invitation to get state for * @returns The status string */ export function getInvitationState(invitation: Invitation): string { return invitation.status; } /** * Get the semantic color name for an invitation state. * * @param state - The invitation state string * @returns A semantic color name */ export function getStateColorName(state: string): StateColorName { switch (state) { case "created": case "published": return "info"; case "pending": return "warning"; case "ready": case "signed": case "complete": case "broadcast": case "completed": return "success"; case "expired": case "error": return "error"; default: return "muted"; } } /** * Extract all inputs from invitation commits. * * @param invitation - The invitation to extract inputs from * @returns Array of input data */ export function getInvitationInputs(invitation: Invitation): InvitationInput[] { const inputs: InvitationInput[] = []; for (const commit of invitation.data.commits || []) { for (const input of commit.data?.inputs || []) { inputs.push({ inputIdentifier: input.inputIdentifier, roleIdentifier: input.roleIdentifier, entityIdentifier: commit.entityIdentifier, }); } } return inputs; } /** * Extract all outputs from invitation commits. * * @param invitation - The invitation to extract outputs from * @returns Array of output data */ export function getInvitationOutputs( invitation: Invitation, ): InvitationOutput[] { const outputs: InvitationOutput[] = []; for (const commit of invitation.data.commits || []) { for (const output of commit.data?.outputs || []) { outputs.push({ outputIdentifier: output.outputIdentifier, roleIdentifier: output.roleIdentifier, valueSatoshis: output.valueSatoshis, entityIdentifier: commit.entityIdentifier, }); } } return outputs; } /** * Extract all variables from invitation commits. * * @param invitation - The invitation to extract variables from * @returns Array of variable data */ export function getInvitationVariables( invitation: Invitation, ): InvitationVariable[] { const variables: InvitationVariable[] = []; for (const commit of invitation.data.commits || []) { for (const variable of commit.data?.variables || []) { variables.push({ variableIdentifier: variable.variableIdentifier, value: variable.value, roleIdentifier: variable.roleIdentifier, entityIdentifier: commit.entityIdentifier, }); } } return variables; } /** * Get the user's role from commits (the role they have accepted). * * @param invitation - The invitation to check * @param userEntityId - The user's entity identifier * @returns The role identifier if found, null otherwise */ export function getUserRole( invitation: Invitation, userEntityId: string | null, ): string | null { if (!userEntityId) return null; for (const commit of invitation.data.commits || []) { if (commit.entityIdentifier === userEntityId) { // Check inputs for role for (const input of commit.data?.inputs || []) { if (input.roleIdentifier) return input.roleIdentifier; } // Check outputs for role for (const output of commit.data?.outputs || []) { if (output.roleIdentifier) return output.roleIdentifier; } // Check variables for role for (const variable of commit.data?.variables || []) { if (variable.roleIdentifier) return variable.roleIdentifier; } } } return null; } /** * Format an invitation for display in a list. * * @param invitation - The invitation to format * @param template - Optional template for additional info (name) * @returns Formatted item data for display */ export function formatInvitationListItem( invitation: Invitation, template?: XOTemplate | null, ): FormattedInvitationItem { // Validate that we have the minimum required data const invitationId = invitation?.data?.invitationIdentifier; const actionId = invitation?.data?.actionIdentifier; if (!invitationId || !actionId) { return { label: "", status: "error", statusColor: "error", isValid: false, }; } const state = getInvitationState(invitation); const templateName = template?.name ?? "Unknown"; const shortId = formatInvitationId(invitationId, 8); return { label: `[${state}] ${templateName}-${actionId} (${shortId})`, status: state, statusColor: getStateColorName(state), isValid: true, }; } /** * Format an invitation ID for display (truncated). * * @param id - The full invitation ID * @param maxLength - Maximum length for display * @returns Truncated ID string */ export function formatInvitationId(id: string, maxLength: number = 16): string { if (id.length <= maxLength) return id; const half = Math.floor((maxLength - 3) / 2); return `${id.slice(0, half)}...${id.slice(-half)}`; } /** * Get all unique entity identifiers from an invitation's commits. * * @param invitation - The invitation to check * @returns Array of unique entity identifiers */ export function getInvitationParticipants(invitation: Invitation): string[] { const participants = new Set(); for (const commit of invitation.data.commits || []) { if (commit.entityIdentifier) { participants.add(commit.entityIdentifier); } } return Array.from(participants); } /** * Check if a user is a participant in an invitation. * * @param invitation - The invitation to check * @param userEntityId - The user's entity identifier * @returns True if the user has made at least one commit */ export function isUserParticipant( invitation: Invitation, userEntityId: string | null, ): boolean { if (!userEntityId) return false; return getInvitationParticipants(invitation).includes(userEntityId); }