# Integrating Sync Library into Existing FlutterFlow App

This guide shows how to integrate the FlutterFlow-Supabase Sync Middleware into your existing FlutterFlow application.

## 📋 Prerequisites

- ✅ Existing FlutterFlow app
- ✅ Supabase project set up
- ✅ Middleware server running
- ✅ Access to your FlutterFlow project

## 🚀 Step 1: Add Custom Code to Your FlutterFlow Project

### 1.1 Add the Sync Client Library

1. In FlutterFlow, go to **Custom Code** → **Actions**
2. Click **Create Action**
3. Name it: `FlutterFlowSyncClient`
4. Add the following code:

```dart
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:supabase_flutter/supabase_flutter.dart';

class FlutterFlowSyncClient {
  static FlutterFlowSyncClient? _instance;
  static FlutterFlowSyncClient get instance => _instance ??= FlutterFlowSyncClient._();
  
  FlutterFlowSyncClient._();
  
  String? _serverUrl;
  String? _supabaseUrl;
  String? _supabaseAnonKey;
  SupabaseClient? _supabaseClient;
  
  // Initialize the sync client
  Future<void> initialize({
    required String serverUrl,
    required String supabaseUrl,
    required String supabaseAnonKey,
  }) async {
    _serverUrl = serverUrl;
    _supabaseUrl = supabaseUrl;
    _supabaseAnonKey = supabaseAnonKey;
    
    // Initialize Supabase
    await Supabase.initialize(
      url: supabaseUrl,
      anonKey: supabaseAnonKey,
    );
    
    _supabaseClient = Supabase.instance.client;
  }
  
  // Get current user
  User? get currentUser => _supabaseClient?.auth.currentUser;
  
  // Check if connected
  bool get isConnected => _supabaseClient?.auth.currentUser != null;
  
  // Read data from sync system
  Future<List<Map<String, dynamic>>> readData({
    required String tableName,
    String? where,
    List<dynamic>? whereArgs,
    String? orderBy,
    int? limit,
  }) async {
    try {
      final response = await http.post(
        Uri.parse('$_serverUrl/api/sync/read'),
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ${_getAuthToken()}',
        },
        body: jsonEncode({
          'tableName': tableName,
          'where': where,
          'whereArgs': whereArgs,
          'orderBy': orderBy,
          'limit': limit,
        }),
      );
      
      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return List<Map<String, dynamic>>.from(data['data'] ?? []);
      } else {
        throw Exception('Failed to read data: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error reading data: $e');
    }
  }
  
  // Write data to sync system
  Future<void> writeData({
    required String tableName,
    required String operation,
    required Map<String, dynamic> data,
  }) async {
    try {
      final response = await http.post(
        Uri.parse('$_serverUrl/api/sync/write'),
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ${_getAuthToken()}',
        },
        body: jsonEncode({
          'tableName': tableName,
          'operation': operation,
          'data': data,
        }),
      );
      
      if (response.statusCode != 200) {
        throw Exception('Failed to write data: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error writing data: $e');
    }
  }
  
  // Upload file
  Future<Map<String, dynamic>?> uploadFile({
    required File file,
    String fieldName = 'file',
  }) async {
    try {
      final request = http.MultipartRequest(
        'POST',
        Uri.parse('$_serverUrl/api/upload/single'),
      );
      
      request.headers['Authorization'] = 'Bearer ${_getAuthToken()}';
      request.files.add(await http.MultipartFile.fromPath(fieldName, file.path));
      
      final response = await request.send();
      final responseBody = await response.stream.bytesToString();
      
      if (response.statusCode == 200) {
        return jsonDecode(responseBody);
      } else {
        throw Exception('Failed to upload file: $responseBody');
      }
    } catch (e) {
      throw Exception('Error uploading file: $e');
    }
  }
  
  // Upload image
  Future<Map<String, dynamic>?> uploadImage({
    required File imageFile,
    String fieldName = 'file',
  }) async {
    return await uploadFile(file: imageFile, fieldName: fieldName);
  }
  
  // Get uploaded files
  Future<List<Map<String, dynamic>>> getUploadedFiles({
    int? limit,
    int? offset,
    String? mimeType,
  }) async {
    try {
      final queryParams = <String, String>{};
      if (limit != null) queryParams['limit'] = limit.toString();
      if (offset != null) queryParams['offset'] = offset.toString();
      if (mimeType != null) queryParams['mimeType'] = mimeType;
      
      final uri = Uri.parse('$_serverUrl/api/upload/files').replace(
        queryParameters: queryParams,
      );
      
      final response = await http.get(
        uri,
        headers: {
          'Authorization': 'Bearer ${_getAuthToken()}',
        },
      );
      
      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return List<Map<String, dynamic>>.from(data['files'] ?? []);
      } else {
        throw Exception('Failed to get files: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error getting files: $e');
    }
  }
  
  // Delete uploaded file
  Future<void> deleteUploadedFile({required String fileId}) async {
    try {
      final response = await http.delete(
        Uri.parse('$_serverUrl/api/upload/files/$fileId'),
        headers: {
          'Authorization': 'Bearer ${_getAuthToken()}',
        },
      );
      
      if (response.statusCode != 200) {
        throw Exception('Failed to delete file: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error deleting file: $e');
    }
  }
  
  // Trigger sync
  Future<void> triggerSync() async {
    try {
      final response = await http.post(
        Uri.parse('$_serverUrl/api/sync/trigger'),
        headers: {
          'Authorization': 'Bearer ${_getAuthToken()}',
        },
      );
      
      if (response.statusCode != 200) {
        throw Exception('Failed to trigger sync: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error triggering sync: $e');
    }
  }
  
  // Get auth token
  String _getAuthToken() {
    final user = currentUser;
    if (user == null) {
      throw Exception('User not authenticated');
    }
    return user.accessToken ?? '';
  }
}
```

