Fix receive and send
This commit is contained in:
@@ -205,7 +205,13 @@ export function ActionWizardScreen(): React.ReactElement {
|
||||
/>
|
||||
);
|
||||
case 'publish':
|
||||
return <PublishStep invitationId={wizard.invitationId} />;
|
||||
return (
|
||||
<PublishStep
|
||||
invitationId={wizard.invitationId}
|
||||
requirementsComplete={wizard.requirementsComplete}
|
||||
hasSignedAndBroadcasted={wizard.hasSignedAndBroadcasted}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -284,7 +290,9 @@ export function ActionWizardScreen(): React.ReactElement {
|
||||
</Box>
|
||||
<Button
|
||||
label={
|
||||
wizard.currentStepData?.type === "publish" ? "Done" : "Next"
|
||||
wizard.currentStepData?.type === "publish"
|
||||
? (wizard.canSignAndBroadcast ? "Sign & Broadcast" : "Done")
|
||||
: "Next"
|
||||
}
|
||||
focused={
|
||||
wizard.focusArea === "buttons" &&
|
||||
|
||||
@@ -4,15 +4,19 @@ import { colors } from '../../../theme.js';
|
||||
|
||||
interface PublishStepProps {
|
||||
invitationId: string | null;
|
||||
requirementsComplete: boolean;
|
||||
hasSignedAndBroadcasted: boolean;
|
||||
}
|
||||
|
||||
export function PublishStep({
|
||||
invitationId,
|
||||
requirementsComplete,
|
||||
hasSignedAndBroadcasted,
|
||||
}: PublishStepProps): React.ReactElement {
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Text color={colors.success} bold>
|
||||
✓ Invitation Created & Published!
|
||||
✓ Invitation Ready
|
||||
</Text>
|
||||
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
@@ -30,9 +34,19 @@ export function PublishStep({
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={colors.textMuted}>
|
||||
Share this ID with the other party to complete the transaction.
|
||||
</Text>
|
||||
{hasSignedAndBroadcasted ? (
|
||||
<Text color={colors.success}>
|
||||
Transaction signed and broadcasted.
|
||||
</Text>
|
||||
) : requirementsComplete ? (
|
||||
<Text color={colors.textMuted}>
|
||||
Requirements are complete. Use the Sign & Broadcast button to finalize.
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={colors.warning}>
|
||||
Requirements are incomplete. Complete missing requirements before signing.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
|
||||
@@ -4,6 +4,15 @@ import { useAppContext, useStatus } from '../../hooks/useAppContext.js';
|
||||
import { formatSatoshis } from '../../theme.js';
|
||||
import { copyToClipboard } from '../../utils/clipboard.js';
|
||||
import type { XOTemplate, XOInvitation, XOTemplateTransactionOutput } from '@xo-cash/types';
|
||||
import {
|
||||
autoSelectGreedyUtxos,
|
||||
getTransactionOutputIdentifier,
|
||||
isInvitationRequirementsComplete,
|
||||
mapUnspentOutputsToSelectable,
|
||||
resolveActionRoles,
|
||||
resolveProvidedLockingBytecodeHex,
|
||||
roleRequiresInputs,
|
||||
} from '../../../utils/invitation-flow.js';
|
||||
import type {
|
||||
WizardStep,
|
||||
VariableInput,
|
||||
@@ -22,6 +31,7 @@ export function useActionWizard() {
|
||||
const templateIdentifier = navData.templateIdentifier as string | undefined;
|
||||
const actionIdentifier = navData.actionIdentifier as string | undefined;
|
||||
const template = navData.template as XOTemplate | undefined;
|
||||
const actionRolesFromNavigation = navData.actionRoles as string[] | undefined;
|
||||
|
||||
// ── Role selection state ────────────────────────────────────────
|
||||
const [roleIdentifier, setRoleIdentifier] = useState<string | undefined>();
|
||||
@@ -32,14 +42,20 @@ export function useActionWizard() {
|
||||
* `start` entries filtered to the current action.
|
||||
*/
|
||||
const availableRoles = useMemo(() => {
|
||||
if (!template || !actionIdentifier) return [];
|
||||
const starts = template.start ?? [];
|
||||
const roleIds = starts
|
||||
.filter((s) => s.action === actionIdentifier)
|
||||
.map((s) => s.role);
|
||||
// Deduplicate while preserving order
|
||||
return [...new Set(roleIds)];
|
||||
}, [template, actionIdentifier]);
|
||||
return resolveActionRoles(template, actionIdentifier, actionRolesFromNavigation);
|
||||
}, [template, actionIdentifier, actionRolesFromNavigation]);
|
||||
|
||||
const effectiveRoleForFlow = roleIdentifier ?? (
|
||||
availableRoles.length === 1 ? availableRoles[0] : undefined
|
||||
);
|
||||
|
||||
// Keep role state aligned when only one role exists for the selected action.
|
||||
// This preserves existing UI bindings that read roleIdentifier directly.
|
||||
useEffect(() => {
|
||||
if (!roleIdentifier && availableRoles.length === 1) {
|
||||
setRoleIdentifier(availableRoles[0]);
|
||||
}
|
||||
}, [roleIdentifier, availableRoles]);
|
||||
|
||||
// ── Wizard state ─────────────────────────────────────────────────
|
||||
const [steps, setSteps] = useState<WizardStep[]>([]);
|
||||
@@ -57,6 +73,8 @@ export function useActionWizard() {
|
||||
// ── Invitation ───────────────────────────────────────────────────
|
||||
const [invitation, setInvitation] = useState<XOInvitation | null>(null);
|
||||
const [invitationId, setInvitationId] = useState<string | null>(null);
|
||||
const [requirementsComplete, setRequirementsComplete] = useState(false);
|
||||
const [hasSignedAndBroadcasted, setHasSignedAndBroadcasted] = useState(false);
|
||||
|
||||
// ── UI state ─────────────────────────────────────────────────────
|
||||
const [focusedInput, setFocusedInput] = useState(0);
|
||||
@@ -78,9 +96,19 @@ export function useActionWizard() {
|
||||
const textInputHasFocus =
|
||||
currentStepData?.type === 'variables' && focusArea === 'content';
|
||||
|
||||
// Whether the wizard actually includes an inputs step — this determines if
|
||||
// the creator provided funding and therefore can sign & broadcast locally.
|
||||
const wizardCollectedInputs = steps.some((s) => s.type === 'inputs');
|
||||
|
||||
const canSignAndBroadcast =
|
||||
currentStepData?.type === 'publish'
|
||||
&& wizardCollectedInputs
|
||||
&& requirementsComplete
|
||||
&& !hasSignedAndBroadcasted;
|
||||
|
||||
// ── Initialization ───────────────────────────────────────────────
|
||||
// Builds the wizard steps dynamically based on the selected role.
|
||||
// Re-runs when roleIdentifier changes to add role-specific steps.
|
||||
// Re-runs when role selection changes to add role-specific steps.
|
||||
useEffect(() => {
|
||||
if (!template || !actionIdentifier) {
|
||||
showError('Missing wizard data');
|
||||
@@ -89,14 +117,17 @@ export function useActionWizard() {
|
||||
}
|
||||
|
||||
const wizardSteps: WizardStep[] = [];
|
||||
const shouldShowRoleSelection = availableRoles.length > 1;
|
||||
|
||||
// Always start with role selection
|
||||
wizardSteps.push({ name: 'Select Role', type: 'role-select' });
|
||||
// Only require explicit role selection when the action is actually ambiguous.
|
||||
if (shouldShowRoleSelection) {
|
||||
wizardSteps.push({ name: 'Select Role', type: 'role-select' });
|
||||
}
|
||||
|
||||
// Add role-specific steps only after role is selected
|
||||
if (roleIdentifier) {
|
||||
if (effectiveRoleForFlow) {
|
||||
const act = template.actions?.[actionIdentifier];
|
||||
const role = act?.roles?.[roleIdentifier];
|
||||
const role = act?.roles?.[effectiveRoleForFlow];
|
||||
const requirements = role?.requirements;
|
||||
|
||||
// Add variables step if needed
|
||||
@@ -116,8 +147,23 @@ export function useActionWizard() {
|
||||
setVariables(varInputs);
|
||||
}
|
||||
|
||||
// Add inputs step if role requires slots (funding inputs)
|
||||
if (requirements?.slots && requirements.slots.min > 0) {
|
||||
// Determine whether the creator should provide inputs during this wizard.
|
||||
//
|
||||
// Single-role actions (e.g. "send"): the creator is the sole participant,
|
||||
// so we collect inputs here if the role needs them at all.
|
||||
//
|
||||
// Multi-role actions (e.g. "receive"): the creator is setting up the
|
||||
// invitation for another party to accept. We only collect inputs during
|
||||
// creation if the role EXPLICITLY requires them (slots.min > 0).
|
||||
// Implicit inputs (transaction-level) are assumed to be provided later
|
||||
// by the accepting party.
|
||||
const totalActionRoles = Object.keys(act?.roles ?? {}).length;
|
||||
const isSingleRoleAction = totalActionRoles <= 1;
|
||||
|
||||
const shouldCollectInputs =
|
||||
isSingleRoleAction && roleRequiresInputs(template, actionIdentifier, effectiveRoleForFlow);
|
||||
|
||||
if (shouldCollectInputs) {
|
||||
wizardSteps.push({ name: 'Select UTXOs', type: 'inputs' });
|
||||
}
|
||||
}
|
||||
@@ -127,11 +173,12 @@ export function useActionWizard() {
|
||||
wizardSteps.push({ name: 'Publish', type: 'publish' });
|
||||
|
||||
setSteps(wizardSteps);
|
||||
setStatus(roleIdentifier ? `${actionIdentifier}/${roleIdentifier}` : actionIdentifier);
|
||||
setStatus(effectiveRoleForFlow ? `${actionIdentifier}/${effectiveRoleForFlow}` : actionIdentifier);
|
||||
}, [
|
||||
template,
|
||||
actionIdentifier,
|
||||
roleIdentifier,
|
||||
availableRoles.length,
|
||||
effectiveRoleForFlow,
|
||||
showError,
|
||||
goBack,
|
||||
setStatus,
|
||||
@@ -141,12 +188,12 @@ export function useActionWizard() {
|
||||
// This runs after the main useEffect has rebuilt steps, ensuring
|
||||
// we advance to the correct step (variables, inputs, or review).
|
||||
useEffect(() => {
|
||||
if (roleIdentifier && currentStep === 0 && steps[0]?.type === 'role-select') {
|
||||
if (effectiveRoleForFlow && currentStep === 0 && steps[0]?.type === 'role-select') {
|
||||
setCurrentStep(1);
|
||||
setFocusArea('content');
|
||||
setFocusedInput(0);
|
||||
}
|
||||
}, [roleIdentifier, currentStep, steps]);
|
||||
}, [effectiveRoleForFlow, currentStep, steps]);
|
||||
|
||||
// ── Update a single variable value ───────────────────────────────
|
||||
const updateVariable = useCallback((index: number, value: string) => {
|
||||
@@ -195,6 +242,25 @@ export function useActionWizard() {
|
||||
}
|
||||
}, [invitationId, showInfo, showError]);
|
||||
|
||||
const refreshRequirementState = useCallback(async (identifier: string | null = invitationId) => {
|
||||
if (!identifier || !appService) {
|
||||
setRequirementsComplete(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const invitationInstance = appService.invitations.find(
|
||||
(inv) => inv.data.invitationIdentifier === identifier
|
||||
);
|
||||
if (!invitationInstance) {
|
||||
setRequirementsComplete(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const complete = await isInvitationRequirementsComplete(invitationInstance);
|
||||
setRequirementsComplete(complete);
|
||||
return complete;
|
||||
}, [appService, invitationId]);
|
||||
|
||||
// ── Load available UTXOs for the inputs step ────────────────────
|
||||
const loadAvailableUtxos = useCallback(async () => {
|
||||
if (!invitation || !templateIdentifier || !appService || !invitationId) {
|
||||
@@ -225,49 +291,19 @@ export function useActionWizard() {
|
||||
throw new Error('Invitation not found');
|
||||
}
|
||||
|
||||
// Query for suitable resources
|
||||
// Query for suitable resources.
|
||||
// NOTE: Even for single-role actions we still keep the user in the loop for inputs:
|
||||
// we only surface UTXOs the engine/template currently considers "selectable" and let
|
||||
// the user confirm them in the inputs step. If selectable semantics evolve, revisit here.
|
||||
const unspentOutputs = await invitationInstance.findSuitableResources({
|
||||
templateIdentifier,
|
||||
outputIdentifier: 'receiveOutput',
|
||||
});
|
||||
|
||||
// Map to selectable UTXOs
|
||||
const utxos: SelectableUTXO[] = unspentOutputs.map((utxo: any) => ({
|
||||
outpointTransactionHash: utxo.outpointTransactionHash,
|
||||
outpointIndex: utxo.outpointIndex,
|
||||
valueSatoshis: BigInt(utxo.valueSatoshis),
|
||||
lockingBytecode: utxo.lockingBytecode
|
||||
? typeof utxo.lockingBytecode === 'string'
|
||||
? utxo.lockingBytecode
|
||||
: Buffer.from(utxo.lockingBytecode).toString('hex')
|
||||
: undefined,
|
||||
selected: false,
|
||||
}));
|
||||
|
||||
// Auto-select UTXOs greedily until the requirement is met
|
||||
let accumulated = 0n;
|
||||
const seenLockingBytecodes = new Set<string>();
|
||||
|
||||
for (const utxo of utxos) {
|
||||
if (
|
||||
utxo.lockingBytecode &&
|
||||
seenLockingBytecodes.has(utxo.lockingBytecode)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (utxo.lockingBytecode) {
|
||||
seenLockingBytecodes.add(utxo.lockingBytecode);
|
||||
}
|
||||
|
||||
utxo.selected = true;
|
||||
accumulated += utxo.valueSatoshis;
|
||||
|
||||
if (accumulated >= requested + fee) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setAvailableUtxos(utxos);
|
||||
// Map to selectable UTXOs and pre-select greedily.
|
||||
const mappedUtxos = mapUnspentOutputsToSelectable(unspentOutputs);
|
||||
const autoSelectedUtxos = autoSelectGreedyUtxos(mappedUtxos, requested + fee);
|
||||
setAvailableUtxos(autoSelectedUtxos as SelectableUTXO[]);
|
||||
setStatus('Ready');
|
||||
} catch (error) {
|
||||
showError(
|
||||
@@ -301,7 +337,7 @@ export function useActionWizard() {
|
||||
*/
|
||||
const createInvitationWithVariables = useCallback(
|
||||
async (roleId?: string): Promise<boolean> => {
|
||||
const effectiveRole = roleId ?? roleIdentifier;
|
||||
const effectiveRole = roleId ?? effectiveRoleForFlow;
|
||||
|
||||
if (
|
||||
!templateIdentifier ||
|
||||
@@ -350,6 +386,14 @@ export function useActionWizard() {
|
||||
inv = invitationInstance.data;
|
||||
}
|
||||
|
||||
const variableValuesByIdentifier = variables.reduce((acc, variable) => {
|
||||
if (typeof variable.value === 'string' && variable.value.trim().length > 0) {
|
||||
acc[variable.id] = variable.value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
// Add template-required outputs for the current role
|
||||
const act = template.actions?.[actionIdentifier];
|
||||
const transaction = act?.transaction
|
||||
@@ -358,17 +402,26 @@ export function useActionWizard() {
|
||||
|
||||
if (transaction?.outputs && transaction.outputs.length > 0) {
|
||||
setStatus('Adding required outputs...');
|
||||
const outputsToAdd = await Promise.all(transaction.outputs.map(async (output: XOTemplateTransactionOutput) => {
|
||||
const outputIdentifier = getTransactionOutputIdentifier(output);
|
||||
if (!outputIdentifier) {
|
||||
throw new Error('Invalid transaction output definition');
|
||||
}
|
||||
|
||||
const outputsToAdd = await Promise.all(transaction.outputs.map(
|
||||
async (output: XOTemplateTransactionOutput) => ({
|
||||
// TODO: Fix this. Currently, there is a type mismatch due to branches/versions of the libraries
|
||||
outputIdentifier: output as unknown as string,
|
||||
// roleIdentifier: roleIdentifier,
|
||||
const providedLockingBytecodeHex = resolveProvidedLockingBytecodeHex(
|
||||
template,
|
||||
outputIdentifier,
|
||||
variableValuesByIdentifier,
|
||||
);
|
||||
|
||||
// TODO: This feels like an odd requirement? Shouldnt this be handled in the engine?
|
||||
lockingBytecode: await invitationInstance.generateLockingBytecode(output as unknown as string, roleIdentifier),
|
||||
})
|
||||
));
|
||||
const lockingBytecodeHex = providedLockingBytecodeHex
|
||||
?? await invitationInstance.generateLockingBytecode(outputIdentifier, effectiveRole);
|
||||
|
||||
return {
|
||||
outputIdentifier,
|
||||
lockingBytecode: lockingBytecodeHex,
|
||||
};
|
||||
}));
|
||||
|
||||
// TODO: Clean this up. Suggestions: 1. Convert to bytes above. 2. Have addOuputs accept a hex string. 3. Have addOutputs handling the lockscript generation
|
||||
await invitationInstance.addOutputs(outputsToAdd.map((output) => ({
|
||||
@@ -381,6 +434,7 @@ export function useActionWizard() {
|
||||
}
|
||||
|
||||
setInvitation(inv);
|
||||
await refreshRequirementState(invId);
|
||||
setStatus('Invitation created');
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -395,15 +449,51 @@ export function useActionWizard() {
|
||||
[
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
roleIdentifier,
|
||||
effectiveRoleForFlow,
|
||||
template,
|
||||
variables,
|
||||
appService,
|
||||
showError,
|
||||
setStatus,
|
||||
refreshRequirementState,
|
||||
]
|
||||
);
|
||||
|
||||
// Ensure invitation exists before entering input/review/publish stages.
|
||||
useEffect(() => {
|
||||
const ensureInvitation = async () => {
|
||||
if (!currentStepData) return;
|
||||
if (currentStepData.type !== 'inputs' && currentStepData.type !== 'review' && currentStepData.type !== 'publish') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (invitationId) {
|
||||
if (currentStepData.type === 'inputs' && availableUtxos.length === 0 && !isProcessing) {
|
||||
await loadAvailableUtxos();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!effectiveRoleForFlow || isProcessing) return;
|
||||
const success = await createInvitationWithVariables(effectiveRoleForFlow);
|
||||
if (!success) return;
|
||||
|
||||
if (currentStepData.type === 'inputs') {
|
||||
await loadAvailableUtxos();
|
||||
}
|
||||
};
|
||||
|
||||
ensureInvitation().catch(() => {});
|
||||
}, [
|
||||
currentStepData,
|
||||
invitationId,
|
||||
effectiveRoleForFlow,
|
||||
isProcessing,
|
||||
createInvitationWithVariables,
|
||||
loadAvailableUtxos,
|
||||
availableUtxos.length,
|
||||
]);
|
||||
|
||||
// ── Add selected inputs + change output to the invitation ───────
|
||||
const addInputsAndOutputs = useCallback(async () => {
|
||||
if (!invitationId || !invitation || !appService) return;
|
||||
@@ -459,6 +549,7 @@ export function useActionWizard() {
|
||||
];
|
||||
|
||||
await invitationInstance.addOutputs(outputs);
|
||||
await refreshRequirementState(invitationId);
|
||||
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
setStatus('Inputs and outputs added');
|
||||
@@ -480,14 +571,15 @@ export function useActionWizard() {
|
||||
appService,
|
||||
showError,
|
||||
setStatus,
|
||||
refreshRequirementState,
|
||||
]);
|
||||
|
||||
// ── Publish the invitation ──────────────────────────────────────
|
||||
const publishInvitation = useCallback(async () => {
|
||||
// ── Move to publish step ────────────────────────────────────────
|
||||
const advanceToPublishStep = useCallback(async () => {
|
||||
if (!invitationId || !appService) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
setStatus('Publishing invitation...');
|
||||
setStatus('Preparing publish step...');
|
||||
|
||||
try {
|
||||
const invitationInstance = appService.invitations.find(
|
||||
@@ -498,23 +590,61 @@ export function useActionWizard() {
|
||||
throw new Error('Invitation not found');
|
||||
}
|
||||
|
||||
// Already tracked and synced via SSE from createInvitation
|
||||
await refreshRequirementState(invitationId);
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
setStatus('Invitation published');
|
||||
setStatus('Ready to publish');
|
||||
} catch (error) {
|
||||
showError(
|
||||
`Failed to publish: ${error instanceof Error ? error.message : String(error)}`
|
||||
`Failed to prepare publish step: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [invitationId, appService, showError, setStatus]);
|
||||
}, [invitationId, appService, showError, setStatus, refreshRequirementState]);
|
||||
|
||||
// ── Sign and broadcast from publish step ────────────────────────
|
||||
const signAndBroadcastInvitation = useCallback(async () => {
|
||||
if (!invitationId || !appService) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
setStatus('Signing invitation...');
|
||||
|
||||
try {
|
||||
const invitationInstance = appService.invitations.find(
|
||||
(inv) => inv.data.invitationIdentifier === invitationId
|
||||
);
|
||||
if (!invitationInstance) {
|
||||
throw new Error('Invitation not found');
|
||||
}
|
||||
|
||||
const complete = await refreshRequirementState(invitationId);
|
||||
if (!complete) {
|
||||
showError('Invitation requirements are not complete yet.');
|
||||
return;
|
||||
}
|
||||
if (!wizardCollectedInputs) {
|
||||
showError('This action does not require funding inputs, so it cannot be signed and broadcasted here.');
|
||||
return;
|
||||
}
|
||||
|
||||
await invitationInstance.sign();
|
||||
setStatus('Broadcasting transaction...');
|
||||
await invitationInstance.broadcast();
|
||||
setHasSignedAndBroadcasted(true);
|
||||
setStatus('Transaction signed and broadcasted');
|
||||
showInfo('Transaction signed and broadcasted.');
|
||||
await refreshRequirementState(invitationId);
|
||||
} catch (error) {
|
||||
showError(`Failed to sign and broadcast: ${error instanceof Error ? error.message : String(error)}`);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [invitationId, appService, setStatus, showError, showInfo, refreshRequirementState, wizardCollectedInputs]);
|
||||
|
||||
// ── Navigate to the next step ───────────────────────────────────
|
||||
const nextStep = useCallback(async () => {
|
||||
if (currentStep >= steps.length - 1) return;
|
||||
|
||||
const stepType = currentStepData?.type;
|
||||
if (currentStep >= steps.length - 1 && stepType !== 'publish') return;
|
||||
|
||||
// ── Role selection ──────────────────────────────────────────
|
||||
if (stepType === 'role-select') {
|
||||
@@ -531,7 +661,19 @@ export function useActionWizard() {
|
||||
|
||||
const hasVariables =
|
||||
requirements?.variables && requirements.variables.length > 0;
|
||||
const hasSlots = requirements?.slots && requirements.slots.min > 0;
|
||||
|
||||
// Mirror the inputs-step inference from the step-building effect:
|
||||
// single-role → any inputs; multi-role → explicit requirements only.
|
||||
const totalActionRoles = Object.keys(act?.roles ?? {}).length;
|
||||
const roleExplicitlyNeedsInputs =
|
||||
(requirements?.slots && requirements.slots.min > 0)
|
||||
|| (act?.requirements?.roles?.find(
|
||||
(r: { role: string; slots?: { min?: number } }) => r.role === selectedRole,
|
||||
)?.slots?.min ?? 0) > 0;
|
||||
|
||||
const hasSlots = totalActionRoles <= 1
|
||||
? roleRequiresInputs(template, actionIdentifier, selectedRole)
|
||||
: roleExplicitlyNeedsInputs;
|
||||
|
||||
// If there is no variables step, the invitation must be created now
|
||||
// because the variables step would normally handle it.
|
||||
@@ -582,17 +724,38 @@ export function useActionWizard() {
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────────
|
||||
if (stepType === 'inputs') {
|
||||
if (!invitationId) {
|
||||
const success = await createInvitationWithVariables();
|
||||
if (!success) return;
|
||||
await loadAvailableUtxos();
|
||||
return;
|
||||
}
|
||||
await addInputsAndOutputs();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Review ──────────────────────────────────────────────────
|
||||
if (stepType === 'review') {
|
||||
await publishInvitation();
|
||||
if (!invitationId) {
|
||||
const success = await createInvitationWithVariables();
|
||||
if (!success) return;
|
||||
}
|
||||
await advanceToPublishStep();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Generic advance (e.g. publish → done) ───────────────────
|
||||
// ── Publish ─────────────────────────────────────────────────
|
||||
if (stepType === 'publish') {
|
||||
if (canSignAndBroadcast) {
|
||||
await signAndBroadcastInvitation();
|
||||
return;
|
||||
}
|
||||
// Done should exit the wizard, not advance past the final step.
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Generic advance ─────────────────────────────────────────
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
setFocusArea('content');
|
||||
setFocusedInput(0);
|
||||
@@ -600,6 +763,7 @@ export function useActionWizard() {
|
||||
currentStep,
|
||||
steps,
|
||||
currentStepData,
|
||||
canSignAndBroadcast,
|
||||
availableRoles,
|
||||
selectedRoleIndex,
|
||||
template,
|
||||
@@ -609,7 +773,11 @@ export function useActionWizard() {
|
||||
createInvitationWithVariables,
|
||||
loadAvailableUtxos,
|
||||
addInputsAndOutputs,
|
||||
publishInvitation,
|
||||
advanceToPublishStep,
|
||||
requirementsComplete,
|
||||
hasSignedAndBroadcasted,
|
||||
signAndBroadcastInvitation,
|
||||
goBack,
|
||||
]);
|
||||
|
||||
// ── Navigate to the previous step ──────────────────────────────
|
||||
@@ -667,6 +835,9 @@ export function useActionWizard() {
|
||||
// Invitation
|
||||
invitation,
|
||||
invitationId,
|
||||
requirementsComplete,
|
||||
hasSignedAndBroadcasted,
|
||||
canSignAndBroadcast,
|
||||
|
||||
// UI focus
|
||||
focusedInput,
|
||||
|
||||
Reference in New Issue
Block a user