+ );
+}
\ No newline at end of file
diff --git a/components/escrow/__tests__/PayoutUI.test.tsx b/components/escrow/__tests__/PayoutUI.test.tsx
new file mode 100644
index 0000000..9793fe3
--- /dev/null
+++ b/components/escrow/__tests__/PayoutUI.test.tsx
@@ -0,0 +1,89 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { PayoutUI } from '../PayoutUI';
+import { useEscrowPayout } from '@/hooks/useEscrowPayout';
+
+// Mock the useEscrowPayout hook
+jest.mock('@/hooks/useEscrowPayout');
+const mockedUseEscrowPayout = useEscrowPayout as jest.Mock;
+
+describe('PayoutUI', () => {
+ const mockEscrowId = 'escrow-123';
+ const mockReleaseFunds = jest.fn();
+
+ beforeEach(() => {
+ // Reset mock before each test
+ mockedUseEscrowPayout.mockClear();
+ mockReleaseFunds.mockClear();
+ });
+
+ it('should render loading state', () => {
+ mockedUseEscrowPayout.mockReturnValue({
+ isLoading: true,
+ error: null,
+ requiredSignatures: 0,
+ currentSignatures: 0,
+ canRelease: false,
+ releaseFunds: mockReleaseFunds,
+ });
+
+ render();
+ expect(screen.getByLabelText('Loading payout UI')).toBeInTheDocument();
+ });
+
+ it('should render error state', () => {
+ mockedUseEscrowPayout.mockReturnValue({
+ isLoading: false,
+ error: 'Failed to fetch escrow details',
+ requiredSignatures: 0,
+ currentSignatures: 0,
+ canRelease: false,
+ releaseFunds: mockReleaseFunds,
+ });
+
+ render();
+ expect(screen.getByRole('alert')).toHaveTextContent(
+ 'Error: Failed to fetch escrow details',
+ );
+ });
+
+ it('should disable the "Release Funds" button when signatures are below threshold (1 of 2)', () => {
+ mockedUseEscrowPayout.mockReturnValue({
+ isLoading: false,
+ error: null,
+ requiredSignatures: 2,
+ currentSignatures: 1,
+ canRelease: false,
+ releaseFunds: mockReleaseFunds,
+ });
+
+ render();
+
+ const releaseButton = screen.getByRole('button', { name: 'Release Funds' });
+ expect(releaseButton).toBeDisabled();
+ expect(screen.getByText('Signatures: 1 / 2')).toBeInTheDocument();
+
+ // Assert that clicking the disabled button throws no exceptions and does not call releaseFunds
+ fireEvent.click(releaseButton);
+ expect(mockReleaseFunds).not.toHaveBeenCalled();
+ });
+
+ it('should enable the "Release Funds" button when signatures meet the threshold (2 of 2)', () => {
+ mockedUseEscrowPayout.mockReturnValue({
+ isLoading: false,
+ error: null,
+ requiredSignatures: 2,
+ currentSignatures: 2,
+ canRelease: true,
+ releaseFunds: mockReleaseFunds,
+ });
+
+ render();
+
+ const releaseButton = screen.getByRole('button', { name: 'Release Funds' });
+ expect(releaseButton).not.toBeDisabled();
+ expect(screen.getByText('Signatures: 2 / 2')).toBeInTheDocument();
+
+ fireEvent.click(releaseButton);
+ expect(mockReleaseFunds).toHaveBeenCalledTimes(1);
+ });
+});
\ No newline at end of file
diff --git a/components/escrow/__tests__/SignatureProgressBar.test.tsx b/components/escrow/__tests__/SignatureProgressBar.test.tsx
new file mode 100644
index 0000000..ea9b28e
--- /dev/null
+++ b/components/escrow/__tests__/SignatureProgressBar.test.tsx
@@ -0,0 +1,50 @@
+import { render, screen } from '@testing-library/react';
+import { SignatureProgressBar } from '../SignatureProgressBar';
+
+describe('SignatureProgressBar', () => {
+ it('should render a message when multi-signature is not required', () => {
+ render();
+ expect(
+ screen.getByText('Multi-signature not required for this escrow.'),
+ ).toBeInTheDocument();
+ });
+
+ it('should render the progress correctly for an incomplete state (1 of 3)', () => {
+ render();
+
+ const progressbar = screen.getByRole('progressbar');
+ expect(progressbar).toBeInTheDocument();
+ // Note: Jest stringifies the style, so we check for the substring.
+ expect(progressbar.style.width).toContain('33.33');
+ expect(progressbar).toHaveClass('bg-blue-600');
+ expect(progressbar).toHaveAttribute('aria-valuenow', '33.33333333333333');
+
+ expect(screen.getByText('1 of 3')).toBeInTheDocument();
+ expect(screen.getByText('1 of 3')).toHaveClass('text-gray-500');
+ });
+
+ it('should render the progress correctly for a complete state (3 of 3)', () => {
+ render();
+
+ const progressbar = screen.getByRole('progressbar');
+ expect(progressbar).toBeInTheDocument();
+ expect(progressbar).toHaveStyle('width: 100%');
+ expect(progressbar).toHaveClass('bg-green-500');
+ expect(progressbar).toHaveAttribute('aria-valuenow', '100');
+
+ expect(screen.getByText('3 of 3')).toBeInTheDocument();
+ expect(screen.getByText('3 of 3')).toHaveClass('text-green-600');
+ });
+
+ it('should cap the progress at 100% if current signatures exceed required', () => {
+ render();
+
+ const progressbar = screen.getByRole('progressbar');
+ expect(progressbar).toHaveStyle('width: 100%');
+ expect(progressbar).toHaveClass('bg-green-500');
+ expect(progressbar).toHaveAttribute('aria-valuenow', '100');
+
+ expect(screen.getByText('5 of 4')).toBeInTheDocument();
+ expect(screen.getByText('5 of 4')).toHaveClass('text-green-600');
+ });
+});
\ No newline at end of file
diff --git a/components/escrow/__tests__/SignerList.test.tsx b/components/escrow/__tests__/SignerList.test.tsx
new file mode 100644
index 0000000..3c5c32b
--- /dev/null
+++ b/components/escrow/__tests__/SignerList.test.tsx
@@ -0,0 +1,78 @@
+import { render, screen, fireEvent, act } from '@testing-library/react';
+import { SignerList } from '../SignerList';
+
+// Mock the clipboard API
+Object.assign(navigator, {
+ clipboard: {
+ writeText: jest.fn(),
+ },
+});
+
+const mockSigners = [
+ 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF',
+ 'GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB5432',
+];
+
+describe('SignerList', () => {
+ beforeEach(() => {
+ (navigator.clipboard.writeText as jest.Mock).mockClear();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('should render a message when no signers are present but signatures are required', () => {
+ render();
+ expect(
+ screen.getByText('No signatures have been recorded yet.'),
+ ).toBeInTheDocument();
+ });
+
+ it('should render nothing when no signers are present and none are required', () => {
+ const { container } = render(
+ ,
+ );
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render a list of signers with truncated keys', () => {
+ render();
+
+ expect(screen.getByText('GAAAAAAA...AAAAWHF')).toBeInTheDocument();
+ expect(screen.getByText('GBBBBBBB...BBBB5432')).toBeInTheDocument();
+ expect(screen.getAllByRole('listitem').length).toBe(2);
+ });
+
+ it('should copy a key to the clipboard and show feedback', () => {
+ render();
+
+ const copyButtons = screen.getAllByTitle('Copy public key');
+ expect(copyButtons.length).toBe(2);
+
+ // Click the first copy button
+ fireEvent.click(copyButtons[0]);
+
+ // Check that clipboard.writeText was called with the full key
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(mockSigners[0]);
+ expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
+
+ // The icon should change to a checkmark, which has a distinct green color
+ const checkIcon = copyButtons[0].querySelector('.text-green-500');
+ expect(checkIcon).toBeInTheDocument();
+
+ // The other button should still have the default copy icon
+ const otherButtonCheckIcon = copyButtons[1].querySelector('.text-green-500');
+ expect(otherButtonCheckIcon).not.toBeInTheDocument();
+
+ // Fast-forward time to reset the copied state
+ act(() => {
+ jest.advanceTimersByTime(2000);
+ });
+
+ // The checkmark icon should be gone, reverting to the copy icon
+ const checkIconAfterTimeout = copyButtons[0].querySelector('.text-green-500');
+ expect(checkIconAfterTimeout).not.toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/components/fleet/DriverReputation.tsx b/components/fleet/DriverReputation.tsx
new file mode 100644
index 0000000..d6545da
--- /dev/null
+++ b/components/fleet/DriverReputation.tsx
@@ -0,0 +1,120 @@
+'use client';
+
+import { useState } from 'react';
+import {
+ StarIcon,
+ ShieldCheckIcon,
+ InformationCircleIcon,
+} from '@heroicons/react/24/solid';
+import { useDriverReputation } from '@/hooks/useDriverReputation';
+
+function ReputationModal({ onClose }: { onClose: () => void }) {
+ return (
+
+
e.stopPropagation()}
+ >
+
+
+ About Reputation Scores
+
+
+
+
+
+ Driver reputation is measured in two ways to provide a complete
+ picture of their reliability and experience.
+
+
+
Platform Rating (★)
+
+ This is the standard star rating provided by customers after a
+ successful delivery. It reflects overall satisfaction.
+
+
+
+
On-Chain Score (🛡️)
+
+ This is a tokenized score recorded directly on the Stellar
+ blockchain. Drivers earn these "Reputation Tokens" by successfully
+ completing escrow-secured deliveries.
+
+
+ An on-chain score is a verifiable, tamper-proof record of a
+ driver's history within the SwiftChain ecosystem, representing a
+ higher level of trust.
+