Fix history for the 100th time. Fix role resolution in the invitation screen
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -350,29 +350,32 @@ export function WalletStateScreen(): React.ReactElement {
|
|||||||
const indicator = isFocused ? '▸ ' : ' ';
|
const indicator = isFocused ? '▸ ' : ' ';
|
||||||
const groupingPrefix = row.isNested ? ' -> ' : '';
|
const groupingPrefix = row.isNested ? ' -> ' : '';
|
||||||
|
|
||||||
if (row.type === 'invitation') {
|
if (row.type === 'history_item') {
|
||||||
|
const sats = row.valueSatoshis ?? 0n;
|
||||||
|
const fiatSuffix = getFiatSuffix(sats);
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="row" justifyContent="space-between">
|
<Box flexDirection="row" justifyContent="space-between">
|
||||||
<Text color={itemColor}>
|
<Box flexDirection="row">
|
||||||
{indicator}[Invitation] {row.label}
|
<Text color={itemColor}>
|
||||||
</Text>
|
{indicator}{formatSatoshis(sats)}{fiatSuffix}
|
||||||
|
</Text>
|
||||||
|
<Text color={colors.textMuted}> {row.label}</Text>
|
||||||
|
</Box>
|
||||||
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
|
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.type === 'invitation_input') {
|
if (row.type === 'history_input') {
|
||||||
const inputSatoshis = row.utxo?.valueSatoshis;
|
const sats = row.valueSatoshis ?? 0n;
|
||||||
const inputFiatSuffix = inputSatoshis !== undefined
|
|
||||||
? getFiatSuffix(inputSatoshis)
|
|
||||||
: '';
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="row" justifyContent="space-between">
|
<Box flexDirection="row" justifyContent="space-between">
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={itemColor}>
|
<Text color={itemColor}>
|
||||||
{indicator}{groupingPrefix}[Input] {row.label}
|
{indicator}{groupingPrefix}[Input] {formatSatoshis(sats)}
|
||||||
{inputFiatSuffix}
|
{getFiatSuffix(sats)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text color={colors.textMuted}> {row.label}</Text>
|
||||||
{row.description && <Text color={colors.textMuted}> {row.description}</Text>}
|
{row.description && <Text color={colors.textMuted}> {row.description}</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
|
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
|
||||||
@@ -380,8 +383,9 @@ export function WalletStateScreen(): React.ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row.type === 'invitation_output') {
|
if (row.type === 'history_output') {
|
||||||
const sats = row.utxo?.valueSatoshis ?? 0n;
|
const sats = row.valueSatoshis ?? 0n;
|
||||||
|
const reservedTag = row.reserved ? ' [Reserved]' : '';
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="row" justifyContent="space-between">
|
<Box flexDirection="row" justifyContent="space-between">
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
@@ -389,6 +393,7 @@ export function WalletStateScreen(): React.ReactElement {
|
|||||||
{indicator}{groupingPrefix}[Output] {formatSatoshis(sats)}
|
{indicator}{groupingPrefix}[Output] {formatSatoshis(sats)}
|
||||||
{getFiatSuffix(sats)}
|
{getFiatSuffix(sats)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text color={colors.textMuted}> {row.label}{reservedTag}</Text>
|
||||||
{row.description && <Text color={colors.textMuted}> {row.description}</Text>}
|
{row.description && <Text color={colors.textMuted}> {row.description}</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
{dateStr && <Text color={colors.textMuted}>{dateStr}</Text>}
|
{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
|
// Fallback for other types
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="row" justifyContent="space-between">
|
<Box flexDirection="row" justifyContent="space-between">
|
||||||
@@ -515,7 +503,7 @@ export function WalletStateScreen(): React.ReactElement {
|
|||||||
height={14}
|
height={14}
|
||||||
overflow="hidden"
|
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 ? (
|
{isLoading ? (
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color={colors.textMuted}>Loading...</Text>
|
<Text color={colors.textMuted}>Loading...</Text>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { useSatoshisConversion } from '../../hooks/useSatoshisConversion.js';
|
|||||||
import { colors, logoSmall, formatSatoshis } from '../../theme.js';
|
import { colors, logoSmall, formatSatoshis } from '../../theme.js';
|
||||||
import { copyToClipboard } from '../../utils/clipboard.js';
|
import { copyToClipboard } from '../../utils/clipboard.js';
|
||||||
import type { Invitation } from '../../../services/invitation.js';
|
import type { Invitation } from '../../../services/invitation.js';
|
||||||
import type { XOTemplate } from '@xo-cash/types';
|
import type { XOInvitationCommit, XOTemplate } from '@xo-cash/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getInvitationState,
|
getInvitationState,
|
||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
getInvitationInputs,
|
getInvitationInputs,
|
||||||
getInvitationOutputs,
|
getInvitationOutputs,
|
||||||
getInvitationVariables,
|
getInvitationVariables,
|
||||||
getUserRole,
|
|
||||||
formatInvitationListItem,
|
formatInvitationListItem,
|
||||||
formatInvitationId,
|
formatInvitationId,
|
||||||
} from '../../../utils/invitation-utils.js';
|
} from '../../../utils/invitation-utils.js';
|
||||||
@@ -80,6 +79,29 @@ const invitationListGroups: ListGroup[] = [
|
|||||||
{ id: 'invitations', separator: true },
|
{ 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.
|
* Invitation Screen Component.
|
||||||
*/
|
*/
|
||||||
@@ -107,6 +129,10 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
// ── Template cache ───────────────────────────────────────────────────────
|
// ── Template cache ───────────────────────────────────────────────────────
|
||||||
const [templateCache, setTemplateCache] = useState<Map<string, XOTemplate>>(new Map());
|
const [templateCache, setTemplateCache] = useState<Map<string, XOTemplate>>(new Map());
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<XOTemplate | null>(null);
|
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
|
// Check if we should open import dialog on mount
|
||||||
const initialMode = navData.mode as string | undefined;
|
const initialMode = navData.mode as string | undefined;
|
||||||
@@ -180,6 +206,43 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
.then(template => setSelectedTemplate(template ?? null));
|
.then(template => setSelectedTemplate(template ?? null));
|
||||||
}, [selectedInvitation, appService]);
|
}, [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 ──────────────────────────────────────────────
|
// ── Import flow callbacks ──────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -512,9 +575,8 @@ export function InvitationScreen(): React.ReactElement {
|
|||||||
const inputs = getInvitationInputs(selectedInvitation);
|
const inputs = getInvitationInputs(selectedInvitation);
|
||||||
const outputs = getInvitationOutputs(selectedInvitation);
|
const outputs = getInvitationOutputs(selectedInvitation);
|
||||||
const variables = getInvitationVariables(selectedInvitation);
|
const variables = getInvitationVariables(selectedInvitation);
|
||||||
|
const userEntityId = ownInvitationContext.entityIdentifier;
|
||||||
const userEntityId = selectedInvitation.data.commits?.[0]?.entityIdentifier ?? null;
|
const userRole = ownInvitationContext.roleIdentifier;
|
||||||
const userRole = getUserRole(selectedInvitation, userEntityId);
|
|
||||||
const roleInfoRaw = userRole && selectedTemplate?.roles?.[userRole];
|
const roleInfoRaw = userRole && selectedTemplate?.roles?.[userRole];
|
||||||
const roleInfo = roleInfoRaw && typeof roleInfoRaw === 'object' ? roleInfoRaw : null;
|
const roleInfo = roleInfoRaw && typeof roleInfoRaw === 'object' ? roleInfoRaw : null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
HistoryItem,
|
HistoryItem,
|
||||||
HistoryInvitationItem,
|
WalletHistoryInput,
|
||||||
HistoryUtxoItem,
|
WalletHistoryItem,
|
||||||
|
WalletHistoryOutput,
|
||||||
} from "../services/history.js";
|
} from "../services/history.js";
|
||||||
|
|
||||||
export type HistoryColorName =
|
export type HistoryColorName =
|
||||||
@@ -13,10 +14,9 @@ export type HistoryColorName =
|
|||||||
| "text";
|
| "text";
|
||||||
|
|
||||||
export type HistoryRowType =
|
export type HistoryRowType =
|
||||||
| "invitation"
|
| "history_item"
|
||||||
| "invitation_input"
|
| "history_input"
|
||||||
| "invitation_output"
|
| "history_output";
|
||||||
| "utxo";
|
|
||||||
|
|
||||||
export interface HistoryDisplayRow {
|
export interface HistoryDisplayRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -25,8 +25,11 @@ export interface HistoryDisplayRow {
|
|||||||
description?: string;
|
description?: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
isNested: boolean;
|
isNested: boolean;
|
||||||
utxo?: HistoryUtxoItem;
|
valueSatoshis?: bigint;
|
||||||
invitation?: HistoryInvitationItem;
|
reserved?: boolean;
|
||||||
|
input?: WalletHistoryInput;
|
||||||
|
output?: WalletHistoryOutput;
|
||||||
|
item?: WalletHistoryItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatHistoryDate(timestamp?: number): string | undefined {
|
export function formatHistoryDate(timestamp?: number): string | undefined {
|
||||||
@@ -40,61 +43,68 @@ export function buildHistoryDisplayRows(
|
|||||||
const rows: HistoryDisplayRow[] = [];
|
const rows: HistoryDisplayRow[] = [];
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.kind === "invitation") {
|
const roles = item.roles.length > 0 ? item.roles.join(", ") : "unknown";
|
||||||
rows.push({
|
if (item.source === "utxo") {
|
||||||
id: item.id,
|
|
||||||
type: "invitation",
|
|
||||||
label: item.description,
|
|
||||||
timestamp: item.createdAtTimestamp,
|
|
||||||
isNested: false,
|
|
||||||
invitation: item,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const input of item.inputs) {
|
|
||||||
const satsPrefix =
|
|
||||||
input.valueSatoshis !== undefined
|
|
||||||
? `${input.valueSatoshis.toLocaleString()} sats `
|
|
||||||
: "";
|
|
||||||
rows.push({
|
|
||||||
id: `${item.id}-input-${input.id}`,
|
|
||||||
type: "invitation_input",
|
|
||||||
label: `${satsPrefix}${input.outpoint.txid}:${input.outpoint.index}`,
|
|
||||||
description: input.description,
|
|
||||||
isNested: true,
|
|
||||||
utxo: input,
|
|
||||||
invitation: item,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const output of item.outputs) {
|
for (const output of item.outputs) {
|
||||||
rows.push({
|
rows.push({
|
||||||
id: `${item.id}-output-${output.id}`,
|
id: `${item.id}-output-${output.id}`,
|
||||||
type: "invitation_output",
|
type: "history_output",
|
||||||
label:
|
label: output.outpoint
|
||||||
output.valueSatoshis !== undefined
|
? `${output.outpoint.txid}:${output.outpoint.index}`
|
||||||
? `${output.valueSatoshis.toLocaleString()} sats`
|
: output.outputIdentifier ?? "Output",
|
||||||
: "Output",
|
description: `${item.template} | ${roles} | ${output.description}`,
|
||||||
description: output.description,
|
timestamp: item.createdAtTimestamp,
|
||||||
isNested: true,
|
isNested: false,
|
||||||
utxo: output,
|
valueSatoshis: output.valueSatoshis,
|
||||||
invitation: item,
|
reserved: output.reserved,
|
||||||
|
output,
|
||||||
|
item,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
type: "utxo",
|
type: "history_item",
|
||||||
label:
|
label: `${item.template} | ${roles} | ${item.description}`,
|
||||||
item.valueSatoshis !== undefined
|
description: item.action,
|
||||||
? `${item.valueSatoshis.toLocaleString()} sats`
|
timestamp: item.createdAtTimestamp,
|
||||||
: "UTXO",
|
|
||||||
description: item.description,
|
|
||||||
isNested: false,
|
isNested: false,
|
||||||
utxo: item,
|
valueSatoshis: item.valueSatoshis,
|
||||||
|
item,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (item.source !== "invitation") continue;
|
||||||
|
|
||||||
|
for (const input of item.inputs) {
|
||||||
|
rows.push({
|
||||||
|
id: `${item.id}-input-${input.id}`,
|
||||||
|
type: "history_input",
|
||||||
|
label: `${input.outpoint.txid}:${input.outpoint.index}`,
|
||||||
|
description: input.description,
|
||||||
|
isNested: true,
|
||||||
|
valueSatoshis: input.valueSatoshis,
|
||||||
|
input,
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const output of item.outputs) {
|
||||||
|
rows.push({
|
||||||
|
id: `${item.id}-output-${output.id}`,
|
||||||
|
type: "history_output",
|
||||||
|
label: output.outpoint
|
||||||
|
? `${output.outpoint.txid}:${output.outpoint.index}`
|
||||||
|
: output.outputIdentifier ?? "Output",
|
||||||
|
description: output.description,
|
||||||
|
isNested: true,
|
||||||
|
valueSatoshis: output.valueSatoshis,
|
||||||
|
reserved: output.reserved,
|
||||||
|
output,
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
@@ -106,14 +116,14 @@ export function getHistoryItemColorName(
|
|||||||
): HistoryColorName {
|
): HistoryColorName {
|
||||||
if (isSelected) return "info";
|
if (isSelected) return "info";
|
||||||
switch (row.type) {
|
switch (row.type) {
|
||||||
case "invitation":
|
case "history_input":
|
||||||
return "text";
|
|
||||||
case "invitation_input":
|
|
||||||
return "error";
|
return "error";
|
||||||
case "invitation_output":
|
case "history_output":
|
||||||
return "success";
|
return row.reserved ? "warning" : "success";
|
||||||
case "utxo":
|
case "history_item":
|
||||||
return row.utxo?.reserved ? "warning" : "success";
|
if ((row.valueSatoshis ?? 0n) < 0n) return "error";
|
||||||
|
if ((row.valueSatoshis ?? 0n) > 0n) return "success";
|
||||||
|
return "text";
|
||||||
default:
|
default:
|
||||||
return "text";
|
return "text";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user