// app/(tabs)/sync.tsx — Sync Tab import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, TextInput, Alert, Modal, SafeAreaView, ActivityIndicator, } from 'react-native'; import { router } from 'expo-router'; import { RefreshCw, QrCode, Camera, Wifi, Clock, ChevronDown, ChevronUp, Share2, AlertCircle, } from 'lucide-react-native'; import { useUserStore } from '@/store/useUserStore'; import { getSyncHistory, SyncLogEntry } from '@/lib/syncService'; import QRScanner from '@/components/sync/QRScanner'; // Lazy-load QRCode to handle missing package gracefully let QRCode: any = null; try { QRCode = require('react-native-qrcode-svg').default; } catch { QRCode = null; } function formatDate(ts: number): string { return new Date(ts * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); } export default function SyncScreen() { const user = useUserStore((s) => s.user); const shareId = user?.shareId ?? 'TL-????????'; const [syncHistory, setSyncHistory] = useState([]); const [loadingHistory, setLoadingHistory] = useState(true); const [partnerShareId, setPartnerShareId] = useState(''); const [showScanner, setShowScanner] = useState(false); const [showHowTo, setShowHowTo] = useState(false); useEffect(() => { loadHistory(); }, []); const loadHistory = async () => { try { const history = await getSyncHistory(); setSyncHistory(history); } catch (e) { console.error('Failed to load sync history:', e); } finally { setLoadingHistory(false); } }; const handleScanned = useCallback((scannedId: string) => { setShowScanner(false); setPartnerShareId(scannedId); }, []); const handleStartSync = () => { const trimmed = partnerShareId.trim().toUpperCase(); if (!trimmed.match(/^TL-[A-Z0-9]{8}$/)) { Alert.alert( 'Invalid Share ID', 'Please enter a valid Share ID in the format TL-XXXXXXXX' ); return; } if (trimmed === shareId) { Alert.alert('Same Device', 'You cannot sync with yourself.'); return; } // Navigate to sync-progress (actual sync requires bare workflow) Alert.alert( 'WiFi Sync Not Available', 'Direct WiFi sync requires expo-dev-client and bare workflow.\n\nShare IDs can be exchanged manually. Network sync will be available in a future release.', [ { text: 'OK' }, { text: 'View Progress Demo', onPress: () => router.push({ pathname: '/sync-progress', params: { partnerName: trimmed, sentCount: '0', receivedCount: '0', conflictCount: '0', }, }), }, ] ); }; return ( {/* Header */} Sync {/* ── Your Share ID ── */} Your Share ID {QRCode ? ( ) : ( QR Code Install react-native-qrcode-svg to display )} Share ID {shareId} Show this QR code to your partner so they can connect to you. {/* ── Connect to Partner ── */} Connect to Partner {/* Scan QR button */} setShowScanner(true)} > Scan Partner QR Code or enter manually {/* Manual input */} Start Sync {/* ── Discovered Devices ── */} Discovered Devices WiFi Sync Requires Setup Automatic device discovery (mDNS) requires expo-dev-client and bare workflow. Exchange Share IDs manually for now. {/* ── Sync History ── */} Sync History {loadingHistory ? ( ) : syncHistory.length === 0 ? ( No syncs yet ) : ( syncHistory.map((entry) => ( {entry.partner_name ?? entry.partner_id} {formatDate(entry.synced_at)} ↑ {entry.sent_count} ↓ {entry.received_count} )) )} {/* ── How to Sync ── */} setShowHowTo(!showHowTo)} > How to Sync {showHowTo ? ( ) : ( )} {showHowTo && ( {HOW_TO_STEPS.map((step, i) => ( {i + 1} {step.title} {step.body} ))} )} {/* QR Scanner Modal */} setShowScanner(false)} > setShowScanner(false)} /> ); } const HOW_TO_STEPS = [ { title: 'Open TerritoryLog on both devices', body: 'Both devices must have TerritoryLog installed and be on the same WiFi network.', }, { title: 'Share your QR code or Share ID', body: 'Show the QR code above to your partner, or tell them your Share ID (TL-XXXXXXXX).', }, { title: 'Scan or enter their Share ID', body: "Use the scanner or type their Share ID into the input field on one device.", }, { title: 'Start sync', body: 'Tap "Start Sync". Records are merged intelligently — the most recently updated version of each record wins.', }, { title: 'Done!', body: 'Both devices will have the same up-to-date data. No cloud required.', }, ]; const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: '#F8F4EF', }, container: { flex: 1, }, content: { paddingBottom: 32, }, header: { flexDirection: 'row', alignItems: 'center', gap: 10, paddingHorizontal: 20, paddingTop: 20, paddingBottom: 8, }, headerTitle: { fontSize: 26, fontWeight: '700', color: '#111827', }, section: { marginTop: 20, paddingHorizontal: 16, gap: 8, }, sectionTitle: { fontSize: 13, fontWeight: '600', color: '#6B7280', letterSpacing: 0.6, textTransform: 'uppercase', }, card: { backgroundColor: '#fff', borderRadius: 16, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 4, elevation: 2, gap: 12, }, qrCard: { backgroundColor: '#fff', borderRadius: 16, padding: 20, alignItems: 'center', gap: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 4, elevation: 2, }, qrCodeWrapper: { padding: 12, backgroundColor: '#fff', borderRadius: 12, borderWidth: 1, borderColor: '#E5E7EB', }, qrPlaceholder: { width: 180, height: 180, backgroundColor: '#F3F4F6', borderRadius: 12, justifyContent: 'center', alignItems: 'center', gap: 8, borderWidth: 2, borderColor: '#E5E7EB', borderStyle: 'dashed', }, qrPlaceholderText: { color: '#6B7280', fontSize: 14, fontWeight: '600', }, qrPlaceholderSub: { color: '#9CA3AF', fontSize: 11, textAlign: 'center', paddingHorizontal: 12, }, shareIdRow: { flexDirection: 'row', alignItems: 'center', gap: 10, }, shareIdLabel: { color: '#6B7280', fontSize: 13, }, shareIdBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: '#EFF9F9', paddingVertical: 6, paddingHorizontal: 12, borderRadius: 20, borderWidth: 1, borderColor: '#B2DFDF', }, shareIdText: { color: '#1A6B72', fontSize: 15, fontWeight: '700', letterSpacing: 1, fontFamily: 'monospace', }, shareIdHint: { color: '#9CA3AF', fontSize: 12, textAlign: 'center', }, scanBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, backgroundColor: '#1A6B72', borderRadius: 12, paddingVertical: 13, }, scanBtnText: { color: '#fff', fontSize: 15, fontWeight: '600', }, orRow: { flexDirection: 'row', alignItems: 'center', gap: 10, }, orLine: { flex: 1, height: 1, backgroundColor: '#E5E7EB', }, orText: { color: '#9CA3AF', fontSize: 12, }, input: { borderWidth: 1, borderColor: '#E5E7EB', borderRadius: 12, paddingVertical: 12, paddingHorizontal: 16, fontSize: 16, color: '#111827', letterSpacing: 1, fontFamily: 'monospace', }, connectBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, backgroundColor: '#115E67', borderRadius: 12, paddingVertical: 13, }, connectBtnDisabled: { backgroundColor: '#D1D5DB', }, connectBtnText: { color: '#fff', fontSize: 15, fontWeight: '600', }, wifiNotice: { flexDirection: 'row', alignItems: 'flex-start', gap: 12, backgroundColor: '#FFFBEB', borderWidth: 1, borderColor: '#FDE68A', }, wifiNoticeTitle: { fontSize: 14, fontWeight: '600', color: '#92400E', }, wifiNoticeText: { fontSize: 12, color: '#92400E', lineHeight: 18, }, emptyHistory: { alignItems: 'center', gap: 8, paddingVertical: 16, }, emptyHistoryText: { color: '#9CA3AF', fontSize: 14, }, historyItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#F3F4F6', }, historyLeft: { gap: 2, }, historyPartner: { fontSize: 14, fontWeight: '600', color: '#111827', }, historyDate: { fontSize: 12, color: '#9CA3AF', }, historyRight: { flexDirection: 'row', gap: 12, }, historySent: { fontSize: 13, color: '#1A6B72', fontWeight: '600', }, historyReceived: { fontSize: 13, color: '#10B981', fontWeight: '600', }, howToToggle: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, howToStep: { flexDirection: 'row', gap: 12, alignItems: 'flex-start', }, howToNumber: { width: 24, height: 24, borderRadius: 12, backgroundColor: '#1A6B72', justifyContent: 'center', alignItems: 'center', flexShrink: 0, marginTop: 1, }, howToNumberText: { color: '#fff', fontSize: 12, fontWeight: '700', }, howToStepTitle: { fontSize: 14, fontWeight: '600', color: '#111827', }, howToStepBody: { fontSize: 13, color: '#6B7280', lineHeight: 18, }, });