Save the role as an input when accepting

This commit is contained in:
2026-02-28 08:17:55 +00:00
parent 38a0ac436b
commit 66e9918e04
6 changed files with 85 additions and 53 deletions

View File

@@ -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 { hasInvitationExpired } from '@xo-cash/engine';
import type { XOInvitation, XOInvitationCommit, XOInvitationInput, XOInvitationOutput, XOInvitationVariable } from '@xo-cash/types'; import type { XOInvitation, XOInvitationCommit, XOInvitationInput, XOInvitationOutput, XOInvitationVariable } from '@xo-cash/types';
import type { UnspentOutputData } from '@xo-cash/state'; import type { UnspentOutputData } from '@xo-cash/state';
@@ -217,6 +217,7 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
/** /**
* Internal status computation: returns a single word. * 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 * - expired: any commit has expired
* - complete: we have broadcast this invitation * - complete: we have broadcast this invitation
* - ready: no missing requirements and we have signed (ready to broadcast) * - ready: no missing requirements and we have signed (ready to broadcast)
@@ -266,9 +267,9 @@ export class Invitation extends EventEmitter<InvitationEventMap> {
/** /**
* Accept the invitation * Accept the invitation
*/ */
async accept(): Promise<void> { async accept(acceptParams?: AcceptInvitationParameters): Promise<void> {
// Accept the invitation // 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 // Sync the invitation to the sync server
this.syncServer.publishInvitation(this.data); this.syncServer.publishInvitation(this.data);

View File

@@ -18,6 +18,8 @@ import { ActionWizardScreen } from './screens/action-wizard/ActionWizardScreen.j
import { InvitationScreen } from './screens/Invitation.js'; import { InvitationScreen } from './screens/Invitation.js';
import { TransactionScreen } from './screens/Transaction.js'; import { TransactionScreen } from './screens/Transaction.js';
import { MessageDialog } from './components/Dialog.js';
/** /**
* Props for the App component. * Props for the App component.
*/ */
@@ -107,28 +109,14 @@ function DialogOverlay(): React.ReactElement | null {
width="100%" width="100%"
height="100%" height="100%"
> >
<Box <MessageDialog
flexDirection="column" title={dialog.type === 'error' ? '✗ Error' :
borderStyle="double"
borderColor={borderColor}
paddingX={2}
paddingY={1}
width={60}
>
<Text color={borderColor} bold>
{dialog.type === 'error' ? '✗ Error' :
dialog.type === 'confirm' ? '? Confirm' : dialog.type === 'confirm' ? '? Confirm' :
' Info'} ' Info'}
</Text> message={dialog.message}
<Box marginY={1}> onClose={dialog.onCancel ?? (() => {})}
<Text wrap="wrap">{dialog.message}</Text> type={dialog.type as 'error' | 'info' | 'success'}
</Box> />
<Text color={colors.textMuted}>
{dialog.type === 'confirm'
? 'Press Y to confirm, N or ESC to cancel'
: 'Press Enter or ESC to close'}
</Text>
</Box>
</Box> </Box>
); );
} }

View File

@@ -2,8 +2,8 @@
* Dialog components for modals, confirmations, and input dialogs. * Dialog components for modals, confirmations, and input dialogs.
*/ */
import React, { useState } from 'react'; import React, { useRef, useState } from 'react';
import { Box, Text, useInput } from 'ink'; import { Box, Text, useInput, measureElement } from 'ink';
import TextInput from 'ink-text-input'; import TextInput from 'ink-text-input';
import { colors } from '../theme.js'; import { colors } from '../theme.js';
@@ -23,32 +23,61 @@ interface DialogWrapperProps {
backgroundColor?: string; backgroundColor?: string;
} }
/**
* Base dialog wrapper component.
*/
function DialogWrapper({ function DialogWrapper({
title, title,
borderColor = colors.primary, borderColor = colors.primary,
children, children,
width = 60, width = 60,
backgroundColor = colors.bg,
}: DialogWrapperProps): React.ReactElement { }: DialogWrapperProps): React.ReactElement {
const ref = useRef<any>(null);
const [height, setHeight] = useState<number | null>(null);
// measure after render
React.useLayoutEffect(() => {
if (ref.current) {
const { height } = measureElement(ref.current);
setHeight(height);
}
}, [children, title, width]);
return ( return (
<Box flexDirection="column">
{/* Opaque backing layer */}
{height !== null && (
<Box <Box
position="absolute"
flexDirection="column"
width={width}
height={height}
>
{Array.from({ length: height }).map((_, i) => (
<Text key={i}>{' '.repeat(width)}</Text>
))}
</Box>
)}
{/* Actual dialog */}
<Box
ref={ref}
flexDirection="column" flexDirection="column"
borderStyle="double" borderStyle="double"
borderColor={borderColor} borderColor={borderColor}
// backgroundColor={backgroundColor || 'white'}
backgroundColor="white"
paddingX={2} paddingX={2}
paddingY={1} paddingY={1}
width={width} width={width}
> >
<Text color={borderColor} bold>{title}</Text> <Text color={borderColor} bold>
{title}
</Text>
<Box marginY={1} flexDirection="column"> <Box marginY={1} flexDirection="column">
{children} {children}
</Box> </Box>
</Box> </Box>
</Box>
); );
} }