### 1.2 Add Sync Actions

Create these custom actions in FlutterFlow:

#### InitializeSync Action
```dart
import 'package:your_app/custom_actions/flutterflow_sync_client.dart';

Future<void> initializeSync() async {
  try {
    await FlutterFlowSyncClient.instance.initialize(
      serverUrl: 'http://your-server-url:3000', // Replace with your server URL
      supabaseUrl: 'https://your-project.supabase.co', // Your Supabase URL
      supabaseAnonKey: 'your-supabase-anon-key', // Your Supabase anon key
    );
    
    // Show success message
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Sync initialized successfully!')),
    );
  } catch (e) {
    // Show error message
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Sync initialization failed: $e')),
    );
  }
}
```

#### ReadSyncData Action
```dart
import 'package:your_app/custom_actions/flutterflow_sync_client.dart';

Future<List<dynamic>> readSyncData(String tableName, String? where, int? limit) async {
  try {
    final data = await FlutterFlowSyncClient.instance.readData(
      tableName: tableName,
      where: where,
      limit: limit,
    );
    
    return data;
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Failed to read data: $e')),
    );
    return [];
  }
}
```

#### WriteSyncData Action
```dart
import 'package:your_app/custom_actions/flutterflow_sync_client.dart';

Future<void> writeSyncData(String tableName, String operation, Map<String, dynamic> data) async {
  try {
    await FlutterFlowSyncClient.instance.writeData(
      tableName: tableName,
      operation: operation,
      data: data,
    );
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Data saved successfully!')),
    );
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Failed to save data: $e')),
    );
  }
}
```

#### UploadImage Action
```dart
import 'package:your_app/custom_actions/flutterflow_sync_client.dart';
import 'dart:io';

Future<Map<String, dynamic>?> uploadImage(File imageFile) async {
  try {
    final result = await FlutterFlowSyncClient.instance.uploadImage(
      imageFile: imageFile,
    );
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Image uploaded successfully!')),
    );
    
    return result;
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Image upload failed: $e')),
    );
    return null;
  }
}
```

## 🎨 Step 2: Add Custom Widgets

