diff --git a/src/services/invitation.ts b/src/services/invitation.ts index 5a5100e..a55fccc 100644 --- a/src/services/invitation.ts +++ b/src/services/invitation.ts @@ -1,4 +1,4 @@ -import type { AppendInvitationParameters, Engine, FindSuitableResourcesParameters } from '@xo-cash/engine'; +import type { AcceptInvitationParameters, 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'; @@ -217,6 +217,7 @@ export class Invitation extends EventEmitter { /** * Internal status computation: returns a single word. + * NOTE: This could be a Enum-like object as well. May be a nice improvement. - DO NOT USE TS ENUM, THEY ARENT NATIVELY SUPPORTED IN NODE.JS * - expired: any commit has expired * - complete: we have broadcast this invitation * - ready: no missing requirements and we have signed (ready to broadcast) @@ -266,9 +267,9 @@ export class Invitation extends EventEmitter { /** * Accept the invitation */ - async accept(): Promise { + async accept(acceptParams?: AcceptInvitationParameters): Promise { // Accept the invitation - this.data = await this.engine.acceptInvitation(this.data); + this.data = await this.engine.acceptInvitation(this.data, acceptParams); // Sync the invitation to the sync server this.syncServer.publishInvitation(this.data); diff --git a/src/tui/App.tsx b/src/tui/App.tsx index 5c6714c..7ea245e 100644 --- a/src/tui/App.tsx +++ b/src/tui/App.tsx @@ -18,6 +18,8 @@ import { ActionWizardScreen } from './screens/action-wizard/ActionWizardScreen.j import { InvitationScreen } from './screens/Invitation.js'; import { TransactionScreen } from './screens/Transaction.js'; +import { MessageDialog } from './components/Dialog.js'; + /** * Props for the App component. */ @@ -107,28 +109,14 @@ function DialogOverlay(): React.ReactElement | null { width="100%" height="100%" > - - - {dialog.type === 'error' ? '✗ Error' : - dialog.type === 'confirm' ? '? Confirm' : - 'ℹ Info'} - - - {dialog.message} - - - {dialog.type === 'confirm' - ? 'Press Y to confirm, N or ESC to cancel' - : 'Press Enter or ESC to close'} - - + {})} + type={dialog.type as 'error' | 'info' | 'success'} + /> ); } diff --git a/src/tui/components/Dialog.tsx b/src/tui/components/Dialog.tsx index 4ff608f..c34c742 100644 --- a/src/tui/components/Dialog.tsx +++ b/src/tui/components/Dialog.tsx @@ -2,8 +2,8 @@ * Dialog components for modals, confirmations, and input dialogs. */ -import React, { useState } from 'react'; -import { Box, Text, useInput } from 'ink'; +import React, { useRef, useState } from 'react'; +import { Box, Text, useInput, measureElement } from 'ink'; import TextInput from 'ink-text-input'; import { colors } from '../theme.js'; @@ -23,31 +23,60 @@ interface DialogWrapperProps { backgroundColor?: string; } -/** - * Base dialog wrapper component. - */ + function DialogWrapper({ title, borderColor = colors.primary, children, width = 60, - backgroundColor = colors.bg, }: DialogWrapperProps): React.ReactElement { + const ref = useRef(null); + const [height, setHeight] = useState(null); + + // measure after render + React.useLayoutEffect(() => { + if (ref.current) { + const { height } = measureElement(ref.current); + setHeight(height); + } + }, [children, title, width]); + return ( - - {title} - - {children} + + + {/* Opaque backing layer */} + {height !== null && ( + + {Array.from({ length: height }).map((_, i) => ( + {' '.repeat(width)} + ))} + + )} + + {/* Actual dialog */} + + + {title} + + + + {children} + + ); } diff --git a/src/tui/components/List.tsx b/src/tui/components/List.tsx index a50ca81..f3cebf1 100644 --- a/src/tui/components/List.tsx +++ b/src/tui/components/List.tsx @@ -147,7 +147,7 @@ function findNextValidIndex( if (currentVisiblePos === -1) { // Current index is hidden, find nearest visible - return visibleIndices[0]; + return visibleIndices[0] ?? 0; } // Calculate next position @@ -160,7 +160,7 @@ function findNextValidIndex( nextVisiblePos = Math.max(0, Math.min(visibleIndices.length - 1, nextVisiblePos)); } - return visibleIndices[nextVisiblePos]; + return visibleIndices[nextVisiblePos] ?? 0; } /** diff --git a/src/tui/screens/Invitation.tsx b/src/tui/screens/Invitation.tsx index c3d6f09..f466af9 100644 --- a/src/tui/screens/Invitation.tsx +++ b/src/tui/screens/Invitation.tsx @@ -205,9 +205,16 @@ export function InvitationScreen(): React.ReactElement { // Create invitation instance (will fetch from sync server) const invitation = await appService.createInvitation(invitationId); + console.log(invitation); + + const missingRequirements = await invitation.getMissingRequirements(); + console.log(missingRequirements); + // Get available roles for this invitation const roles = await invitation.getAvailableRoles(); - + + console.log(roles); + // Get the template for display const template = await appService.engine.getTemplate(invitation.data.templateIdentifier); @@ -244,8 +251,14 @@ export function InvitationScreen(): React.ReactElement { setIsLoading(true); setStatus(`Accepting as ${selectedRole}...`); - await importingInvitation.accept(); - + // TODO: Engine doesnt support "accepting" without supplying some kind of data along with it. + // We also dont have a way to say "this action will require inputs, so i will do that." + // If it did, we could add an "input" with the role identifier. + // For now, we are just going to hard-code the input with the role identifier. + await importingInvitation.addInputs([{ + roleIdentifier: selectedRole, + }]); + showInfo(`Invitation imported and accepted!\n\nRole: ${selectedRole}\nTemplate: ${importTemplate?.name ?? importingInvitation.data.templateIdentifier}\nAction: ${importingInvitation.data.actionIdentifier}`); setStatus('Ready'); @@ -779,7 +792,7 @@ export function InvitationScreen(): React.ReactElement { flexDirection="column" borderStyle="double" borderColor={colors.primary} - backgroundColor="white" + backgroundColor="black" paddingX={2} paddingY={1} width={70} diff --git a/src/tui/screens/action-wizard/useActionWizard.ts b/src/tui/screens/action-wizard/useActionWizard.ts index 8b5a21f..e0b5c9a 100644 --- a/src/tui/screens/action-wizard/useActionWizard.ts +++ b/src/tui/screens/action-wizard/useActionWizard.ts @@ -3,7 +3,7 @@ import { useNavigation } from '../../hooks/useNavigation.js'; import { useAppContext, useStatus } from '../../hooks/useAppContext.js'; import { formatSatoshis } from '../../theme.js'; import { copyToClipboard } from '../../utils/clipboard.js'; -import type { XOTemplate, XOInvitation } from '@xo-cash/types'; +import type { XOTemplate, XOInvitation, XOTemplateTransactionOutput } from '@xo-cash/types'; import type { WizardStep, VariableInput, @@ -360,8 +360,9 @@ export function useActionWizard() { setStatus('Adding required outputs...'); const outputsToAdd = transaction.outputs.map( - (outputId: string) => ({ - outputIdentifier: outputId, + (output: XOTemplateTransactionOutput) => ({ + outputIdentifier: output.output, + roleIdentifier: roleIdentifier, }) );