Format with prettier. Use screen mode for invitation import - dialog mode is broken.
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useNavigation } from '../../../hooks/useNavigation.js';
|
||||
import { useAppContext, useStatus } from '../../../hooks/useAppContext.js';
|
||||
import { copyToClipboard } from '../../../utils/clipboard.js';
|
||||
import { roleRequiresInputs } from '../../../../utils/invitation-flow.js';
|
||||
import type { XOTemplate } from '@xo-cash/types';
|
||||
import type { StepConfig, FlowContext, DataResult } from '../types.js';
|
||||
import { createWizardFlow, type WizardFlow, DataWizardFlow } from '../flows/index.js';
|
||||
import { useRoleSelection } from './useRoleSelection.js';
|
||||
import { useVariableInputs } from './useVariableInputs.js';
|
||||
import { useUtxoSelection } from './useUtxoSelection.js';
|
||||
import { useInvitationManager } from './useInvitationManager.js';
|
||||
import { useWizardFocus } from './useWizardFocus.js';
|
||||
import { useWizardSteps } from './useWizardSteps.js';
|
||||
import { useState, useMemo, useCallback, useEffect } from "react";
|
||||
import { useNavigation } from "../../../hooks/useNavigation.js";
|
||||
import { useAppContext, useStatus } from "../../../hooks/useAppContext.js";
|
||||
import { copyToClipboard } from "../../../utils/clipboard.js";
|
||||
import { roleRequiresInputs } from "../../../../utils/invitation-flow.js";
|
||||
import type { XOTemplate } from "@xo-cash/types";
|
||||
import type { StepConfig, FlowContext, DataResult } from "../types.js";
|
||||
import {
|
||||
createWizardFlow,
|
||||
type WizardFlow,
|
||||
DataWizardFlow,
|
||||
} from "../flows/index.js";
|
||||
import { useRoleSelection } from "./useRoleSelection.js";
|
||||
import { useVariableInputs } from "./useVariableInputs.js";
|
||||
import { useUtxoSelection } from "./useUtxoSelection.js";
|
||||
import { useInvitationManager } from "./useInvitationManager.js";
|
||||
import { useWizardFocus } from "./useWizardFocus.js";
|
||||
import { useWizardSteps } from "./useWizardSteps.js";
|
||||
|
||||
/**
|
||||
* Thin orchestrator that composes domain hooks and wires them
|
||||
@@ -25,7 +29,7 @@ export function useActionWizard() {
|
||||
const { setStatus } = useStatus();
|
||||
|
||||
if (!appService) {
|
||||
throw new Error('AppService not initialized');
|
||||
throw new Error("AppService not initialized");
|
||||
}
|
||||
|
||||
// ── Navigation data ───────────────────────────────────────────
|
||||
@@ -35,14 +39,14 @@ export function useActionWizard() {
|
||||
const actionRolesFromNavigation = navData.actionRoles as string[] | undefined;
|
||||
|
||||
// ── Derived template data ─────────────────────────────────────
|
||||
const action = template?.actions?.[actionIdentifier ?? ''];
|
||||
const actionName = action?.name || actionIdentifier || 'Unknown';
|
||||
const action = template?.actions?.[actionIdentifier ?? ""];
|
||||
const actionName = action?.name || actionIdentifier || "Unknown";
|
||||
|
||||
// ── Flow strategy ─────────────────────────────────────────────
|
||||
const flow = useMemo<WizardFlow>(() => {
|
||||
// Create a default action if no action is found
|
||||
if (!action) {
|
||||
return createWizardFlow({ name: '', description: '' });
|
||||
return createWizardFlow({ name: "", description: "" });
|
||||
}
|
||||
|
||||
// Create the flow from the action
|
||||
@@ -50,10 +54,19 @@ export function useActionWizard() {
|
||||
}, [action]);
|
||||
|
||||
// ── Domain hooks ──────────────────────────────────────────────
|
||||
const roleSelection = useRoleSelection(template, actionIdentifier, actionRolesFromNavigation);
|
||||
const roleSelection = useRoleSelection(
|
||||
template,
|
||||
actionIdentifier,
|
||||
actionRolesFromNavigation,
|
||||
);
|
||||
const variableInputs = useVariableInputs();
|
||||
const utxoSelection = useUtxoSelection();
|
||||
const invitationManager = useInvitationManager({ appService, showError, showInfo, setStatus });
|
||||
const invitationManager = useInvitationManager({
|
||||
appService,
|
||||
showError,
|
||||
showInfo,
|
||||
setStatus,
|
||||
});
|
||||
const focus = useWizardFocus();
|
||||
|
||||
// ── Data results (data-only flows) ────────────────────────────
|
||||
@@ -66,37 +79,57 @@ export function useActionWizard() {
|
||||
const role = act?.roles?.[roleSelection.effectiveRole];
|
||||
const varIds = role?.requirements?.variables;
|
||||
if (varIds && varIds.length > 0) {
|
||||
variableInputs.initFromTemplate(template, actionIdentifier, roleSelection.effectiveRole);
|
||||
variableInputs.initFromTemplate(
|
||||
template,
|
||||
actionIdentifier,
|
||||
roleSelection.effectiveRole,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [template, actionIdentifier, roleSelection.effectiveRole, variableInputs.initFromTemplate]);
|
||||
}, [
|
||||
template,
|
||||
actionIdentifier,
|
||||
roleSelection.effectiveRole,
|
||||
variableInputs.initFromTemplate,
|
||||
]);
|
||||
|
||||
// ── Determine whether creator should provide inputs ───────────
|
||||
const shouldCollectInputs = useMemo(() => {
|
||||
if (flow.type !== 'transaction') return false;
|
||||
if (!template || !actionIdentifier || !roleSelection.effectiveRole) return false;
|
||||
if (flow.type !== "transaction") return false;
|
||||
if (!template || !actionIdentifier || !roleSelection.effectiveRole)
|
||||
return false;
|
||||
|
||||
const act = template.actions?.[actionIdentifier];
|
||||
const totalActionRoles = Object.keys(act?.roles ?? {}).length;
|
||||
const isSingleRoleAction = totalActionRoles <= 1;
|
||||
return isSingleRoleAction && roleRequiresInputs(template, actionIdentifier, roleSelection.effectiveRole);
|
||||
return (
|
||||
isSingleRoleAction &&
|
||||
roleRequiresInputs(
|
||||
template,
|
||||
actionIdentifier,
|
||||
roleSelection.effectiveRole,
|
||||
)
|
||||
);
|
||||
}, [flow.type, template, actionIdentifier, roleSelection.effectiveRole]);
|
||||
|
||||
// ── Build flow context for strategy methods ───────────────────
|
||||
const flowContext = useMemo<FlowContext>(() => ({
|
||||
availableRoles: roleSelection.availableRoles,
|
||||
hasVariables: variableInputs.variables.length > 0,
|
||||
shouldCollectInputs,
|
||||
requirementsComplete: invitationManager.requirementsComplete,
|
||||
wizardCollectedInputs: shouldCollectInputs,
|
||||
hasSignedAndBroadcasted: invitationManager.hasSignedAndBroadcasted,
|
||||
}), [
|
||||
roleSelection.availableRoles,
|
||||
variableInputs.variables.length,
|
||||
shouldCollectInputs,
|
||||
invitationManager.requirementsComplete,
|
||||
invitationManager.hasSignedAndBroadcasted,
|
||||
]);
|
||||
const flowContext = useMemo<FlowContext>(
|
||||
() => ({
|
||||
availableRoles: roleSelection.availableRoles,
|
||||
hasVariables: variableInputs.variables.length > 0,
|
||||
shouldCollectInputs,
|
||||
requirementsComplete: invitationManager.requirementsComplete,
|
||||
wizardCollectedInputs: shouldCollectInputs,
|
||||
hasSignedAndBroadcasted: invitationManager.hasSignedAndBroadcasted,
|
||||
}),
|
||||
[
|
||||
roleSelection.availableRoles,
|
||||
variableInputs.variables.length,
|
||||
shouldCollectInputs,
|
||||
invitationManager.requirementsComplete,
|
||||
invitationManager.hasSignedAndBroadcasted,
|
||||
],
|
||||
);
|
||||
|
||||
// ── Handle Enter inside a TextInput ───────────────────────────
|
||||
const handleTextInputSubmit = useCallback(() => {
|
||||
@@ -105,7 +138,12 @@ export function useActionWizard() {
|
||||
} else {
|
||||
focus.moveToButtons();
|
||||
}
|
||||
}, [focus.focusedInput, variableInputs.variables.length, focus.setFocusedInput, focus.moveToButtons]);
|
||||
}, [
|
||||
focus.focusedInput,
|
||||
variableInputs.variables.length,
|
||||
focus.setFocusedInput,
|
||||
focus.moveToButtons,
|
||||
]);
|
||||
|
||||
// ── Copy invitation ID to clipboard ───────────────────────────
|
||||
const copyId = useCallback(async () => {
|
||||
@@ -121,38 +159,61 @@ export function useActionWizard() {
|
||||
}, [invitationManager.invitationId, showInfo, showError]);
|
||||
|
||||
// ── Helper: create invitation if it doesn't exist yet ─────────
|
||||
const ensureInvitation = useCallback(async (roleId?: string): Promise<string | null> => {
|
||||
if (invitationManager.invitationId) return invitationManager.invitationId;
|
||||
const role = roleId ?? roleSelection.effectiveRole;
|
||||
if (!templateIdentifier || !actionIdentifier || !role || !template) return null;
|
||||
return invitationManager.createWithVariables(
|
||||
templateIdentifier, actionIdentifier, role, template, variableInputs.variables,
|
||||
);
|
||||
}, [
|
||||
invitationManager.invitationId,
|
||||
invitationManager.createWithVariables,
|
||||
roleSelection.effectiveRole,
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
template,
|
||||
variableInputs.variables,
|
||||
]);
|
||||
const ensureInvitation = useCallback(
|
||||
async (roleId?: string): Promise<string | null> => {
|
||||
if (invitationManager.invitationId) return invitationManager.invitationId;
|
||||
const role = roleId ?? roleSelection.effectiveRole;
|
||||
if (!templateIdentifier || !actionIdentifier || !role || !template)
|
||||
return null;
|
||||
return invitationManager.createWithVariables(
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
role,
|
||||
template,
|
||||
variableInputs.variables,
|
||||
);
|
||||
},
|
||||
[
|
||||
invitationManager.invitationId,
|
||||
invitationManager.createWithVariables,
|
||||
roleSelection.effectiveRole,
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
template,
|
||||
variableInputs.variables,
|
||||
],
|
||||
);
|
||||
|
||||
// ── Helper: load UTXOs after invitation is created ────────────
|
||||
const loadUtxosForInvitation = useCallback(async (invId: string) => {
|
||||
if (!appService || !templateIdentifier) return;
|
||||
const instance = appService.invitations.find(
|
||||
(inv) => inv.data.invitationIdentifier === invId,
|
||||
);
|
||||
if (instance) {
|
||||
invitationManager.setIsProcessing(true);
|
||||
try {
|
||||
await utxoSelection.loadUtxos(instance, templateIdentifier, variableInputs.variables, setStatus);
|
||||
} finally {
|
||||
invitationManager.setIsProcessing(false);
|
||||
const loadUtxosForInvitation = useCallback(
|
||||
async (invId: string) => {
|
||||
if (!appService || !templateIdentifier) return;
|
||||
const instance = appService.invitations.find(
|
||||
(inv) => inv.data.invitationIdentifier === invId,
|
||||
);
|
||||
if (instance) {
|
||||
invitationManager.setIsProcessing(true);
|
||||
try {
|
||||
await utxoSelection.loadUtxos(
|
||||
instance,
|
||||
templateIdentifier,
|
||||
variableInputs.variables,
|
||||
setStatus,
|
||||
);
|
||||
} finally {
|
||||
invitationManager.setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [appService, templateIdentifier, variableInputs.variables, utxoSelection.loadUtxos, invitationManager.setIsProcessing, setStatus]);
|
||||
},
|
||||
[
|
||||
appService,
|
||||
templateIdentifier,
|
||||
variableInputs.variables,
|
||||
utxoSelection.loadUtxos,
|
||||
invitationManager.setIsProcessing,
|
||||
setStatus,
|
||||
],
|
||||
);
|
||||
|
||||
// ── Build step configs from flow strategy ─────────────────────
|
||||
const stepConfigs = useMemo<StepConfig[]>(() => {
|
||||
@@ -160,39 +221,56 @@ export function useActionWizard() {
|
||||
|
||||
return stepTypes.map((type): StepConfig => {
|
||||
switch (type) {
|
||||
case 'role-select':
|
||||
case "role-select":
|
||||
return {
|
||||
type,
|
||||
name: 'Select Role',
|
||||
name: "Select Role",
|
||||
validate: () => {
|
||||
const selectedRole = roleSelection.availableRoles[roleSelection.selectedRoleIndex];
|
||||
return selectedRole ? null : 'Please select a role';
|
||||
const selectedRole =
|
||||
roleSelection.availableRoles[roleSelection.selectedRoleIndex];
|
||||
return selectedRole ? null : "Please select a role";
|
||||
},
|
||||
onNext: async () => {
|
||||
const selectedRole = roleSelection.availableRoles[roleSelection.selectedRoleIndex];
|
||||
const selectedRole =
|
||||
roleSelection.availableRoles[roleSelection.selectedRoleIndex];
|
||||
if (!selectedRole) return false;
|
||||
|
||||
// Initialize variables for this role immediately
|
||||
if (template && actionIdentifier) {
|
||||
const act = template.actions?.[actionIdentifier];
|
||||
const role = act?.roles?.[selectedRole];
|
||||
const hasVars = (role?.requirements?.variables?.length ?? 0) > 0;
|
||||
const hasVars =
|
||||
(role?.requirements?.variables?.length ?? 0) > 0;
|
||||
|
||||
if (hasVars) {
|
||||
variableInputs.initFromTemplate(template, actionIdentifier, selectedRole);
|
||||
variableInputs.initFromTemplate(
|
||||
template,
|
||||
actionIdentifier,
|
||||
selectedRole,
|
||||
);
|
||||
}
|
||||
|
||||
// If no variables step follows, create the invitation now (transaction flows only)
|
||||
if (!hasVars && flow.type === 'transaction') {
|
||||
if (!hasVars && flow.type === "transaction") {
|
||||
if (templateIdentifier && template) {
|
||||
const invId = await invitationManager.createWithVariables(
|
||||
templateIdentifier, actionIdentifier, selectedRole, template, [],
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
selectedRole,
|
||||
template,
|
||||
[],
|
||||
);
|
||||
if (!invId) return false;
|
||||
|
||||
// Pre-load UTXOs if the inputs step follows
|
||||
const totalRoles = Object.keys(act?.roles ?? {}).length;
|
||||
const needsInputs = totalRoles <= 1 && roleRequiresInputs(template, actionIdentifier, selectedRole);
|
||||
const needsInputs =
|
||||
totalRoles <= 1 &&
|
||||
roleRequiresInputs(
|
||||
template,
|
||||
actionIdentifier,
|
||||
selectedRole,
|
||||
);
|
||||
if (needsInputs) {
|
||||
await loadUtxosForInvitation(invId);
|
||||
}
|
||||
@@ -206,18 +284,27 @@ export function useActionWizard() {
|
||||
},
|
||||
};
|
||||
|
||||
case 'variables':
|
||||
case "variables":
|
||||
return {
|
||||
type,
|
||||
name: 'Variables',
|
||||
name: "Variables",
|
||||
validate: () => variableInputs.validate(),
|
||||
onNext: async () => {
|
||||
if (flow.type === 'transaction') {
|
||||
if (!templateIdentifier || !actionIdentifier || !template || !roleSelection.effectiveRole) return false;
|
||||
if (flow.type === "transaction") {
|
||||
if (
|
||||
!templateIdentifier ||
|
||||
!actionIdentifier ||
|
||||
!template ||
|
||||
!roleSelection.effectiveRole
|
||||
)
|
||||
return false;
|
||||
|
||||
const invId = await invitationManager.createWithVariables(
|
||||
templateIdentifier, actionIdentifier, roleSelection.effectiveRole,
|
||||
template, variableInputs.variables,
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
roleSelection.effectiveRole,
|
||||
template,
|
||||
variableInputs.variables,
|
||||
);
|
||||
if (!invId) return false;
|
||||
|
||||
@@ -233,23 +320,28 @@ export function useActionWizard() {
|
||||
},
|
||||
};
|
||||
|
||||
case 'inputs':
|
||||
case "inputs":
|
||||
return {
|
||||
type,
|
||||
name: 'Select UTXOs',
|
||||
name: "Select UTXOs",
|
||||
validate: () => utxoSelection.validate(),
|
||||
onNext: async () => {
|
||||
const selectedUtxos = utxoSelection.availableUtxos.filter((u) => u.selected);
|
||||
const success = await invitationManager.addInputsAndOutputs(selectedUtxos, utxoSelection.changeAmount);
|
||||
const selectedUtxos = utxoSelection.availableUtxos.filter(
|
||||
(u) => u.selected,
|
||||
);
|
||||
const success = await invitationManager.addInputsAndOutputs(
|
||||
selectedUtxos,
|
||||
utxoSelection.changeAmount,
|
||||
);
|
||||
if (success) focus.resetToContent();
|
||||
return success;
|
||||
},
|
||||
};
|
||||
|
||||
case 'review':
|
||||
case "review":
|
||||
return {
|
||||
type,
|
||||
name: 'Review',
|
||||
name: "Review",
|
||||
validate: () => null,
|
||||
onNext: async () => {
|
||||
// Ensure invitation exists (covers the case where no prior step created it)
|
||||
@@ -261,10 +353,10 @@ export function useActionWizard() {
|
||||
},
|
||||
};
|
||||
|
||||
case 'publish':
|
||||
case "publish":
|
||||
return {
|
||||
type,
|
||||
name: 'Publish',
|
||||
name: "Publish",
|
||||
validate: () => null,
|
||||
onNext: async () => {
|
||||
if (flow.canFinalize(flowContext)) {
|
||||
@@ -277,10 +369,10 @@ export function useActionWizard() {
|
||||
},
|
||||
};
|
||||
|
||||
case 'result':
|
||||
case "result":
|
||||
return {
|
||||
type,
|
||||
name: 'Result',
|
||||
name: "Result",
|
||||
validate: () => null,
|
||||
onNext: async () => {
|
||||
// Data-only flows: populate stubbed results, then exit
|
||||
@@ -290,7 +382,7 @@ export function useActionWizard() {
|
||||
return {
|
||||
id: dataId,
|
||||
name: dataDef?.hint ?? dataId,
|
||||
type: dataDef?.type ?? 'unknown',
|
||||
type: dataDef?.type ?? "unknown",
|
||||
hint: dataDef?.hint,
|
||||
value: null, // Engine-level data execution not yet implemented
|
||||
};
|
||||
@@ -303,13 +395,30 @@ export function useActionWizard() {
|
||||
};
|
||||
|
||||
default:
|
||||
return { type, name: type, validate: () => null, onNext: async () => true };
|
||||
return {
|
||||
type,
|
||||
name: type,
|
||||
validate: () => null,
|
||||
onNext: async () => true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}, [
|
||||
flow, flowContext, roleSelection, variableInputs, utxoSelection,
|
||||
invitationManager, focus, template, templateIdentifier, actionIdentifier,
|
||||
shouldCollectInputs, ensureInvitation, loadUtxosForInvitation, goBack, setStatus,
|
||||
flow,
|
||||
flowContext,
|
||||
roleSelection,
|
||||
variableInputs,
|
||||
utxoSelection,
|
||||
invitationManager,
|
||||
focus,
|
||||
template,
|
||||
templateIdentifier,
|
||||
actionIdentifier,
|
||||
shouldCollectInputs,
|
||||
ensureInvitation,
|
||||
loadUtxosForInvitation,
|
||||
goBack,
|
||||
setStatus,
|
||||
]);
|
||||
|
||||
// ── Step navigation ───────────────────────────────────────────
|
||||
@@ -318,7 +427,7 @@ export function useActionWizard() {
|
||||
// ── Set initial status ────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!template || !actionIdentifier) {
|
||||
showError('Missing wizard data');
|
||||
showError("Missing wizard data");
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
@@ -327,22 +436,30 @@ export function useActionWizard() {
|
||||
? `${actionIdentifier}/${roleSelection.effectiveRole}`
|
||||
: actionIdentifier,
|
||||
);
|
||||
}, [template, actionIdentifier, roleSelection.effectiveRole, showError, goBack, setStatus]);
|
||||
}, [
|
||||
template,
|
||||
actionIdentifier,
|
||||
roleSelection.effectiveRole,
|
||||
showError,
|
||||
goBack,
|
||||
setStatus,
|
||||
]);
|
||||
|
||||
// ── Convenience derived values ────────────────────────────────
|
||||
const textInputHasFocus =
|
||||
stepper.currentStepData?.type === 'variables' && focus.focusArea === 'content';
|
||||
stepper.currentStepData?.type === "variables" &&
|
||||
focus.focusArea === "content";
|
||||
|
||||
const canSignAndBroadcast = flow.canFinalize(flowContext);
|
||||
|
||||
const isLastStep = stepper.currentStep >= stepper.steps.length - 1;
|
||||
const lastStepType = stepper.currentStepData?.type;
|
||||
const nextButtonLabel =
|
||||
lastStepType === 'publish'
|
||||
lastStepType === "publish"
|
||||
? flow.getFinalActionLabel(flowContext)
|
||||
: lastStepType === 'result'
|
||||
? 'Done'
|
||||
: 'Next';
|
||||
: lastStepType === "result"
|
||||
? "Done"
|
||||
: "Next";
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user