View File

@@ -147,7 +147,7 @@ function findNextValidIndex<T>(
if (currentVisiblePos === -1) { if (currentVisiblePos === -1) {
// Current index is hidden, find nearest visible // Current index is hidden, find nearest visible
return visibleIndices[0]; return visibleIndices[0] ?? 0;
} }
// Calculate next position // Calculate next position
@@ -160,7 +160,7 @@ function findNextValidIndex<T>(
nextVisiblePos = Math.max(0, Math.min(visibleIndices.length - 1, nextVisiblePos)); nextVisiblePos = Math.max(0, Math.min(visibleIndices.length - 1, nextVisiblePos));
} }
return visibleIndices[nextVisiblePos]; return visibleIndices[nextVisiblePos] ?? 0;
} }
/** /**

View File

@@ -205,9 +205,16 @@ export function InvitationScreen(): React.ReactElement {
// Create invitation instance (will fetch from sync server) // Create invitation instance (will fetch from sync server)
const invitation = await appService.createInvitation(invitationId); const invitation = await appService.createInvitation(invitationId);
console.log(invitation);
const missingRequirements = await invitation.getMissingRequirements();
console.log(missingRequirements);
// Get available roles for this invitation // Get available roles for this invitation
const roles = await invitation.getAvailableRoles(); const roles = await invitation.getAvailableRoles();
console.log(roles);
// Get the template for display // Get the template for display
const template = await appService.engine.getTemplate(invitation.data.templateIdentifier); const template = await appService.engine.getTemplate(invitation.data.templateIdentifier);
@@ -244,7 +251,13 @@ export function InvitationScreen(): React.ReactElement {
setIsLoading(true); setIsLoading(true);
setStatus(`Accepting as ${selectedRole}...`); 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}`); showInfo(`Invitation imported and accepted!\n\nRole: ${selectedRole}\nTemplate: ${importTemplate?.name ?? importingInvitation.data.templateIdentifier}\nAction: ${importingInvitation.data.actionIdentifier}`);
setStatus('Ready'); setStatus('Ready');
@@ -779,7 +792,7 @@ export function InvitationScreen(): React.ReactElement {
flexDirection="column" flexDirection="column"
borderStyle="double" borderStyle="double"
borderColor={colors.primary} borderColor={colors.primary}
backgroundColor="white" backgroundColor="black"
paddingX={2} paddingX={2}
paddingY={1} paddingY={1}
width={70} width={70}

View File

@@ -3,7 +3,7 @@ import { useNavigation } from '../../hooks/useNavigation.js';
import { useAppContext, useStatus } from '../../hooks/useAppContext.js'; import { useAppContext, useStatus } from '../../hooks/useAppContext.js';
import { formatSatoshis } from '../../theme.js'; import { formatSatoshis } from '../../theme.js';
import { copyToClipboard } from '../../utils/clipboard.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 { import type {
WizardStep, WizardStep,
VariableInput, VariableInput,
@@ -360,8 +360,9 @@ export function useActionWizard() {
setStatus('Adding required outputs...'); setStatus('Adding required outputs...');
const outputsToAdd = transaction.outputs.map( const outputsToAdd = transaction.outputs.map(
(outputId: string) => ({ (output: XOTemplateTransactionOutput) => ({
outputIdentifier: outputId, outputIdentifier: output.output,
roleIdentifier: roleIdentifier,
}) })
); );