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: 0 additions & 1 deletion apps/mobile/__tests__/useNotifications.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { renderHook, act } from '@testing-library/react-native';
import { useNotifications } from '../hooks/useNotifications';
import { NotificationService } from '../services/NotificationService';

describe('useNotifications', () => {
it('loads and counts unread notifications', async () => {
Expand Down
1 change: 0 additions & 1 deletion apps/mobile/app/(tabs)/notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/
import React, { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
FlatList,
RefreshControl,
StyleSheet,
Expand Down
9 changes: 1 addition & 8 deletions apps/mobile/app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* the biometric lock shipped in #333 could never be turned on.
*/
import React, { useState } from 'react';
import { Alert, Clipboard, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { Alert, ScrollView, StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { useRouter } from 'expo-router';
import * as LocalAuthentication from 'expo-local-authentication';
import { useBiometricLock } from '../../hooks/useBiometricLock';
Expand Down Expand Up @@ -77,13 +77,6 @@ export default function SettingsScreen() {
}
};

const handleCopySeed = () => {
if (seedValue) {
Clipboard.setString(seedValue);
Alert.alert('Copied', 'Seed copied to clipboard. Remember to clear it after backup.');
}
};

const handleImportWallet = () => {
if (!importSeed.trim()) {
Alert.alert('Error', 'Please enter a secret seed.');
Expand Down
1 change: 0 additions & 1 deletion apps/mobile/app/escrow/release.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
StyleSheet,
Text,
TouchableOpacity,
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/components/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Uses expo-clipboard for cross-platform clipboard access.
*/
import React, { useCallback, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { StyleSheet, Text, TouchableOpacity } from 'react-native';
import * as Clipboard from 'expo-clipboard';
import { showToast } from './Toast';

Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/components/RaiseDisputeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,12 @@ export const RaiseDisputeModal: React.FC<RaiseDisputeModalProps> = ({ visible, o
: f,
),
);
} catch (error: any) {
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Upload failed';
setUploadedFiles(prev =>
prev.map(f =>
f.id === file.id
? { ...f, progress: 0, error: error.message || 'Upload failed', isRetrying: false }
? { ...f, progress: 0, error: message, isRetrying: false }
: f,
),
);
Expand Down
12 changes: 6 additions & 6 deletions apps/mobile/hooks/useDashboardCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ export function useDashboardCache(
if (cached) {
setData(cached.data);
setUpdatedAt(cached.updatedAt);

const age =
Date.now() - cached.updatedAt;

setStale(age > 1000 * 60 * 30);
setStale(cached.stale);
} else {
setData(null);
setUpdatedAt(undefined);
setStale(false);
}

setLoading(false);
Expand All @@ -75,4 +75,4 @@ export function useDashboardCache(
stale,
refresh: load,
};
}
}
12 changes: 6 additions & 6 deletions apps/mobile/hooks/useEscrowDetailCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ export function useEscrowDetailCache(
if (cached) {
setData(cached.data);
setUpdatedAt(cached.updatedAt);

const age =
Date.now() - cached.updatedAt;

setStale(age > 1000 * 60 * 30);
setStale(cached.stale);
} else {
setData(null);
setUpdatedAt(undefined);
setStale(false);
}

return;
Expand Down Expand Up @@ -76,4 +76,4 @@ export function useEscrowDetailCache(
stale,
refresh: load,
};
}
}
12 changes: 0 additions & 12 deletions apps/mobile/hooks/useNetworkStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,6 @@
*/
import { useState, useEffect, useCallback } from 'react';

interface NetInfoState {
isConnected: boolean | null;
isInternetReachable: boolean | null;
type: string;
}

const DEFAULT_STATE: NetInfoState = {
isConnected: null,
isInternetReachable: null,
type: 'unknown',
};

let NetInfoModule: typeof import('@react-native-community/netinfo') | null = null;

try {
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/services/NotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class NotificationService {
];
}

static async markAsRead(id: string): Promise<boolean> {
static async markAsRead(_id: string): Promise<boolean> {
// API call stub
return true;
}
Expand Down
11 changes: 5 additions & 6 deletions apps/mobile/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ReleaseMilestonePayload,
} from '../types/escrow';
import { withRetry } from '../utils/retry';
import { Notification, NotificationsResponse } from '../types/notification';
import { NotificationsResponse } from '../types/notification';
import { getAccessToken, getSecureAccessToken } from './session';

const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL ?? 'http://localhost:3000';
Expand Down Expand Up @@ -186,11 +186,10 @@ export const disputeApi = {
mimeType: string,
): Promise<{ cid: string; url: string }> => {
const formData = new FormData();
formData.append('file', {
uri: fileUri,
name: fileName,
type: mimeType,
} as any);
/* React Native's FormData accepts { uri, name, type } but TS types don't reflect it */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fileBlob = { uri: fileUri, name: fileName, type: mimeType } as any;
formData.append('file', fileBlob);

const { data } = await api.post<{ cid: string; url: string }>(
`/api/escrows/${escrowId}/evidence`,
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isSessionHydrated,
subscribeToSession,
} from './session';
import { clearEscrowCache } from '../services/cache/escrowCache';
import { clearAllCache } from './cache/cacheKeys';

type RedirectTarget = {
Expand Down Expand Up @@ -75,6 +76,7 @@ export function exitGuestMode(): void {
export async function signOut(): Promise<void> {
guestMode = false;
await clearSession();
await clearEscrowCache();
notify();
}

Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/services/cache/cacheConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const SCHEMA_VERSION = 1;

export const CACHE_TTL_MS = 30 * 60 * 1000;

export const MAX_ESCROW_CACHE_ENTRIES = 50;
48 changes: 44 additions & 4 deletions apps/mobile/services/cache/dashboardCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@ import AsyncStorage from
"@react-native-async-storage/async-storage";

import { CACHE_KEYS } from "./cacheKeys";
import { CACHE_TTL_MS, SCHEMA_VERSION } from "./cacheConfig";

export interface DashboardCacheEntry {
version: number;
updatedAt: number;
expiresAt: number;
data: unknown;
stale: boolean;
}

export async function cacheDashboardData(
data: unknown
) {
const payload = {
const entry: DashboardCacheEntry = {
version: SCHEMA_VERSION,
updatedAt: Date.now(),
expiresAt: Date.now() + CACHE_TTL_MS,
data,
stale: false,
};

await AsyncStorage.setItem(
CACHE_KEYS.DASHBOARD,
JSON.stringify(payload)
JSON.stringify(entry)
);
}

Expand All @@ -24,5 +36,33 @@ export async function getCachedDashboardData() {

if (!raw) return null;

return JSON.parse(raw);
}
try {
const entry = JSON.parse(raw) as DashboardCacheEntry;

if (entry.version !== SCHEMA_VERSION) {
await AsyncStorage.removeItem(
CACHE_KEYS.DASHBOARD
);
return null;
}

const now = Date.now();
const stale = now > entry.expiresAt;

return {
...entry,
stale,
};
} catch {
await AsyncStorage.removeItem(
CACHE_KEYS.DASHBOARD
);
return null;
}
}

export async function clearDashboardCache() {
await AsyncStorage.removeItem(
CACHE_KEYS.DASHBOARD
);
}
96 changes: 89 additions & 7 deletions apps/mobile/services/cache/escrowCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,112 @@ import AsyncStorage from
"@react-native-async-storage/async-storage";

import { CACHE_KEYS } from "./cacheKeys";
import {
CACHE_TTL_MS,
MAX_ESCROW_CACHE_ENTRIES,
SCHEMA_VERSION,
} from "./cacheConfig";

export interface EscrowCacheEntry {
version: number;
updatedAt: number;
expiresAt: number;
data: unknown;
stale: boolean;
}

const LRU_INDEX_KEY = "escrow_lru_index";

async function updateLruIndex(key: string) {
const raw = await AsyncStorage.getItem(
LRU_INDEX_KEY
);

let index: string[] = raw
? JSON.parse(raw)
: [];

index = index.filter((k) => k !== key);
index.unshift(key);

const excess = index.splice(
MAX_ESCROW_CACHE_ENTRIES
);

if (excess.length > 0) {
await AsyncStorage.removeMany(excess);
}

await AsyncStorage.setItem(
LRU_INDEX_KEY,
JSON.stringify(index)
);
}

export async function cacheEscrowDetail(
id: string,
data: unknown
) {
const payload = {
const key = CACHE_KEYS.escrowDetail(id);

const entry: EscrowCacheEntry = {
version: SCHEMA_VERSION,
updatedAt: Date.now(),
expiresAt: Date.now() + CACHE_TTL_MS,
data,
stale: false,
};

await AsyncStorage.setItem(
CACHE_KEYS.escrowDetail(id),
JSON.stringify(payload)
key,
JSON.stringify(entry)
);

await updateLruIndex(key);
}

export async function getCachedEscrowDetail(
id: string
) {
const key = CACHE_KEYS.escrowDetail(id);

const raw = await AsyncStorage.getItem(key);

if (!raw) return null;

try {
const entry = JSON.parse(raw) as EscrowCacheEntry;

if (entry.version !== SCHEMA_VERSION) {
await AsyncStorage.removeItem(key);
return null;
}

const now = Date.now();
const stale = now > entry.expiresAt;

await updateLruIndex(key);

return {
...entry,
stale,
};
} catch {
await AsyncStorage.removeItem(key);
return null;
}
}

export async function clearEscrowCache() {
const raw = await AsyncStorage.getItem(
CACHE_KEYS.escrowDetail(id)
LRU_INDEX_KEY
);

if (!raw) return null;
const index: string[] = raw
? JSON.parse(raw)
: [];

index.push(LRU_INDEX_KEY);

return JSON.parse(raw);
}
await AsyncStorage.removeMany(index);
}
Loading