### 2.1 Sync Status Widget

1. Go to **Custom Code** → **Widgets**
2. Click **Create Widget**
3. Name it: `SyncStatusWidget`
4. Add the following code:

```dart
import 'package:flutter/material.dart';
import 'package:your_app/custom_actions/flutterflow_sync_client.dart';

class SyncStatusWidget extends StatefulWidget {
  const SyncStatusWidget({Key? key}) : super(key: key);

  @override
  _SyncStatusWidgetState createState() => _SyncStatusWidgetState();
}

class _SyncStatusWidgetState extends State<SyncStatusWidget> {
  bool _isConnected = false;
  
  @override
  void initState() {
    super.initState();
    _checkConnection();
  }
  
  void _checkConnection() {
    setState(() {
      _isConnected = FlutterFlowSyncClient.instance.isConnected;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(
          _isConnected ? Icons.cloud_done : Icons.cloud_off,
          color: _isConnected ? Colors.green : Colors.red,
          size: 16,
        ),
        const SizedBox(width: 4),
        Text(
          _isConnected ? 'Connected' : 'Offline',
          style: TextStyle(
            color: _isConnected ? Colors.green : Colors.red,
            fontSize: 12,
          ),
        ),
      ],
    );
  }
}
```

### 2.2 Sync Button Widget

1. Create widget named: `SyncButtonWidget`
2. Add the following code:

```dart
import 'package:flutter/material.dart';
import 'package:your_app/custom_actions/flutterflow_sync_client.dart';

class SyncButtonWidget extends StatefulWidget {
  const SyncButtonWidget({Key? key}) : super(key: key);

  @override
  _SyncButtonWidgetState createState() => _SyncButtonWidgetState();
}

class _SyncButtonWidgetState extends State<SyncButtonWidget> {
  bool _isSyncing = false;
  
  Future<void> _triggerSync() async {
    setState(() {
      _isSyncing = true;
    });
    
    try {
      await FlutterFlowSyncClient.instance.triggerSync();
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Sync completed!')),
      );
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Sync failed: $e')),
      );
    } finally {
      setState(() {
        _isSyncing = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton.icon(
      onPressed: _isSyncing ? null : _triggerSync,
      icon: _isSyncing 
        ? const SizedBox(
            width: 16,
            height: 16,
            child: CircularProgressIndicator(strokeWidth: 2),
          )
        : const Icon(Icons.sync),
      label: Text(_isSyncing ? 'Syncing...' : 'Sync Now'),
    );
  }
}
```

## 🔧 Step 3: Update Your Existing Pages

### 3.1 Add Sync Initialization

1. Go to your main page (usually the first page)
2. Add an **On Page Load** action
3. Call the `InitializeSync` action

### 3.2 Add Sync Controls to Existing Pages

1. **Add to App Bar:**
   - Add `SyncStatusWidget` to app bar actions
   - Add `SyncButtonWidget` to app bar actions

2. **Add to Page Body:**
   - Add sync status indicator
   - Add sync button for manual sync

### 3.3 Update Data Operations

Replace your existing data operations with sync-enabled versions:

#### Before (Direct Supabase):
```dart
// Old way - direct Supabase
final response = await Supabase.instance.client
  .from('my_table')
  .select()
  .eq('user_id', currentUserId);
```

#### After (Sync-enabled):
```dart
// New way - through sync system
final data = await ReadSyncData(
  tableName: 'my_table',
  where: 'user_id = ?',
  whereArgs: [currentUserId],
);
```

## 📱 Step 4: Add File Upload to Existing Pages

### 4.1 Add Image Upload to Forms

1. **Add Image Picker:**
   - Add an `Image` widget for preview
   - Add an `ElevatedButton` for image selection
   - Connect to `UploadImage` action

