When sync is fully automatic with no manual escape hatch, users who suspect stale data — a merchant checking a just-placed order, a field technician pulling the latest work ticket — have no way to force a refresh and must kill the app or wait for an opaque background timer. This produces 'the app is broken' perception even when the sync engine is healthy, and it blocks power users from recovering from transient failures. The user-experience taxon depends on giving users visible control over system state.
Low because automatic sync still works; the gap is user confidence and recovery, not data loss or security.
Add a sync control to the primary screen — either a pull-to-refresh gesture on the main list or an explicit 'Sync Now' button in the header — that calls your sync function, shows a spinner while running, and surfaces the last-sync timestamp. Wire it in src/components/SyncButton.tsx and keep it disabled while a sync is in flight to prevent concurrent runs.
<TouchableOpacity onPress={handleSync} disabled={isSyncing}>
{isSyncing ? <ActivityIndicator /> : <Text>Sync Now</Text>}
</TouchableOpacity>
{lastSyncTime && <Text>Last synced: {lastSyncTime}</Text>}
ID: mobile-offline-storage.offline-behavior.manual-sync-trigger
Severity: low
What to look for: Examine the UI for a manual sync button or refresh option. Look for code that allows users to explicitly trigger a sync when they suspect their local data is out of date or to upload pending changes immediately without waiting for automatic sync.
Pass criteria: Count all manual sync trigger points in the UI (buttons, pull-to-refresh, menu items). App provides at least 1 visible manual sync option that users can trigger. Manual sync must provide feedback (loading indicator, success/failure message).
Fail criteria: No manual sync option found. Users cannot explicitly trigger sync and must wait for automatic sync or app restart.
Skip (N/A) when: Never — manual sync control improves user experience and confidence.
Detail on fail: "No manual sync button found in UI. Users cannot proactively trigger sync or update their data on demand."
Remediation: Add a manual sync button with feedback:
import { useEffect, useState } from 'react';
import { View, TouchableOpacity, Text, ActivityIndicator } from 'react-native';
export function SyncButton() {
const [isSyncing, setIsSyncing] = useState(false);
const [lastSyncTime, setLastSyncTime] = useState<string | null>(null);
const handleSync = async () => {
setIsSyncing(true);
try {
await syncAllData();
setLastSyncTime(new Date().toLocaleTimeString());
console.log('Sync completed successfully');
} catch (error) {
console.error('Sync failed:', error);
// Show error toast
} finally {
setIsSyncing(false);
}
};
return (
<View>
<TouchableOpacity
onPress={handleSync}
disabled={isSyncing}
style={{
backgroundColor: isSyncing ? '#ccc' : '#007AFF',
padding: 12,
borderRadius: 8,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}}
>
{isSyncing ? (
<ActivityIndicator size="small" color="white" />
) : (
<Text style={{ color: 'white', fontWeight: '600' }}>Sync Now</Text>
)}
</TouchableOpacity>
{lastSyncTime && (
<Text style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
Last synced: {lastSyncTime}
</Text>
)}
</View>
);
}
Or implement pull-to-refresh:
import { ScrollView, RefreshControl } from 'react-native';
export function MyScreen() {
const [isRefreshing, setIsRefreshing] = useState(false);
const onRefresh = async () => {
setIsRefreshing(true);
try {
await syncAllData();
} finally {
setIsRefreshing(false);
}
};
return (
<ScrollView
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} />
}
>
{/* Content */}
</ScrollView>
);
}