Fix history for the 100th time. Fix role resolution in the invitation screen

This commit is contained in:
2026-05-04 11:36:09 +00:00
parent 2f2e515d72
commit dedfb69dff
4 changed files with 663 additions and 680 deletions

View File

@@ -350,29 +350,32 @@ export function WalletStateScreen(): React.ReactElement {
const indicator = isFocused ? '▸ ' : ' ';
const groupingPrefix = row.isNested ? ' -> ' : '';
if (row.type === 'invitation') {
if (row.type === 'history_item') {
const sats = row.valueSatoshis ?? 0n;
const fiatSuffix = getFiatSuffix(sats);
return (
<Box flexDirection="row" justifyContent="space-between">
<Text color={itemColor}>
{indicator}[Invitation] {row.label}
</Text>
<Box flexDirection="row">
<Text color={itemColor}>
{indicator}{formatSatoshis(sats)}{fiatSuffix}
</Text>
<Text color={colors.textMuted}> {row.label}</Text>
</Box>
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
</Box>
);
}
if (row.type === 'invitation_input') {
const inputSatoshis = row.utxo?.valueSatoshis;
const inputFiatSuffix = inputSatoshis !== undefined
? getFiatSuffix(inputSatoshis)
: '';
if (row.type === 'history_input') {
const sats = row.valueSatoshis ?? 0n;
return (
<Box flexDirection="row" justifyContent="space-between">
<Box>
<Text color={itemColor}>
{indicator}{groupingPrefix}[Input] {row.label}
{inputFiatSuffix}
{indicator}{groupingPrefix}[Input] {formatSatoshis(sats)}
{getFiatSuffix(sats)}
</Text>
<Text color={colors.textMuted}> {row.label}</Text>
{row.description && <Text color={colors.textMuted}> {row.description}</Text>}
</Box>
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
@@ -380,8 +383,9 @@ export function WalletStateScreen(): React.ReactElement {
);
}
if (row.type === 'invitation_output') {
const sats = row.utxo?.valueSatoshis ?? 0n;
if (row.type === 'history_output') {
const sats = row.valueSatoshis ?? 0n;
const reservedTag = row.reserved ? ' [Reserved]' : '';
return (
<Box flexDirection="row" justifyContent="space-between">
<Box flexDirection="row">
@@ -389,6 +393,7 @@ export function WalletStateScreen(): React.ReactElement {
{indicator}{groupingPrefix}[Output] {formatSatoshis(sats)}
{getFiatSuffix(sats)}
</Text>
<Text color={colors.textMuted}> {row.label}{reservedTag}</Text>
{row.description && <Text color={colors.textMuted}> {row.description}</Text>}
</Box>
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
@@ -396,23 +401,6 @@ export function WalletStateScreen(): React.ReactElement {
);
}
if (row.type === 'utxo') {
const sats = row.utxo?.valueSatoshis ?? 0n;
const reservedTag = row.utxo?.reserved ? ' [Reserved]' : '';
return (
<Box flexDirection="row" justifyContent="space-between">
<Box flexDirection="row">
<Text color={itemColor}>
{indicator}{formatSatoshis(sats)}
{getFiatSuffix(sats)}
</Text>
{row.description && <Text color={colors.textMuted}> {row.description}{reservedTag}</Text>}
</Box>
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
</Box>
);
}
// Fallback for other types
return (
<Box flexDirection="row" justifyContent="space-between">
@@ -515,7 +503,7 @@ export function WalletStateScreen(): React.ReactElement {
height={14}
overflow="hidden"
>
<Text color={colors.primary} bold> Wallet History {history.length > 0 ? `(${selectedHistoryIndex + 1}/${history.length})` : ''}</Text>
<Text color={colors.primary} bold> Wallet History {historyListItems.length > 0 ? `(${selectedHistoryIndex + 1}/${historyListItems.length})` : ''}</Text>
{isLoading ? (
<Box marginTop={1}>
<Text color={colors.textMuted}>Loading...</Text>

View File

@@ -21,7 +21,7 @@ import { useSatoshisConversion } from '../../hooks/useSatoshisConversion.js';
import { colors, logoSmall, formatSatoshis } from '../../theme.js';
import { copyToClipboard } from '../../utils/clipboard.js';
import type { Invitation } from '../../../services/invitation.js';
import type { XOTemplate } from '@xo-cash/types';
import type { XOInvitationCommit, XOTemplate } from '@xo-cash/types';
import {
getInvitationState,
@@ -29,7 +29,6 @@ import {
getInvitationInputs,
getInvitationOutputs,
getInvitationVariables,
getUserRole,
formatInvitationListItem,
formatInvitationId,
} from '../../../utils/invitation-utils.js';
@@ -80,6 +79,29 @@ const invitationListGroups: ListGroup[] = [
{ id: 'invitations', separator: true },
];
type OwnInvitationContext = {
entityIdentifier: string | null;
roleIdentifier: string | null;
};
function getRoleIdentifierFromCommits(commits: XOInvitationCommit[]): string | null {
for (const commit of commits) {
for (const input of commit.data.inputs ?? []) {
if (input.roleIdentifier) return input.roleIdentifier;
}
for (const output of commit.data.outputs ?? []) {
if (output.roleIdentifier) return output.roleIdentifier;
}
for (const variable of commit.data.variables ?? []) {
if (variable.roleIdentifier) return variable.roleIdentifier;
}
}
return null;
}
/**
* Invitation Screen Component.
*/
@@ -107,6 +129,10 @@ export function InvitationScreen(): React.ReactElement {
// ── Template cache ───────────────────────────────────────────────────────
const [templateCache, setTemplateCache] = useState<Map<string, XOTemplate>>(new Map());
const [selectedTemplate, setSelectedTemplate] = useState<XOTemplate | null>(null);
const [ownInvitationContext, setOwnInvitationContext] = useState<OwnInvitationContext>({
entityIdentifier: null,
roleIdentifier: null,
});
// Check if we should open import dialog on mount
const initialMode = navData.mode as string | undefined;
@@ -180,6 +206,43 @@ export function InvitationScreen(): React.ReactElement {
.then(template => setSelectedTemplate(template ?? null));
}, [selectedInvitation, appService]);
/**
* Load the current engine entity's commits for the selected invitation.
*/
useEffect(() => {
if (!selectedInvitation || !appService) {
setOwnInvitationContext({
entityIdentifier: null,
roleIdentifier: null,
});
return;
}
let isCurrent = true;
appService.engine.getOwnCommits(selectedInvitation.data)
.then((ownCommits) => {
if (!isCurrent) return;
setOwnInvitationContext({
entityIdentifier: ownCommits[0]?.entityIdentifier ?? null,
roleIdentifier: getRoleIdentifierFromCommits(ownCommits),
});
})
.catch(() => {
if (!isCurrent) return;
setOwnInvitationContext({
entityIdentifier: null,
roleIdentifier: null,
});
});
return () => {
isCurrent = false;
};
}, [selectedInvitation, appService]);
// ── Import flow callbacks ──────────────────────────────────────────────
/**
@@ -512,9 +575,8 @@ export function InvitationScreen(): React.ReactElement {
const inputs = getInvitationInputs(selectedInvitation);
const outputs = getInvitationOutputs(selectedInvitation);
const variables = getInvitationVariables(selectedInvitation);
const userEntityId = selectedInvitation.data.commits?.[0]?.entityIdentifier ?? null;
const userRole = getUserRole(selectedInvitation, userEntityId);
const userEntityId = ownInvitationContext.entityIdentifier;
const userRole = ownInvitationContext.roleIdentifier;
const roleInfoRaw = userRole && selectedTemplate?.roles?.[userRole];
const roleInfo = roleInfoRaw && typeof roleInfoRaw === 'object' ? roleInfoRaw : null;