Save the role as an input when accepting
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
dialog.type === 'confirm' ? '? Confirm' :
|
||||||
borderColor={borderColor}
|
'ℹ Info'}
|
||||||
paddingX={2}
|
message={dialog.message}
|
||||||
paddingY={1}
|
onClose={dialog.onCancel ?? (() => {})}
|
||||||
width={60}
|
type={dialog.type as 'error' | 'info' | 'success'}
|
||||||
>
|
/>
|
||||||
<Text color={borderColor} bold>
|
|
||||||
{dialog.type === 'error' ? '✗ Error' :
|
|
||||||
dialog.type === 'confirm' ? '? Confirm' :
|
|
||||||
'ℹ Info'}
|
|
||||||
</Text>
|
|
||||||
<Box marginY={1}>
|
|
||||||
<Text wrap="wrap">{dialog.message}</Text>
|
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,31 +23,60 @@ 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
|
<Box flexDirection="column">
|
||||||
flexDirection="column"
|
|
||||||
borderStyle="double"
|
{/* Opaque backing layer */}
|
||||||
borderColor={borderColor}
|
{height !== null && (
|
||||||
// backgroundColor={backgroundColor || 'white'}
|
<Box
|
||||||
backgroundColor="white"
|
position="absolute"
|
||||||
paddingX={2}
|
flexDirection="column"
|
||||||
paddingY={1}
|
width={width}
|
||||||
width={width}
|
height={height}
|
||||||
>
|
>
|
||||||
<Text color={borderColor} bold>{title}</Text>
|
{Array.from({ length: height }).map((_, i) => (
|
||||||
<Box marginY={1} flexDirection="column">
|
<Text key={i}>{' '.repeat(width)}</Text>
|
||||||
{children}
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actual dialog */}
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
flexDirection="column"
|
||||||
|
borderStyle="double"
|
||||||
|
borderColor={borderColor}
|
||||||
|
paddingX={2}
|
||||||
|
paddingY={1}
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
<Text color={borderColor} bold>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box marginY={1} flexDirection="column">
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user