Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Key resources for contributors:
- [Bulk Compliance Review](docs/bulk-compliance-review.md) — Bulk compliance review table with action confirmation modal
- [Compliance Status Panel](docs/compliance-status-panel.md) — Address-level compliance status for investor and admin views (Issue #175)
- [Transaction Review Modal](docs/transaction-review-modal.md) — Pre-signature review modal, operation summary mapper, and risk notes (Issue #177)
- [Admin Action Receipts](docs/admin-action-receipts.md) — Privileged action status, target, hash, explorer link, and next-step guidance (Issue #179)
- [Environment Mismatch Blocking Screen](docs/environment-mismatch-blocking.md) — Full-page blocking screen when the wallet network does not match the dashboard target network
- [Investor Onboarding Eligibility](docs/investor-onboarding-eligibility.md) — Investor onboarding eligibility page, evaluation precedence, and SDK mapping
- [Performance Budget Review](docs/performance-budget-review.md) — Typed budget threshold evaluation, edge cases, and reviewer checklist
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Reference material for contributors implementing new functionality.
|---|---|
| [transaction-components.md](transaction-components.md) | Transfer modal, history table, and operation-type mapping |
| [transaction-review-modal.md](transaction-review-modal.md) | Pre-signature review modal, operation summary mapper, risk notes (Issue #177) |
| [admin-action-receipts.md](admin-action-receipts.md) | Admin operation receipts, explorer links, next actions, limitations (Issue #179) |
| [transaction-history.md](transaction-history.md) | Normalised transaction model, supported operation types, fixture coverage |
| [table-filtering.md](table-filtering.md) | Reusable table filtering, sorting, search, and saved-views pattern |
| [empty-state-components.md](empty-state-components.md) | Reusable `EmptyState` component — variants, props, and usage examples |
Expand Down
92 changes: 92 additions & 0 deletions docs/admin-action-receipts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Admin Action Receipts

Issue #179 adds a consistent receipt view for privileged dashboard actions.
After an action resolves, admins see its status, operation, target, transaction
hash, explorer link (when available), and a context-specific next action.

## Architecture

The feature lives in `src/features/admin/receipts/`:

| File | Purpose |
| --- | --- |
| `types.ts` | Typed admin operations and normalized receipt model |
| `mapAdminActionReceipt.ts` | Maps provider/local outcomes into receipt data |
| `AdminActionReceiptView.tsx` | Admin view composed from shared `TransactionReceipt` |
| `fixtures.ts` | Major operations and success/failure/pending/unknown states |

The mapper reuses:

- `mapToTransactionResult` for SDK/RPC status normalization
- `getExplorerUrl` for trusted Stellar Expert links
- `TransactionReceipt` for consistent status, hash, and detail rendering

## Represented operations

| Operation | Current integration | Chain evidence |
| --- | --- | --- |
| Whitelist add / revoke | `WhitelistActionModal` | Provider hash and explorer link when returned |
| Mint | `MintWorkflow` and legacy admin mint | Provider hash and explorer link when returned |
| Asset registration | `AssetCreationWizard` | Local issuance request only; no hash |
| Role change | Typed fixture / expected view | No dashboard SDK action is wired yet |
| Bulk compliance update | `ComplianceUpdateModal` | Local success today; explorer link only when a future provider returns a hash |

`AdminActionOperation` intentionally distinguishes whitelist add from revoke even
though both use the shared `whitelist` transaction presentation label.

## Receipt states

The view supports the shared transaction statuses:

- `success` — action confirmed or local request accepted
- `failure` — provider rejected the action or returned an error
- `pending` — submitted but not confirmed
- `unknown` — the outcome cannot be confirmed

Pending and unknown receipts tell the admin to verify network state before
retrying. This reduces duplicate privileged actions when the original submission
may still complete.

## Explorer links

Explorer links are shown only when both are true:

1. The provider returned a transaction hash.
2. The wallet network maps to a supported Stellar Expert network (`TESTNET` or
public/mainnet aliases).

A hash on an unsupported network remains visible, but the link is omitted and a
limitation note explains why. Missing hashes are never fabricated.

## Next actions

Each operation maps to a useful follow-up:

- Whitelist add/revoke → **Back to whitelist**
- Mint → **Mint another**
- Asset registration → **Create another**
- Role change → **Review role assignments**

Failure uses **Review action**. Pending/unknown uses **Check transaction status**.
The caller owns navigation/reset behavior; the mapper owns labels and guidance.

## Limitations

- Asset creation currently creates a local `IssuanceRequest` and does not submit
an on-chain registration transaction. Its receipt therefore has no hash or
explorer link.
- Role-change provider/UI support does not exist yet. Fixtures document the
expected receipt contract so future SDK integration does not require a new
view.
- Mock provider hashes are synthetic and intended only for dashboard testing.
- An absent hash does not prove an action failed. Admins should check the list or
transaction history before retrying.

## Tests and fixtures

- `mapAdminActionReceipt.test.ts` covers status mapping, explorer behavior,
limitations, major operations, and receipt states.
- `AdminActionReceiptView.test.tsx` covers status/operation/target/hash display,
explorer links, next actions, and local-action limitations.
- Existing whitelist, mint, admin, and asset-creation flow tests exercise the
integrated view.
6 changes: 5 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ The UI is strictly separated into pages and domain-specific features:
- `src/components/transactions/` provides the shared review-before-sign UI:
- `TransactionReview` / `TransactionReviewModal`
- `operationSummary` mapper for transfer, mint, whitelist, and compliance updates
- progress / receipt / status mapping used by all sensitive signing flows
- progress / receipt / status mapping used by all sensitive signing flows
- `src/features/admin/receipts/` encapsulates:
- admin operation receipt types for whitelist, mint, asset registration, and role changes
- SDK/local-outcome mapping onto shared transaction status and explorer helpers
- next-action guidance and fixtures for all receipt states
8 changes: 8 additions & 0 deletions docs/transaction-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ const outcome = await transfer(recipient, amount, setState);
details={details} // same object used for the review
onClose={reset}
explorerUrl={getExplorerUrl(result.txHash, network)}
nextAction={{ // optional — used by admin receipts
label: 'Mint another',
description: 'Review the confirmed amount before starting another mint.',
onClick: reset,
}}
/>
```

Expand All @@ -108,6 +113,9 @@ Handles all four outcomes with its own icon, colour and badge:

The transaction hash row and the explorer link are only rendered when available,
so a failure that never reached the network shows neither.
`nextAction` and `limitation` are optional. The admin receipt feature uses them
for operation-specific follow-up guidance and to explain missing chain evidence.
See [admin-action-receipts.md](admin-action-receipts.md).

## Status mapping

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ describe('TransactionFixtureGallery', () => {
// expect(screen.getByText(/submitting to the network/i)).toBeInTheDocument();
expect(screen.getAllByText(/submitting to the network/i),).toHaveLength(2);
expect(screen.getByText(/transaction confirmed/i)).toBeInTheDocument();
expect(screen.getByText(/pending receipt/i)).toBeInTheDocument();
expect(screen.getByText(/transaction submitted/i)).toBeInTheDocument();
expect(screen.getByText(/transaction failed/i)).toBeInTheDocument();
expect(screen.getByText(/transaction status unknown/i)).toBeInTheDocument();
});
Expand Down
41 changes: 40 additions & 1 deletion src/components/transactions/TransactionReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import {
type TransactionStatus,
} from './types';

export interface TransactionReceiptAction {
label: string;
onClick: () => void;
description?: string;
}

interface TransactionReceiptProps {
result: TransactionResult;
details: TransactionDetails;
Expand All @@ -23,6 +29,10 @@ interface TransactionReceiptProps {
* `getExplorerUrl` — `null` simply hides the link.
*/
explorerUrl?: string | null;
/** Optional operation-specific action shown above the generic close button. */
nextAction?: TransactionReceiptAction;
/** Explains why a hash or explorer link may not be available. */
limitation?: string;
}

const STATUS_STYLES: Record<
Expand Down Expand Up @@ -65,6 +75,8 @@ export default function TransactionReceipt({
details,
onClose,
explorerUrl,
nextAction,
limitation,
}: TransactionReceiptProps) {
const { Icon, iconClass, badgeClass, label } = STATUS_STYLES[result.status];

Expand Down Expand Up @@ -122,10 +134,37 @@ export default function TransactionReceipt({
</a>
)}

{limitation && (
<p className="rounded-lg bg-slate-50 p-3 text-xs text-slate-600">
{limitation}
</p>
)}

{nextAction && (
<div className="space-y-2">
{nextAction.description && (
<p className="text-center text-xs text-slate-500">
{nextAction.description}
</p>
)}
<button
type="button"
onClick={nextAction.onClick}
className="w-full rounded bg-aegis-brand py-2 font-medium text-white transition hover:bg-blue-600"
>
{nextAction.label}
</button>
</div>
)}

<button
type="button"
onClick={onClose}
className="w-full rounded bg-aegis-dark py-2 font-medium text-white transition hover:bg-slate-800"
className={`w-full rounded py-2 font-medium transition ${
nextAction
? 'bg-slate-100 text-slate-700 hover:bg-slate-200'
: 'bg-aegis-dark text-white hover:bg-slate-800'
}`}
>
Close
</button>
Expand Down
23 changes: 23 additions & 0 deletions src/components/transactions/explorerLink.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { getExplorerUrl } from './explorerLink';

describe('getExplorerUrl', () => {
it('builds stellar.expert links for supported networks', () => {
expect(getExplorerUrl('abc123', 'TESTNET')).toBe(
'https://stellar.expert/explorer/testnet/tx/abc123',
);
expect(getExplorerUrl('abc123', 'PUBLIC')).toBe(
'https://stellar.expert/explorer/public/tx/abc123',
);
expect(getExplorerUrl('abc123', 'MAINNET')).toBe(
'https://stellar.expert/explorer/public/tx/abc123',
);
});

it('returns null when the hash or network is missing/unsupported', () => {
expect(getExplorerUrl(undefined, 'TESTNET')).toBeNull();
expect(getExplorerUrl('abc123', null)).toBeNull();
expect(getExplorerUrl('abc123', 'FUTURENET')).toBeNull();
expect(getExplorerUrl(' ', 'TESTNET')).toBeNull();
});
});
9 changes: 9 additions & 0 deletions src/components/transactions/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ export const transactionFixtureGalleryEntries: TransactionFixtureGalleryEntry[]
result: successResultFixture,
explorerUrl: 'https://stellar.expert/explorer/testnet/tx/b9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e',
},
{
id: 'receipt-pending',
kind: 'receipt',
title: 'Pending receipt',
description: 'Preview the submitted-but-unconfirmed receipt state.',
details: transferDetailsFixture,
result: pendingResultFixture,
explorerUrl: 'https://stellar.expert/explorer/testnet/tx/c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3e4f50',
},
{
id: 'receipt-failure',
kind: 'receipt',
Expand Down
53 changes: 53 additions & 0 deletions src/components/transactions/statusMapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest';
import { mapToTransactionResult } from './statusMapper';

describe('mapToTransactionResult', () => {
it('maps successful RPC outcomes with a transaction hash', () => {
expect(
mapToTransactionResult({
status: 'SUCCESS',
hash: 'abc123',
}),
).toMatchObject({
status: 'success',
txHash: 'abc123',
message: 'Transaction confirmed',
});
});

it('maps pending and unknown statuses', () => {
expect(mapToTransactionResult({ status: 'PENDING', txHash: 'pending-hash' })).toMatchObject({
status: 'pending',
txHash: 'pending-hash',
message: 'Transaction submitted',
});

expect(mapToTransactionResult({ status: 'not_a_real_status', hash: 'x' })).toMatchObject({
status: 'unknown',
txHash: 'x',
message: 'Transaction status unknown',
});
});

it('treats thrown errors and error fields as failures', () => {
expect(mapToTransactionResult(new Error('Wallet rejected'))).toMatchObject({
status: 'failure',
detail: 'Wallet rejected',
});

expect(
mapToTransactionResult({
status: 'SUCCESS',
errorMessage: 'Recipient account is not authorised to hold this asset.',
}),
).toMatchObject({
status: 'failure',
detail: 'Recipient account is not authorised to hold this asset.',
});
});

it('maps bare status strings and nullish outcomes', () => {
expect(mapToTransactionResult('CONFIRMED')).toMatchObject({ status: 'success' });
expect(mapToTransactionResult(null)).toMatchObject({ status: 'unknown' });
});
});
6 changes: 5 additions & 1 deletion src/components/transactions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ export type TransactionAction =
| 'transfer'
| 'mint'
| 'whitelist'
| 'compliance-update';
| 'compliance-update'
| 'asset-registration'
| 'role-change';

/** Human-readable label for each action, shared by review and receipt. */
export const TRANSACTION_ACTION_LABELS: Record<TransactionAction, string> = {
transfer: 'Transfer',
mint: 'Mint',
whitelist: 'Whitelist',
'compliance-update': 'Compliance update',
'asset-registration': 'Asset registration',
'role-change': 'Role change',
};

/** A single label/value line in the review and receipt summaries. */
Expand Down
21 changes: 15 additions & 6 deletions src/features/admin/components/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { useWallet } from '@/hooks/useWallet';
import { useFeatureFlags } from '@/hooks/useFeatureFlags';
import TransactionReview from '@/components/transactions/TransactionReview';
import TransactionProgress from '@/components/transactions/TransactionProgress';
import TransactionReceipt from '@/components/transactions/TransactionReceipt';
import { mapToTransactionResult } from '@/components/transactions/statusMapper';
import { getExplorerUrl } from '@/components/transactions/explorerLink';
import { buildMintSummary } from '@/components/transactions/operationSummary';
import {
AdminActionReceiptView,
mapAdminActionReceipt,
} from '@/features/admin/receipts';
import { CheckCircle } from 'lucide-react';
import type {
TransactionResult,
Expand Down Expand Up @@ -61,12 +63,19 @@ function LegacyMintPanel() {
};

if (result) {
const receipt = mapAdminActionReceipt({
operation: 'mint',
target: cleanAddress,
outcome: result,
network,
metadata: { amount: MINT_AMOUNT.toLocaleString('en-US') },
});

return (
<TransactionReceipt
result={result}
details={details}
<AdminActionReceiptView
receipt={receipt}
onNextAction={reset}
onClose={reset}
explorerUrl={getExplorerUrl(result.txHash, network)}
/>
);
}
Expand Down
7 changes: 7 additions & 0 deletions src/features/admin/components/ComplianceUpdateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react';
import TransactionReceipt from '@/components/transactions/TransactionReceipt';
import TransactionReviewModal from '@/components/transactions/TransactionReviewModal';
import { buildComplianceUpdateSummary } from '@/components/transactions/operationSummary';
import { getExplorerUrl } from '@/components/transactions/explorerLink';
import { COMPLIANCE_DISCLAIMER } from '@/lib/complianceReview';
import type { ComplianceSubject, BulkAction } from '@/lib/complianceReview';
import type { TransactionResult } from '@/components/transactions/types';
Expand Down Expand Up @@ -78,6 +79,12 @@ export default function ComplianceUpdateModal({
result={result}
details={details}
onClose={onClose}
explorerUrl={getExplorerUrl(result.txHash, network)}
limitation={
result.txHash
? undefined
: 'Bulk compliance updates are applied locally in this dashboard build. No on-chain transaction hash or explorer link is available until a provider-backed write lands.'
}
/>
)}
</div>
Expand Down
Loading
Loading