Clean up and fixes

This commit is contained in:
2026-02-08 02:32:50 +00:00
parent eb1bf9020e
commit da096af0fa
36 changed files with 2119 additions and 1751 deletions

View File

@@ -13,6 +13,7 @@ import { Box, Text, useInput } from 'ink';
import { ConfirmDialog } from '../components/Dialog.js';
import { useNavigation } from '../hooks/useNavigation.js';
import { useAppContext, useStatus } from '../hooks/useAppContext.js';
import { useInvitation } from '../hooks/useInvitations.js';
import { colors, logoSmall, formatSatoshis, formatHex } from '../theme.js';
import { copyToClipboard } from '../utils/clipboard.js';
import type { XOInvitation } from '@xo-cash/types';
@@ -32,59 +33,51 @@ const actionItems = [
*/
export function TransactionScreen(): React.ReactElement {
const { navigate, goBack, data: navData } = useNavigation();
const { invitationController, showError, showInfo, confirm } = useAppContext();
const { showError, showInfo } = useAppContext();
const { setStatus } = useStatus();
// Extract invitation ID from navigation data
const invitationId = navData.invitationId as string | undefined;
// Use hook to get invitation reactively
const invitationInstance = useInvitation(invitationId ?? null);
// State
const [invitation, setInvitation] = useState<XOInvitation | null>(null);
const [focusedPanel, setFocusedPanel] = useState<'inputs' | 'outputs' | 'actions'>('actions');
const [selectedActionIndex, setSelectedActionIndex] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [showBroadcastConfirm, setShowBroadcastConfirm] = useState(false);
/**
* Load invitation data.
*/
const loadInvitation = useCallback(() => {
// Check if invitation exists
useEffect(() => {
if (!invitationId) {
showError('No invitation ID provided');
goBack();
return;
}
const tracked = invitationController.getInvitation(invitationId);
if (!tracked) {
if (invitationId && !invitationInstance) {
showError('Invitation not found');
goBack();
return;
}
}, [invitationId, invitationInstance, showError, goBack]);
setInvitation(tracked.invitation);
}, [invitationId, invitationController, showError, goBack]);
// Load on mount
useEffect(() => {
loadInvitation();
}, [loadInvitation]);
const invitation = invitationInstance?.data ?? null;
/**
* Broadcast transaction.
*/
const broadcastTransaction = useCallback(async () => {
if (!invitationId) return;
if (!invitationInstance) return;
setShowBroadcastConfirm(false);
setIsLoading(true);
setStatus('Broadcasting transaction...');
try {
const txHash = await invitationController.broadcastTransaction(invitationId);
await invitationInstance.broadcast();
showInfo(
`Transaction Broadcast Successful!\n\n` +
`Transaction Hash:\n${txHash}\n\n` +
`The transaction has been submitted to the network.`
);
navigate('wallet');
@@ -94,20 +87,19 @@ export function TransactionScreen(): React.ReactElement {
setIsLoading(false);
setStatus('Ready');
}
}, [invitationId, invitationController, showInfo, showError, navigate, setStatus]);
}, [invitationInstance, showInfo, showError, navigate, setStatus]);
/**
* Sign transaction.
*/
const signTransaction = useCallback(async () => {
if (!invitationId) return;
if (!invitationInstance) return;
setIsLoading(true);
setStatus('Signing transaction...');
try {
await invitationController.signInvitation(invitationId);
loadInvitation();
await invitationInstance.sign();
showInfo('Transaction signed successfully!');
} catch (error) {
showError(`Failed to sign: ${error instanceof Error ? error.message : String(error)}`);
@@ -115,7 +107,7 @@ export function TransactionScreen(): React.ReactElement {
setIsLoading(false);
setStatus('Ready');
}
}, [invitationId, invitationController, loadInvitation, showInfo, showError, setStatus]);
}, [invitationInstance, showInfo, showError, setStatus]);
/**
* Copy transaction hex.
@@ -273,24 +265,24 @@ export function TransactionScreen(): React.ReactElement {
const hasUnresolvedInputs = inputs.length > 0; // Input values are always unknown from commit data
return (
<Box flexDirection="column" flexGrow={1}>
<Box flexDirection='column' flexGrow={1}>
{/* Header */}
<Box borderStyle="single" borderColor={colors.secondary} paddingX={1}>
<Box borderStyle='single' borderColor={colors.secondary} paddingX={1}>
<Text color={colors.primary} bold>{logoSmall} - Transaction Review</Text>
</Box>
{/* Summary box */}
<Box
borderStyle="single"
borderStyle='single'
borderColor={colors.primary}
marginTop={1}
marginX={1}
paddingX={1}
flexDirection="column"
flexDirection='column'
>
<Text color={colors.primary} bold> Transaction Summary </Text>
{invitation ? (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection='column' marginTop={1}>
<Text color={colors.text}>Inputs: {inputs.length} | Outputs: {resolvedOutputs.length} | Commits: {commits.length}</Text>
{hasUnresolvedInputs && (
<Text color={colors.textMuted}>Total In: (requires UTXO lookup)</Text>
@@ -308,22 +300,22 @@ export function TransactionScreen(): React.ReactElement {
</Box>
{/* Inputs and Outputs */}
<Box flexDirection="row" marginTop={1} marginX={1} flexGrow={1}>
<Box flexDirection='row' marginTop={1} marginX={1} flexGrow={1}>
{/* Inputs */}
<Box
borderStyle="single"
borderStyle='single'
borderColor={focusedPanel === 'inputs' ? colors.focus : colors.border}
width="50%"
flexDirection="column"
width='50%'
flexDirection='column'
paddingX={1}
>
<Text color={colors.primary} bold> Inputs </Text>
<Box flexDirection="column" marginTop={1}>
<Box flexDirection='column' marginTop={1}>
{inputs.length === 0 ? (
<Text color={colors.textMuted}>No inputs</Text>
) : (
inputs.map((input, index) => (
<Box key={`${input.txid}-${input.index}`} flexDirection="column" marginBottom={1}>
<Box key={`${input.txid}-${input.index}`} flexDirection='column' marginBottom={1}>
<Text color={colors.text}>
{index + 1}. {formatHex(input.txid, 12)}:{input.index}
</Text>
@@ -338,20 +330,20 @@ export function TransactionScreen(): React.ReactElement {
{/* Outputs */}
<Box
borderStyle="single"
borderStyle='single'
borderColor={focusedPanel === 'outputs' ? colors.focus : colors.border}
width="50%"
flexDirection="column"
width='50%'
flexDirection='column'
paddingX={1}
marginLeft={1}
>
<Text color={colors.primary} bold> Outputs </Text>
<Box flexDirection="column" marginTop={1}>
<Box flexDirection='column' marginTop={1}>
{resolvedOutputs.length === 0 ? (
<Text color={colors.textMuted}>No outputs</Text>
) : (
resolvedOutputs.map((output, index) => (
<Box key={index} flexDirection="column" marginBottom={1}>
<Box key={index} flexDirection='column' marginBottom={1}>
<Text color={colors.text}>
{index + 1}. {output.value !== undefined ? formatSatoshis(output.value) : '(pending)'}
{output.outputIdentifier && (
@@ -371,15 +363,15 @@ export function TransactionScreen(): React.ReactElement {
{/* Actions */}
<Box
borderStyle="single"
borderStyle='single'
borderColor={focusedPanel === 'actions' ? colors.focus : colors.border}
marginTop={1}
marginX={1}
paddingX={1}
flexDirection="column"
flexDirection='column'
>
<Text color={colors.primary} bold> Actions </Text>
<Box flexDirection="column" marginTop={1}>
<Box flexDirection='column' marginTop={1}>
{actionItems.map((item, index) => (
<Text
key={item.value}
@@ -403,16 +395,16 @@ export function TransactionScreen(): React.ReactElement {
{/* Broadcast confirmation dialog */}
{showBroadcastConfirm && (
<Box
position="absolute"
flexDirection="column"
alignItems="center"
justifyContent="center"
width="100%"
height="100%"
position='absolute'
flexDirection='column'
alignItems='center'
justifyContent='center'
width='100%'
height='100%'
>
<ConfirmDialog
title="Broadcast Transaction"
message="Are you sure you want to broadcast this transaction? This action cannot be undone."
title='Broadcast Transaction'
message='Are you sure you want to broadcast this transaction? This action cannot be undone.'
onConfirm={broadcastTransaction}
onCancel={() => setShowBroadcastConfirm(false)}
isActive={showBroadcastConfirm}