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
2 changes: 1 addition & 1 deletion __tests__/lib/csvExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('toCsv', () => {
});

describe('downloadCsv', () => {
const createObjectURL = jest.fn(() => 'blob:mock-url');
const createObjectURL = jest.fn((_blob: Blob) => 'blob:mock-url');
const revokeObjectURL = jest.fn();
let clickSpy: jest.SpyInstance;

Expand Down
1 change: 1 addition & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ValueProps } from '@/components/landing/ValueProps';
import { CallToAction } from '@/components/landing/CallToAction';
import { TrustBar } from '@/components/landing/TrustBar';
import { PricingCards } from '@/components/pricing/PricingCards';
import { KineticExplorer } from '@/components/landing/KineticExplorer';

export default function Home() {
return (
Expand Down
11 changes: 7 additions & 4 deletions components/escrow/__tests__/PaymentLock.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { PaymentLock } from '@/components/escrow/PaymentLock';
import { useCurrencyConversion } from '@/hooks/useCurrencyConversion';
import {
useCurrencyConversion,
type CurrencyConversionResult,
} from '@/hooks/useCurrencyConversion';

// ---------------------------------------------------------------------------
// Mock the hook layer
Expand All @@ -20,16 +23,16 @@ const mockUseCurrencyConversion = useCurrencyConversion as jest.MockedFunction<
typeof useCurrencyConversion
>;

const DEFAULT_HOOK_STATE = {
const DEFAULT_HOOK_STATE: CurrencyConversionResult = {
ngnAmount: '',
ngnRaw: null,
rate: null,
rateUpdatedAt: null,
isLoading: false,
isError: false,
} as const;
};

const mockConversion = (overrides: Partial<typeof DEFAULT_HOOK_STATE> = {}) => {
const mockConversion = (overrides: Partial<CurrencyConversionResult> = {}) => {
mockUseCurrencyConversion.mockReturnValue({
...DEFAULT_HOOK_STATE,
...overrides,
Expand Down
35 changes: 17 additions & 18 deletions components/mobile/MobileFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
`use client
'use client'

import React from react
import Link from next/link
import React from 'react'
import Link from 'next/link'

// Navigation links data
const footerLinks = [
{ label: Home, href: / },
{ label: About, href: /about },
{ label: Services, href: /services },
{ label: Support, href: /support },
{ label: Privacy Policy, href: /privacy },
{ label: Terms of Service, href: /terms },
{ label: Contact, href: /contact },
{ label: FAQs, href: /faq },
{ label: 'Home', href: '/' },
{ label: 'About', href: '/about' },
{ label: 'Services', href: '/services' },
{ label: 'Support', href: '/support' },
{ label: 'Privacy Policy', href: '/privacy' },
{ label: 'Terms of Service', href: '/terms' },
{ label: 'Contact', href: '/contact' },
{ label: 'FAQs', href: '/faq' },
]

// Social icons data
const socialLinks = [
{ label: Twitter, href: https://twitter.com/swiftchain, icon: 🐦 },
{ label: GitHub, href: https://github.com/swiftchain, icon: 🐙 },
{ label: LinkedIn, href: https://linkedin.com/company/swiftchain, icon: 🔗 },
{ label: Discord, href: https://discord.gg/swiftchain, icon: 💬 },
{ label: 'Twitter', href: 'https://twitter.com/swiftchain', icon: '🐦' },
{ label: 'GitHub', href: 'https://github.com/swiftchain', icon: '🐙' },
{ label: 'LinkedIn', href: 'https://linkedin.com/company/swiftchain', icon: '🔗' },
{ label: 'Discord', href: 'https://discord.gg/swiftchain', icon: '💬' },
]

export function MobileFooter() {
Expand All @@ -36,7 +36,7 @@ export function MobileFooter() {
key={link.href}
href={link.href}
className="rounded-lg px-3 py-2.5 text-sm text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 active:bg-gray-200 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100 dark:active:bg-gray-700"
style={{ minHeight: 44px }}
style={{ minHeight: '44px' }}
>
{link.label}
</Link>
Expand All @@ -53,7 +53,7 @@ export function MobileFooter() {
rel="noopener noreferrer"
className="flex h-11 w-11 items-center justify-center rounded-full text-xl transition-colors hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-100"
aria-label={social.label}
style={{ minHeight: 44px, minWidth: 44px }}
style={{ minHeight: '44px', minWidth: '44px' }}
>
<span className="sr-only">{social.label}</span>
<span role="img" aria-hidden="true">
Expand All @@ -73,4 +73,3 @@ export function MobileFooter() {
}

export default MobileFooter

43 changes: 26 additions & 17 deletions hooks/useDriverReputation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,26 @@ export interface UseDriverReputationResult {
* state on an unmounted component.
*/
export function useDriverReputation(driverId: string): UseDriverReputationResult {
const [onChainScore, setOnChainScore] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Store the settled result together with the driverId it belongs to, so
// loading is derived from a stale/absent result rather than written back
// into state from the effect.
const [result, setResult] = useState<{
driverId: string;
onChainScore: number | null;
error: string | null;
} | null>(null);

useEffect(() => {
if (!driverId) {
setOnChainScore(null);
setIsLoading(false);
return;
}
if (!driverId) return;

const controller = new AbortController();
let cancelled = false;

setIsLoading(true);

reputationService
.getDriverReputation(driverId, controller.signal)
.then((data) => {
if (cancelled) return;
setOnChainScore(data.onChainScore);
setError(null);
setResult({ driverId, onChainScore: data.onChainScore, error: null });
})
.catch((err: unknown) => {
if (cancelled) return;
Expand All @@ -48,10 +46,7 @@ export function useDriverReputation(driverId: string): UseDriverReputationResult
err instanceof Error && err.message
? err.message
: 'Failed to load on-chain reputation';
setError(message);
})
.finally(() => {
if (!cancelled) setIsLoading(false);
setResult({ driverId, onChainScore: null, error: message });
});

return () => {
Expand All @@ -60,5 +55,19 @@ export function useDriverReputation(driverId: string): UseDriverReputationResult
};
}, [driverId]);

return { onChainScore, isLoading, error };
// With no driverId there is nothing to fetch.
if (!driverId) {
return { onChainScore: null, isLoading: false, error: null };
}

// No result yet for this driverId means the request is still in flight.
if (!result || result.driverId !== driverId) {
return { onChainScore: null, isLoading: true, error: null };
}

return {
onChainScore: result.onChainScore,
isLoading: false,
error: result.error,
};
}
25 changes: 14 additions & 11 deletions hooks/useTheme.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
// hooks/useTheme.ts

import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { ThemeService, Theme } from '@/services/themeService';

export const useTheme = (userId?: string) => {
const [theme, setThemeState] = useState<Theme>('system');

const updateTheme = useCallback(
(newTheme: Theme) => {
setThemeState(newTheme);
ThemeService.setThemeToStorage(newTheme);

if (userId) {
ThemeService.syncThemeToAPI(userId, newTheme).catch(console.error);
}
},
[userId]
);

useEffect(() => {
const initializeTheme = async () => {
// 1. Sync from local storage immediately to match the blocking script
Expand All @@ -27,7 +39,7 @@ export const useTheme = (userId?: string) => {
}
};
initializeTheme();
}, [userId]);
}, [userId, updateTheme]);

// Apply theme to DOM when state changes
useEffect(() => {
Expand Down Expand Up @@ -60,14 +72,5 @@ export const useTheme = (userId?: string) => {
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);

const updateTheme = (newTheme: Theme) => {
setThemeState(newTheme);
ThemeService.setThemeToStorage(newTheme);

if (userId) {
ThemeService.syncThemeToAPI(userId, newTheme).catch(console.error);
}
};

return { theme, setTheme: updateTheme };
};
10 changes: 5 additions & 5 deletions services/__tests__/feeService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ describe('feeService', () => {

await feeService.getEstimatedFee(200, 'EUR');

const callArgs = mockAxios.get.mock.calls[0];
const callArgs = mockAxios.get.mock.calls[0]!;
expect(callArgs[0]).toContain('/api/wallet/fees/estimate');
expect(callArgs[1].params.amount).toBe(200);
expect(callArgs[1].params.currency).toBe('EUR');
expect(callArgs[1]!.params.amount).toBe(200);
expect(callArgs[1]!.params.currency).toBe('EUR');
});

it('should handle different amounts', async () => {
Expand Down Expand Up @@ -146,8 +146,8 @@ describe('feeService', () => {

await feeService.getEstimatedFee(100);

const callArgs = mockAxios.get.mock.calls[0];
expect(callArgs[1].params.currency).toBe('USD');
const callArgs = mockAxios.get.mock.calls[0]!;
expect(callArgs[1]!.params.currency).toBe('USD');
});

it('should handle multiple currencies', async () => {
Expand Down
25 changes: 13 additions & 12 deletions services/__tests__/imageCompressionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ jest.mock('browser-image-compression');

const mockedCompression = imageCompression as jest.MockedFunction<typeof imageCompression>;

function makeBlob(sizeBytes: number, type = 'image/jpeg'): Blob {
return new Blob([new Uint8Array(sizeBytes)], { type });
// browser-image-compression resolves a File, so the mock must too.
function makeCompressed(sizeBytes: number, type = 'image/jpeg'): File {
return new File([new Uint8Array(sizeBytes)], 'compressed.jpg', { type });
}

function makeFile(sizeBytes: number, name = 'proof.jpg', type = 'image/jpeg'): File {
Expand All @@ -19,7 +20,7 @@ describe('imageCompressionService', () => {
});

it('returns a compressed File under the target size on the first attempt', async () => {
mockedCompression.mockResolvedValueOnce(makeBlob(400 * 1024));
mockedCompression.mockResolvedValueOnce(makeCompressed(400 * 1024));

const input = makeFile(2 * 1024 * 1024);
const result = await imageCompressionService.compressImage(input, 500);
Expand All @@ -30,7 +31,7 @@ describe('imageCompressionService', () => {
});

it('preserves the original file name and mime type', async () => {
mockedCompression.mockResolvedValueOnce(makeBlob(300 * 1024, 'image/jpeg'));
mockedCompression.mockResolvedValueOnce(makeCompressed(300 * 1024, 'image/jpeg'));

const input = makeFile(1024 * 1024, 'delivery-proof.png', 'image/png');
const result = await imageCompressionService.compressImage(input, 500);
Expand All @@ -40,12 +41,12 @@ describe('imageCompressionService', () => {

it('escalates through quality and dimension steps until under the target', async () => {
mockedCompression
.mockResolvedValueOnce(makeBlob(900 * 1024))
.mockResolvedValueOnce(makeBlob(800 * 1024))
.mockResolvedValueOnce(makeBlob(700 * 1024))
.mockResolvedValueOnce(makeBlob(600 * 1024))
.mockResolvedValueOnce(makeBlob(550 * 1024))
.mockResolvedValueOnce(makeBlob(480 * 1024));
.mockResolvedValueOnce(makeCompressed(900 * 1024))
.mockResolvedValueOnce(makeCompressed(800 * 1024))
.mockResolvedValueOnce(makeCompressed(700 * 1024))
.mockResolvedValueOnce(makeCompressed(600 * 1024))
.mockResolvedValueOnce(makeCompressed(550 * 1024))
.mockResolvedValueOnce(makeCompressed(480 * 1024));

const input = makeFile(6 * 1024 * 1024);
const result = await imageCompressionService.compressImage(input, 500);
Expand All @@ -55,7 +56,7 @@ describe('imageCompressionService', () => {
});

it('throws instead of silently returning a file that exceeds the target', async () => {
mockedCompression.mockResolvedValue(makeBlob(600 * 1024));
mockedCompression.mockResolvedValue(makeCompressed(600 * 1024));

const input = makeFile(10 * 1024 * 1024);

Expand All @@ -65,7 +66,7 @@ describe('imageCompressionService', () => {
});

it('passes progressively smaller maxWidthOrHeight values as it escalates', async () => {
mockedCompression.mockResolvedValue(makeBlob(900 * 1024));
mockedCompression.mockResolvedValue(makeCompressed(900 * 1024));

const input = makeFile(6 * 1024 * 1024);
await expect(imageCompressionService.compressImage(input, 500)).rejects.toThrow();
Expand Down
2 changes: 1 addition & 1 deletion services/__tests__/proofService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const mockedAxios = axios as jest.Mocked<typeof axios>;
// Mock browser-image-compression
jest.mock('browser-image-compression');
const mockedImageCompression =
imageCompression as jest.Mocked<typeof imageCompression>;
imageCompression as jest.MockedFunction<typeof imageCompression>;

describe('proofService', () => {
beforeEach(() => {
Expand Down
9 changes: 6 additions & 3 deletions services/__tests__/trackingService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import {
} from '@/services/trackingService';
import { RouteData } from '@/types/tracking';

const mockedAxios = axios as jest.Mocked<typeof axios> & { isAxiosError?: (e: any) => boolean };
const mockedAxios = axios as jest.Mocked<typeof axios>;

// Provide a simple isAxiosError implementation on the mocked axios
mockedAxios.isAxiosError = (error: any) => error && error.isAxiosError === true;
// Provide a simple isAxiosError implementation on the mocked axios. The real
// export is a type guard, so the stub is cast back to its declared signature.
(mockedAxios.isAxiosError as unknown as jest.Mock).mockImplementation(
(error: any) => error && error.isAxiosError === true,
);

describe('trackingService', () => {
const mockDeliveryId = 'delivery-123';
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
"node_modules",
"cypress"
]
}
2 changes: 2 additions & 0 deletions types/delivery.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { DeliveryStatus } from './filters';

export interface DriverInfo {
id: string;
name: string;
Expand Down
Loading