/** * 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(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 ( Review Import {/* Template & action */} Template: {template?.name ?? invitation.data.templateIdentifier} Action: {action?.name ?? invitation.data.actionIdentifier} Role: {roleInfo?.name ?? selectedRole} {/* Funding summary */} Funding: • UTXOs: {selectedInputs.length} • Total: {formatSatoshis(totalSelected)}{getFiatSuffix(totalSelected)} • Required: {formatSatoshis(requiredAmount)}{getFiatSuffix(requiredAmount)} • Fee: {formatSatoshis(fee)}{getFiatSuffix(fee)} {changeAmount >= DUST_THRESHOLD && ( • Change: {formatSatoshis(changeAmount)}{getFiatSuffix(changeAmount)} )} {selectedInputs.length > 0 && ( Selected UTXOs: {selectedInputs.slice(0, 3).map((utxo) => ( {' '}• {formatSatoshis(utxo.valueSatoshis)} {getFiatSuffix(utxo.valueSatoshis)} ))} {selectedInputs.length > 3 && ( {' '}...and {selectedInputs.length - 3} more )} )} {/* Error display */} {error && ( Error: {error} )} {/* Status / hint */} {isSubmitting ? ( Submitting... ) : ( Enter: Confirm & Import • Esc: Cancel )} ); }