141 lines
4.8 KiB
TypeScript
141 lines
4.8 KiB
TypeScript
/**
|
|
* ReviewStep — final step that summarizes the import and executes it.
|
|
*
|
|
* Displays the accumulated selections (role, inputs, amounts) and on confirmation:
|
|
* 1. Adds inputs (with the selected role identifier) to the invitation.
|
|
* 2. Optionally adds a change output if the change exceeds the dust threshold.
|
|
* 3. Calls `onComplete()` to signal the flow is finished.
|
|
*/
|
|
|
|
import React, { useState, useCallback } from 'react';
|
|
import { Box, Text } from 'ink';
|
|
import { colors, formatSatoshis } from '../../../../theme.js';
|
|
import { useSatoshisConversion } from '../../../../hooks/useSatoshisConversion.js';
|
|
import { useLayeredInput } from '../../../../hooks/useInputLayer.js';
|
|
import type { ReviewStepProps, SelectableUTXO } from '../types.js';
|
|
|
|
/** Default fee estimate in satoshis. */
|
|
const DEFAULT_FEE = 500n;
|
|
|
|
/** Dust threshold — outputs below this are unspendable. */
|
|
const DUST_THRESHOLD = 546n;
|
|
|
|
export function ReviewStep({
|
|
invitation,
|
|
template,
|
|
selectedRole,
|
|
selectedInputs,
|
|
requiredAmount,
|
|
changeAmount,
|
|
onComplete,
|
|
onCancel,
|
|
isActive,
|
|
}: ReviewStepProps): React.ReactElement {
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { formatSatoshisToFiat } = useSatoshisConversion('USD');
|
|
|
|
const fee = DEFAULT_FEE;
|
|
const action = template?.actions?.[invitation.data.actionIdentifier];
|
|
|
|
// Compute totals from selected inputs
|
|
const totalSelected = selectedInputs.reduce((sum, u) => sum + u.valueSatoshis, 0n);
|
|
|
|
const getFiatSuffix = (satoshis: bigint): string => {
|
|
const fiatValue = formatSatoshisToFiat(satoshis);
|
|
return fiatValue ? ` (~${fiatValue})` : '';
|
|
};
|
|
|
|
/**
|
|
* Execute the import: add inputs (with role) and optional change output.
|
|
*/
|
|
const submit = useCallback(async () => {
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
onComplete();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [invitation, selectedRole, selectedInputs, onComplete]);
|
|
|
|
// Keyboard handling — gated by the import-flow layer.
|
|
useLayeredInput('import-flow', (_input, key) => {
|
|
if (isSubmitting) return;
|
|
|
|
if (key.return) {
|
|
submit();
|
|
} else if (key.escape) {
|
|
onCancel();
|
|
}
|
|
}, { isActive });
|
|
|
|
// Resolve role display name
|
|
const roleInfoRaw = template?.roles?.[selectedRole];
|
|
const roleInfo = roleInfoRaw && typeof roleInfoRaw === 'object' ? roleInfoRaw : null;
|
|
|
|
return (
|
|
<Box flexDirection="column">
|
|
<Text color={colors.primary} bold>Review Import</Text>
|
|
|
|
{/* Template & action */}
|
|
<Box marginTop={1} flexDirection="column">
|
|
<Text color={colors.text}>Template: {template?.name ?? invitation.data.templateIdentifier}</Text>
|
|
<Text color={colors.text}>Action: {action?.name ?? invitation.data.actionIdentifier}</Text>
|
|
<Text color={colors.text}>Role: {roleInfo?.name ?? selectedRole}</Text>
|
|
</Box>
|
|
|
|
{/* Funding summary */}
|
|
<Box marginTop={1} flexDirection="column">
|
|
<Text color={colors.primary} bold>Funding:</Text>
|
|
<Text color={colors.text}> • UTXOs: {selectedInputs.length}</Text>
|
|
<Text color={colors.text}> • Total: {formatSatoshis(totalSelected)}{getFiatSuffix(totalSelected)}</Text>
|
|
<Text color={colors.text}> • Required: {formatSatoshis(requiredAmount)}{getFiatSuffix(requiredAmount)}</Text>
|
|
<Text color={colors.text}> • Fee: {formatSatoshis(fee)}{getFiatSuffix(fee)}</Text>
|
|
{changeAmount >= DUST_THRESHOLD && (
|
|
<Text color={colors.text}> • Change: {formatSatoshis(changeAmount)}{getFiatSuffix(changeAmount)}</Text>
|
|
)}
|
|
</Box>
|
|
|
|
{selectedInputs.length > 0 && (
|
|
<Box marginTop={1} flexDirection="column">
|
|
<Text color={colors.primary} bold>Selected UTXOs:</Text>
|
|
{selectedInputs.slice(0, 3).map((utxo) => (
|
|
<Text
|
|
key={`${utxo.outpointTransactionHash}:${utxo.outpointIndex}`}
|
|
color={colors.textMuted}
|
|
>
|
|
{' '}• {formatSatoshis(utxo.valueSatoshis)}
|
|
{getFiatSuffix(utxo.valueSatoshis)}
|
|
</Text>
|
|
))}
|
|
{selectedInputs.length > 3 && (
|
|
<Text color={colors.textMuted}>
|
|
{' '}...and {selectedInputs.length - 3} more
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{/* Error display */}
|
|
{error && (
|
|
<Box marginTop={1}>
|
|
<Text color={colors.error} bold>Error: {error}</Text>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Status / hint */}
|
|
<Box marginTop={1}>
|
|
{isSubmitting ? (
|
|
<Text color={colors.info}>Submitting...</Text>
|
|
) : (
|
|
<Text color={colors.textMuted}>Enter: Confirm & Import • Esc: Cancel</Text>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|