Clean up and fixes
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user