diff --git a/src/components/ConnectionIndicator.tsx b/src/components/ConnectionIndicator.tsx
new file mode 100644
index 0000000..d1898c6
--- /dev/null
+++ b/src/components/ConnectionIndicator.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { useOfflineSyncContext } from '../contexts/OfflineSyncContext';
+
+interface ConnectionIndicatorProps {
+ onPress?: () => void;
+ showLabel?: boolean;
+}
+
+export function ConnectionIndicator({ onPress, showLabel = false }: ConnectionIndicatorProps) {
+ const { isOnline, isSyncing, pendingSalesCount } = useOfflineSyncContext();
+
+ const getStatusColor = () => {
+ if (!isOnline) return '#EF4444'; // Red - offline
+ if (isSyncing || pendingSalesCount > 0) return '#F59E0B'; // Yellow - syncing/pending
+ return '#10B981'; // Green - online
+ };
+
+ const getStatusText = () => {
+ if (!isOnline) return 'Sin conexion';
+ if (isSyncing) return 'Sincronizando...';
+ if (pendingSalesCount > 0) return `${pendingSalesCount} pendiente${pendingSalesCount > 1 ? 's' : ''}`;
+ return 'Conectado';
+ };
+
+ const content = (
+
+
+ {isSyncing && }
+
+ {showLabel && (
+
+ {getStatusText()}
+
+ )}
+
+ );
+
+ if (onPress) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return content;
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ indicator: {
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ position: 'relative',
+ },
+ pulseRing: {
+ position: 'absolute',
+ width: 20,
+ height: 20,
+ borderRadius: 10,
+ backgroundColor: 'rgba(245, 158, 11, 0.3)',
+ top: -4,
+ left: -4,
+ },
+ label: {
+ fontSize: 12,
+ fontWeight: '500',
+ },
+});
+
+export default ConnectionIndicator;
diff --git a/src/components/OfflineBanner.tsx b/src/components/OfflineBanner.tsx
new file mode 100644
index 0000000..706b997
--- /dev/null
+++ b/src/components/OfflineBanner.tsx
@@ -0,0 +1,123 @@
+import React, { useEffect, useRef } from 'react';
+import { View, Text, StyleSheet, Animated, TouchableOpacity } from 'react-native';
+import { useOfflineSyncContext } from '../contexts/OfflineSyncContext';
+
+interface OfflineBannerProps {
+ onSyncPress?: () => void;
+}
+
+export function OfflineBanner({ onSyncPress }: OfflineBannerProps) {
+ const { isOnline, isSyncing, pendingSalesCount, error } = useOfflineSyncContext();
+ const slideAnim = useRef(new Animated.Value(-60)).current;
+
+ const shouldShow = !isOnline || error;
+
+ useEffect(() => {
+ Animated.timing(slideAnim, {
+ toValue: shouldShow ? 0 : -60,
+ duration: 300,
+ useNativeDriver: true,
+ }).start();
+ }, [shouldShow, slideAnim]);
+
+ if (!shouldShow) return null;
+
+ const getMessage = () => {
+ if (error) return error;
+ if (!isOnline && pendingSalesCount > 0) {
+ return `Modo offline - ${pendingSalesCount} venta${pendingSalesCount > 1 ? 's' : ''} pendiente${pendingSalesCount > 1 ? 's' : ''}`;
+ }
+ return 'Modo offline - los cambios se sincronizaran cuando vuelva la conexion';
+ };
+
+ const getBannerStyle = () => {
+ if (error) return styles.errorBanner;
+ return styles.offlineBanner;
+ };
+
+ return (
+
+
+ {error ? '!' : '!'}
+
+ {getMessage()}
+
+
+ {!isOnline && pendingSalesCount > 0 && onSyncPress && (
+
+
+ {isSyncing ? 'Sincronizando...' : 'Sincronizar'}
+
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ paddingVertical: 10,
+ paddingHorizontal: 16,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ zIndex: 1000,
+ },
+ offlineBanner: {
+ backgroundColor: '#F59E0B',
+ },
+ errorBanner: {
+ backgroundColor: '#EF4444',
+ },
+ content: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ icon: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: '#FFFFFF',
+ width: 24,
+ height: 24,
+ textAlign: 'center',
+ lineHeight: 24,
+ backgroundColor: 'rgba(0,0,0,0.2)',
+ borderRadius: 12,
+ },
+ message: {
+ flex: 1,
+ color: '#FFFFFF',
+ fontSize: 13,
+ fontWeight: '500',
+ },
+ syncButton: {
+ backgroundColor: 'rgba(255,255,255,0.25)',
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: 4,
+ marginLeft: 8,
+ },
+ syncButtonText: {
+ color: '#FFFFFF',
+ fontSize: 12,
+ fontWeight: '600',
+ },
+});
+
+export default OfflineBanner;
diff --git a/src/components/SyncProgress.tsx b/src/components/SyncProgress.tsx
new file mode 100644
index 0000000..ca1c715
--- /dev/null
+++ b/src/components/SyncProgress.tsx
@@ -0,0 +1,284 @@
+import React from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ Modal,
+ TouchableOpacity,
+ ActivityIndicator,
+} from 'react-native';
+import { useOfflineSyncContext } from '../contexts/OfflineSyncContext';
+
+interface SyncProgressProps {
+ visible: boolean;
+ onClose: () => void;
+}
+
+export function SyncProgress({ visible, onClose }: SyncProgressProps) {
+ const {
+ isOnline,
+ isSyncing,
+ pendingSalesCount,
+ lastSyncAt,
+ error,
+ syncNow,
+ downloadDataForOffline,
+ } = useOfflineSyncContext();
+
+ const formatDate = (date: Date | null) => {
+ if (!date) return 'Nunca';
+ return date.toLocaleString('es-MX', {
+ day: 'numeric',
+ month: 'short',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ };
+
+ return (
+
+
+
+
+ Estado de Sincronizacion
+
+ X
+
+
+
+
+
+ Conexion:
+
+
+
+ {isOnline ? 'Conectado' : 'Sin conexion'}
+
+
+
+
+
+ Ultima sincronizacion:
+ {formatDate(lastSyncAt)}
+
+
+
+ Ventas pendientes:
+ 0 && styles.pendingText,
+ ]}
+ >
+ {pendingSalesCount}
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {isSyncing && (
+
+
+ Sincronizando datos...
+
+ )}
+
+
+
+
+ {isSyncing ? 'Descargando...' : 'Descargar datos'}
+
+
+
+ {pendingSalesCount > 0 && (
+
+
+ {isSyncing ? 'Sincronizando...' : `Sincronizar (${pendingSalesCount})`}
+
+
+ )}
+
+
+
+ Datos disponibles offline:
+
+ • Productos y precios
+ • Clientes
+ • Ventas en efectivo
+
+
+ Las ventas con tarjeta requieren conexion a internet.
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ overlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ modal: {
+ backgroundColor: '#FFFFFF',
+ borderRadius: 16,
+ width: '100%',
+ maxWidth: 400,
+ overflow: 'hidden',
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: '#E5E7EB',
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '700',
+ color: '#111827',
+ },
+ closeButton: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: '#F3F4F6',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ closeText: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#6B7280',
+ },
+ statusCard: {
+ padding: 16,
+ gap: 12,
+ },
+ statusRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ statusLabel: {
+ fontSize: 14,
+ color: '#6B7280',
+ },
+ statusValue: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ statusDot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ },
+ statusText: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#111827',
+ },
+ pendingText: {
+ color: '#F59E0B',
+ },
+ errorCard: {
+ margin: 16,
+ marginTop: 0,
+ padding: 12,
+ backgroundColor: '#FEF2F2',
+ borderRadius: 8,
+ borderLeftWidth: 4,
+ borderLeftColor: '#EF4444',
+ },
+ errorText: {
+ fontSize: 13,
+ color: '#B91C1C',
+ },
+ syncingContainer: {
+ padding: 24,
+ alignItems: 'center',
+ gap: 12,
+ },
+ syncingText: {
+ fontSize: 14,
+ color: '#6B7280',
+ },
+ actions: {
+ padding: 16,
+ gap: 10,
+ },
+ button: {
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+ downloadButton: {
+ backgroundColor: '#F3F4F6',
+ },
+ syncButton: {
+ backgroundColor: '#F97316',
+ },
+ buttonText: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#111827',
+ },
+ infoSection: {
+ padding: 16,
+ backgroundColor: '#F9FAFB',
+ borderTopWidth: 1,
+ borderTopColor: '#E5E7EB',
+ },
+ infoTitle: {
+ fontSize: 13,
+ fontWeight: '600',
+ color: '#374151',
+ marginBottom: 8,
+ },
+ infoList: {
+ gap: 4,
+ marginBottom: 8,
+ },
+ infoItem: {
+ fontSize: 13,
+ color: '#6B7280',
+ },
+ infoNote: {
+ fontSize: 12,
+ color: '#9CA3AF',
+ fontStyle: 'italic',
+ },
+});
+
+export default SyncProgress;
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..cc5bc95
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,3 @@
+export { ConnectionIndicator } from './ConnectionIndicator';
+export { OfflineBanner } from './OfflineBanner';
+export { SyncProgress } from './SyncProgress';
diff --git a/src/services/deepLinking.ts b/src/services/deepLinking.ts
new file mode 100644
index 0000000..6f80974
--- /dev/null
+++ b/src/services/deepLinking.ts
@@ -0,0 +1,204 @@
+import * as Linking from 'expo-linking';
+import { NavigationContainerRef } from '@react-navigation/native';
+
+// URL scheme prefix
+const SCHEME = 'michangarrito://';
+
+// Supported deep link routes
+export const DEEP_LINK_ROUTES = {
+ // Main screens
+ DASHBOARD: 'dashboard',
+ POS: 'pos',
+ POS_NEW: 'pos/new',
+ PRODUCTS: 'products',
+ PRODUCT_DETAIL: 'products/:id',
+ INVENTORY: 'inventory',
+ ORDERS: 'orders',
+ ORDER_DETAIL: 'orders/:id',
+ CUSTOMERS: 'customers',
+ SCAN: 'scan',
+ SETTINGS: 'settings',
+ REPORTS: 'reports',
+};
+
+// Map deep link paths to screen names
+const ROUTE_MAP: Record = {
+ dashboard: { screen: 'Main', nested: 'Dashboard' },
+ pos: { screen: 'Main', nested: 'POS' },
+ 'pos/new': { screen: 'Main', nested: 'POS' },
+ products: { screen: 'Main', nested: 'Products' },
+ inventory: { screen: 'Main', nested: 'Inventory' },
+ orders: { screen: 'Main', nested: 'Orders' },
+ customers: { screen: 'Main', nested: 'Customers' },
+ scan: { screen: 'Scanner' },
+ settings: { screen: 'Main', nested: 'Settings' },
+ reports: { screen: 'Main', nested: 'Reports' },
+};
+
+// Navigation reference (set from App.tsx)
+let navigationRef: NavigationContainerRef | null = null;
+
+export function setNavigationRef(ref: NavigationContainerRef) {
+ navigationRef = ref;
+}
+
+/**
+ * Get the linking configuration for React Navigation
+ */
+export function getLinkingConfig() {
+ return {
+ prefixes: [SCHEME, 'https://michangarrito.com'],
+ config: {
+ screens: {
+ Main: {
+ screens: {
+ Dashboard: 'dashboard',
+ POS: 'pos',
+ Products: 'products',
+ Inventory: 'inventory',
+ Orders: 'orders',
+ Customers: 'customers',
+ Settings: 'settings',
+ Reports: 'reports',
+ },
+ },
+ Scanner: 'scan',
+ ProductDetail: 'products/:id',
+ OrderDetail: 'orders/:id',
+ },
+ },
+ };
+}
+
+/**
+ * Parse a deep link URL and return the route info
+ */
+export function parseDeepLink(url: string): {
+ path: string;
+ params?: Record;
+} | null {
+ try {
+ const parsed = Linking.parse(url);
+
+ if (!parsed.path) {
+ return null;
+ }
+
+ const path = parsed.path;
+ const params = parsed.queryParams as Record || {};
+
+ // Check for path parameters (e.g., products/:id)
+ const pathParts = path.split('/');
+ if (pathParts.length >= 2) {
+ const basePath = pathParts[0];
+ const id = pathParts[1];
+ if (id && !id.startsWith(':')) {
+ params.id = id;
+ }
+ }
+
+ return { path, params };
+ } catch (error) {
+ console.error('Error parsing deep link:', error);
+ return null;
+ }
+}
+
+/**
+ * Navigate to a deep link destination
+ */
+export async function navigateToDeepLink(url: string) {
+ if (!navigationRef?.isReady()) {
+ console.warn('Navigation not ready for deep link:', url);
+ // Store for later processing
+ pendingDeepLink = url;
+ return;
+ }
+
+ const parsed = parseDeepLink(url);
+ if (!parsed) {
+ console.warn('Could not parse deep link:', url);
+ return;
+ }
+
+ const routeInfo = ROUTE_MAP[parsed.path] || ROUTE_MAP[parsed.path.split('/')[0]];
+ if (!routeInfo) {
+ console.warn('Unknown deep link route:', parsed.path);
+ return;
+ }
+
+ try {
+ if (routeInfo.nested) {
+ navigationRef.navigate(routeInfo.screen, {
+ screen: routeInfo.nested,
+ params: parsed.params,
+ });
+ } else {
+ navigationRef.navigate(routeInfo.screen, parsed.params);
+ }
+ } catch (error) {
+ console.error('Error navigating to deep link:', error);
+ }
+}
+
+// Store pending deep link for cold start
+let pendingDeepLink: string | null = null;
+
+/**
+ * Process any pending deep link (called when navigation is ready)
+ */
+export function processPendingDeepLink() {
+ if (pendingDeepLink && navigationRef?.isReady()) {
+ navigateToDeepLink(pendingDeepLink);
+ pendingDeepLink = null;
+ }
+}
+
+/**
+ * Get the initial deep link URL (for cold start)
+ */
+export async function getInitialDeepLink(): Promise {
+ const url = await Linking.getInitialURL();
+ return url;
+}
+
+/**
+ * Subscribe to deep link events
+ */
+export function subscribeToDeepLinks(
+ callback: (url: string) => void
+): () => void {
+ const subscription = Linking.addEventListener('url', ({ url }) => {
+ callback(url);
+ });
+
+ return () => subscription.remove();
+}
+
+/**
+ * Create a deep link URL for a given path
+ */
+export function createDeepLink(path: string, params?: Record): string {
+ let url = `${SCHEME}${path}`;
+
+ if (params && Object.keys(params).length > 0) {
+ const queryString = Object.entries(params)
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
+ .join('&');
+ url += `?${queryString}`;
+ }
+
+ return url;
+}
+
+export default {
+ setNavigationRef,
+ getLinkingConfig,
+ parseDeepLink,
+ navigateToDeepLink,
+ processPendingDeepLink,
+ getInitialDeepLink,
+ subscribeToDeepLinks,
+ createDeepLink,
+ DEEP_LINK_ROUTES,
+};