2. **Example Implementation:**
```dart
// In your form page
File? _selectedImage;

Future<void> _pickImage() async {
  // Use image_picker package
  final picker = ImagePicker();
  final pickedFile = await picker.pickImage(source: ImageSource.gallery);
  
  if (pickedFile != null) {
    setState(() {
      _selectedImage = File(pickedFile.path);
    });
  }
}

Future<void> _uploadImage() async {
  if (_selectedImage != null) {
    final result = await UploadImage(_selectedImage!);
    if (result != null) {
      // Save image reference to your data
      await WriteSyncData(
        tableName: 'user_images',
        operation: 'INSERT',
        data: {
          'id': 'img_${DateTime.now().millisecondsSinceEpoch}',
          'user_id': currentUserId,
          'image_url': result['fileUrl'],
          'created_at': DateTime.now().toIso8601String(),
        },
      );
    }
  }
}
```

### 4.2 Add Image Gallery to Existing Pages

1. **Create Image Gallery Widget:**
```dart
class ImageGalleryWidget extends StatefulWidget {
  final String userId;
  
  const ImageGalleryWidget({Key? key, required this.userId}) : super(key: key);
  
  @override
  _ImageGalleryWidgetState createState() => _ImageGalleryWidgetState();
}

class _ImageGalleryWidgetState extends State<ImageGalleryWidget> {
  List<Map<String, dynamic>> _images = [];
  bool _isLoading = false;
  
  @override
  void initState() {
    super.initState();
    _loadImages();
  }
  
  Future<void> _loadImages() async {
    setState(() => _isLoading = true);
    
    try {
      final images = await ReadSyncData(
        tableName: 'user_images',
        where: 'user_id = ?',
        whereArgs: [widget.userId],
        orderBy: 'created_at DESC',
      );
      
      setState(() {
        _images = images;
      });
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Failed to load images: $e')),
      );
    } finally {
      setState(() => _isLoading = false);
    }
  }
  
  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return Center(child: CircularProgressIndicator());
    }
    
    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
      ),
      itemCount: _images.length,
      itemBuilder: (context, index) {
        final image = _images[index];
        return GestureDetector(
          onTap: () {
            // Show full-size image
            _showImageDialog(image['image_url']);
          },
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(8),
              image: DecorationImage(
                image: NetworkImage(image['image_url']),
                fit: BoxFit.cover,
              ),
            ),
          ),
        );
      },
    );
  }
  
  void _showImageDialog(String imageUrl) {
    showDialog(
      context: context,
      builder: (context) => Dialog(
        child: Container(
          width: double.infinity,
          height: 400,
          decoration: BoxDecoration(
            image: DecorationImage(
              image: NetworkImage(imageUrl),
              fit: BoxFit.contain,
            ),
          ),
        ),
      ),
    );
  }
}
```

## 🔄 Step 5: Update Existing Data Operations

### 5.1 Replace Direct Supabase Calls

Find all instances of direct Supabase calls in your app and replace them:

#### User Data Operations:
```dart
// Before
final user = await Supabase.instance.client
  .from('app_user')
  .select()
  .eq('id', currentUserId)
  .single();

// After
final users = await ReadSyncData(
  tableName: 'app_user',
  where: 'id = ?',
  whereArgs: [currentUserId],
);
final user = users.isNotEmpty ? users.first : null;
```

#### Store Operations:
```dart
// Before
await Supabase.instance.client
  .from('store_user_link')
  .insert({
    'id': storeId,
    'name': storeName,
    'user_id': currentUserId,
  });

// After
await WriteSyncData(
  tableName: 'store_user_link',
  operation: 'INSERT',
  data: {
    'id': storeId,
    'name': storeName,
    'user_id': currentUserId,
    'created_at': DateTime.now().toIso8601String(),
  },
);
```

#### Task Operations:
```dart
// Before
final tasks = await Supabase.instance.client
  .from('task')
  .select()
  .eq('store_code', storeCode)
  .eq('status', 'pending');

// After
final tasks = await ReadSyncData(
  tableName: 'task',
  where: 'store_code = ? AND status = ?',
  whereArgs: [storeCode, 'pending'],
  orderBy: 'created_at ASC',
);
```

### 5.2 Update Form Submissions

Replace form submission logic with sync-enabled versions:

```dart
// Before
Future<void> _submitForm() async {
  await Supabase.instance.client
    .from('survey_responses')
    .insert({
      'id': responseId,
      'store_code': storeCode,
      'question_id': questionId,
      'answer': answer,
    });
}

// After
Future<void> _submitForm() async {
  await WriteSyncData(
    tableName: 'survey_responses',
    operation: 'INSERT',
    data: {
      'id': responseId,
      'store_code': storeCode,
      'question_id': questionId,
      'answer': answer,
      'created_at': DateTime.now().toIso8601String(),
    },
  );
}
```

## 🧪 Step 6: Test Your Integration

### 6.1 Test Sync Initialization

1. Run your app
2. Check that sync initializes without errors
3. Verify the sync status widget shows "Connected"

### 6.2 Test Data Operations

1. **Test Reading Data:**
   - Try reading from your existing tables
   - Verify data loads correctly
   - Check offline functionality

2. **Test Writing Data:**
   - Create new records
   - Update existing records
   - Delete records
   - Verify changes sync to server

3. **Test Image Upload:**
   - Select and upload images
   - Verify images appear in gallery
   - Test image deletion

### 6.3 Test Offline Functionality

1. **Go Offline:**
   - Turn off internet connection
   - Try creating/updating data
   - Verify data is stored locally

2. **Go Online:**
   - Turn internet back on
   - Trigger sync
   - Verify data syncs to server

## 🐛 Step 7: Troubleshooting

### Common Issues and Solutions

**Issue: Sync initialization fails**
- Check your server URL is correct
- Verify Supabase credentials
- Ensure middleware server is running

**Issue: Data not syncing**
- Check network connection
- Verify sync rules configuration
- Check server logs for errors

**Issue: Images not uploading**
- Check file size limits
- Verify image format is supported
- Check server storage permissions

**Issue: Existing data not showing**
- Verify table names match your sync rules
- Check WHERE clauses are correct
- Ensure data exists in Supabase

## 🎯 Step 8: Best Practices

### 8.1 Gradual Migration

1. **Start Small:**
   - Begin with one page/feature
   - Test thoroughly before moving to next
   - Keep old code as backup

2. **Test Each Step:**
   - Test data reading first
   - Then test data writing
   - Finally test file uploads

### 8.2 Error Handling

Always wrap sync operations in try-catch blocks:

```dart
try {
  final data = await ReadSyncData(tableName: 'my_table');
  // Use data
} catch (e) {
  // Handle error appropriately
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('Error: $e')),
  );
}
```

### 8.3 Loading States

Show loading indicators during operations:

```dart
bool _isLoading = false;

Future<void> _loadData() async {
  setState(() {
    _isLoading = true;
  });
  
  try {
    final data = await ReadSyncData(tableName: 'my_table');
    // Use data
  } finally {
    setState(() {
      _isLoading = false;
    });
  }
}
```

## 🚀 Step 9: Production Deployment

### 9.1 Update Configuration

1. **Update Server URLs:**
   - Change from localhost to production URLs
   - Use HTTPS in production

2. **Update Supabase Credentials:**
   - Use production Supabase project
   - Verify RLS policies

### 9.2 Test Production

1. **Test All Features:**
   - Data sync
   - Image uploads
   - Offline functionality
   - Error handling

2. **Performance Testing:**
   - Test with large datasets
   - Test with poor network conditions
   - Monitor server performance

## 🎉 Congratulations!

You've successfully integrated the sync library into your existing FlutterFlow app! Your app now has:

- ✅ Offline-first data synchronization
- ✅ Real-time sync status monitoring
- ✅ Image upload and management
- ✅ Conflict resolution
- ✅ Comprehensive error handling

## 📞 Support

If you encounter issues:

1. Check the main README for troubleshooting
2. Review server logs for errors
3. Test with the dashboard to verify server functionality
4. Check FlutterFlow custom code documentation

Happy coding! 🚀
