User Consent Management

Implement GDPR/CCPA compliant opt-in/opt-out flows in Flutter

User Consent Management

Implement comprehensive user consent management for GDPR, CCPA, and other privacy regulations while maintaining the full power of UXCam's analytics capabilities.

Overview

User consent management is essential for:

  • GDPR Compliance (European Union)
  • CCPA Compliance (California)
  • Privacy-First Development (Global best practice)
  • User Trust Building (Transparent data practices)

UXCam's Flutter SDK provides flexible consent management that allows users to control their data while giving you the insights you need.

Basic Consent Implementation

Simple Opt-In/Opt-Out

import 'package:flutter_uxcam/flutter_uxcam.dart';

class ConsentManager {
  static const String CONSENT_KEY = 'uxcam_user_consent';
  
  // Check current consent status
  static Future<bool> hasUserConsented() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getBool(CONSENT_KEY) ?? false;
  }
  
  // Grant consent and start recording
  static Future<void> grantConsent() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool(CONSENT_KEY, true);
    
    // Opt into video recordings
    FlutterUxcam.optIntoVideoRecordings();
    
    // Start UXCam with configuration
    FlutterUxConfig config = FlutterUxConfig(
      userAppKey: "YOUR_APP_KEY",
      enableAutomaticScreenNameTagging: true,
    );
    await FlutterUxcam.startWithConfiguration(config);
    
    print('User has granted consent - UXCam recording started');
  }
  
  // Revoke consent and stop recording
  static Future<void> revokeConsent() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool(CONSENT_KEY, false);
    
    // Opt out of recordings
    FlutterUxcam.optOutOfVideoRecordings();
    
    // Stop current session
    await FlutterUxcam.stopSessionAndUploadData();
    
    print('User has revoked consent - UXCam recording stopped');
  }
  
  // Initialize UXCam based on consent status
  static Future<void> initializeWithConsent(String appKey) async {
    final hasConsent = await hasUserConsented();
    
    if (hasConsent) {
      await grantConsent();
    } else {
      // Show consent dialog or handle no-consent state
      await _handleNoConsent();
    }
  }
  
  static Future<void> _handleNoConsent() async {
    // Could show consent dialog or just log
    print('User has not granted consent - UXCam not initialized');
  }
}

Consent Dialog Implementation

class ConsentDialog extends StatefulWidget {
  final VoidCallback onConsentGiven;
  final VoidCallback onConsentDenied;
  
  const ConsentDialog({
    Key? key,
    required this.onConsentGiven,
    required this.onConsentDenied,
  }) : super(key: key);
  
  @override
  _ConsentDialogState createState() => _ConsentDialogState();
}

class _ConsentDialogState extends State<ConsentDialog> {
  bool _analyticsConsent = false;
  bool _crashReportingConsent = false;
  
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Privacy & Data Collection'),
      content: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              'We use analytics to improve your experience. You can control what data we collect:',
              style: TextStyle(fontSize: 16),
            ),
            SizedBox(height: 16),
            CheckboxListTile(
              title: Text('User Analytics'),
              subtitle: Text('Help us understand how you use the app'),
              value: _analyticsConsent,
              onChanged: (value) {
                setState(() {
                  _analyticsConsent = value ?? false;
                });
              },
            ),
            CheckboxListTile(
              title: Text('Crash Reporting'),
              subtitle: Text('Help us fix bugs and improve stability'),
              value: _crashReportingConsent,
              onChanged: (value) {
                setState(() {
                  _crashReportingConsent = value ?? false;
                });
              },
            ),
            SizedBox(height: 16),
            Text(
              'You can change these preferences anytime in Settings.',
              style: TextStyle(fontSize: 12, color: Colors.grey[600]),
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () {
            Navigator.of(context).pop();
            widget.onConsentDenied();
          },
          child: Text('Decline All'),
        ),
        ElevatedButton(
          onPressed: (_analyticsConsent || _crashReportingConsent)
              ? () {
                  Navigator.of(context).pop();
                  _handleConsentGiven();
                }
              : null,
          child: Text('Accept Selected'),
        ),
      ],
    );
  }
  
  void _handleConsentGiven() {
    // Store granular consent preferences
    _storeConsentPreferences(_analyticsConsent, _crashReportingConsent);
    widget.onConsentGiven();
  }
  
  Future<void> _storeConsentPreferences(
    bool analytics,
    bool crashReporting,
  ) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setBool('analytics_consent', analytics);
    await prefs.setBool('crash_reporting_consent', crashReporting);
  }
}

// Usage in your app
class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _consentChecked = false;
  
  @override
  void initState() {
    super.initState();
    _checkAndRequestConsent();
  }
  
  Future<void> _checkAndRequestConsent() async {
    final hasConsent = await ConsentManager.hasUserConsented();
    
    if (!hasConsent) {
      // Show consent dialog
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _showConsentDialog();
      });
    } else {
      // Initialize with existing consent
      await ConsentManager.initializeWithConsent("YOUR_APP_KEY");
      setState(() {
        _consentChecked = true;
      });
    }
  }
  
  void _showConsentDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => ConsentDialog(
        onConsentGiven: () async {
          await ConsentManager.grantConsent();
          setState(() {
            _consentChecked = true;
          });
        },
        onConsentDenied: () {
          setState(() {
            _consentChecked = true;
          });
        },
      ),
    );
  }
}

Advanced Consent Management

Granular Consent Controls

class GranularConsentManager {
  static const String ANALYTICS_CONSENT_KEY = 'uxcam_analytics_consent';
  static const String CRASH_CONSENT_KEY = 'uxcam_crash_consent';
  static const String PERFORMANCE_CONSENT_KEY = 'uxcam_performance_consent';
  static const String CONSENT_VERSION_KEY = 'uxcam_consent_version';
  static const int CURRENT_CONSENT_VERSION = 2;
  
  // Consent types enum
  enum ConsentType {
    analytics,
    crashReporting,
    performanceMonitoring,
  }
  
  // Check if consent needs to be refreshed
  static Future<bool> needsConsentRefresh() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final currentVersion = prefs.getInt(CONSENT_VERSION_KEY) ?? 0;
    return currentVersion < CURRENT_CONSENT_VERSION;
  }
  
  // Get consent for specific type
  static Future<bool> hasConsentFor(ConsentType type) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final key = _getConsentKey(type);
    return prefs.getBool(key) ?? false;
  }
  
  // Set consent for specific type
  static Future<void> setConsentFor(ConsentType type, bool granted) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final key = _getConsentKey(type);
    await prefs.setBool(key, granted);
    
    // Update consent version
    await prefs.setInt(CONSENT_VERSION_KEY, CURRENT_CONSENT_VERSION);
    
    // Update UXCam configuration based on consent
    await _updateUXCamConfiguration();
  }
  
  // Set multiple consents at once
  static Future<void> setConsents(Map<ConsentType, bool> consents) async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    
    for (final entry in consents.entries) {
      final key = _getConsentKey(entry.key);
      await prefs.setBool(key, entry.value);
    }
    
    await prefs.setInt(CONSENT_VERSION_KEY, CURRENT_CONSENT_VERSION);
    await _updateUXCamConfiguration();
  }
  
  // Get all consents
  static Future<Map<ConsentType, bool>> getAllConsents() async {
    final result = <ConsentType, bool>{};
    
    for (final type in ConsentType.values) {
      result[type] = await hasConsentFor(type);
    }
    
    return result;
  }
  
  // Update UXCam configuration based on current consents
  static Future<void> _updateUXCamConfiguration() async {
    final analyticsConsent = await hasConsentFor(ConsentType.analytics);
    final crashConsent = await hasConsentFor(ConsentType.crashReporting);
    final performanceConsent = await hasConsentFor(ConsentType.performanceMonitoring);
    
    if (analyticsConsent || crashConsent || performanceConsent) {
      // User has granted some form of consent
      FlutterUxcam.optIntoVideoRecordings();
      
      FlutterUxConfig config = FlutterUxConfig(
        userAppKey: "YOUR_APP_KEY",
        enableAutomaticScreenNameTagging: analyticsConsent,
        enableCrashHandling: crashConsent,
        enableImprovedScreenCapture: performanceConsent,
      );
      
      await FlutterUxcam.startWithConfiguration(config);
    } else {
      // User has denied all consent
      FlutterUxcam.optOutOfVideoRecordings();
      await FlutterUxcam.stopSessionAndUploadData();
    }
  }
  
  static String _getConsentKey(ConsentType type) {
    switch (type) {
      case ConsentType.analytics:
        return ANALYTICS_CONSENT_KEY;
      case ConsentType.crashReporting:
        return CRASH_CONSENT_KEY;
      case ConsentType.performanceMonitoring:
        return PERFORMANCE_CONSENT_KEY;
    }
  }
  
  // Clear all consent data (for account deletion)
  static Future<void> clearAllConsents() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    
    for (final type in ConsentType.values) {
      await prefs.remove(_getConsentKey(type));
    }
    
    await prefs.remove(CONSENT_VERSION_KEY);
    
    // Stop UXCam and clear data
    FlutterUxcam.optOutOfVideoRecordings();
    await FlutterUxcam.stopSessionAndUploadData();
  }
}

Regional Compliance

class RegionalComplianceManager {
  // Detect user's region for compliance requirements
  static Future<String> detectUserRegion() async {
    try {
      // Use a geolocation service or device locale
      final locale = Localizations.localeOf(context);
      final countryCode = locale.countryCode ?? 'US';
      
      return countryCode;
    } catch (e) {
      // Default to most restrictive (EU) if detection fails
      return 'EU';
    }
  }
  
  // Check if region requires explicit consent
  static bool requiresExplicitConsent(String region) {
    const gdprRegions = ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 
                         'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 
                         'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 
                         'SI', 'ES', 'SE', 'GB', 'EU'];
    const ccpaRegions = ['CA']; // California
    
    return gdprRegions.contains(region) || ccpaRegions.contains(region);
  }
  
  // Get compliance requirements for region
  static ComplianceRequirements getComplianceRequirements(String region) {
    if (region == 'CA') {
      return ComplianceRequirements.ccpa();
    } else if (['GB', 'EU'].contains(region) || 
               requiresExplicitConsent(region)) {
      return ComplianceRequirements.gdpr();
    } else {
      return ComplianceRequirements.minimal();
    }
  }
  
  // Initialize UXCam with region-appropriate consent flow
  static Future<void> initializeWithRegionalCompliance() async {
    final region = await detectUserRegion();
    final requirements = getComplianceRequirements(region);
    
    if (requirements.requiresExplicitConsent) {
      // Check if we have valid consent
      final needsConsent = await GranularConsentManager.needsConsentRefresh();
      final hasAnyConsent = await _hasAnyConsent();
      
      if (needsConsent || !hasAnyConsent) {
        // Show appropriate consent dialog
        await _showRegionalConsentDialog(requirements);
      } else {
        // Use existing consent
        await GranularConsentManager._updateUXCamConfiguration();
      }
    } else {
      // Region doesn't require explicit consent, use default settings
      await _initializeWithDefaultSettings();
    }
  }
  
  static Future<bool> _hasAnyConsent() async {
    final consents = await GranularConsentManager.getAllConsents();
    return consents.values.any((consent) => consent);
  }
  
  static Future<void> _showRegionalConsentDialog(
    ComplianceRequirements requirements,
  ) async {
    // Implementation depends on your UI framework
    // This would show a dialog appropriate for the compliance requirements
  }
  
  static Future<void> _initializeWithDefaultSettings() async {
    FlutterUxConfig config = FlutterUxConfig(
      userAppKey: "YOUR_APP_KEY",
      enableAutomaticScreenNameTagging: true,
      enableCrashHandling: true,
    );
    
    await FlutterUxcam.startWithConfiguration(config);
  }
}

class ComplianceRequirements {
  final bool requiresExplicitConsent;
  final bool allowsOptOut;
  final bool requiresDataDeletion;
  final List<String> requiredDisclosures;
  
  ComplianceRequirements({
    required this.requiresExplicitConsent,
    required this.allowsOptOut,
    required this.requiresDataDeletion,
    required this.requiredDisclosures,
  });
  
  factory ComplianceRequirements.gdpr() {
    return ComplianceRequirements(
      requiresExplicitConsent: true,
      allowsOptOut: true,
      requiresDataDeletion: true,
      requiredDisclosures: [
        'What data we collect',
        'How we use your data',
        'Your rights under GDPR',
        'How to contact us',
      ],
    );
  }
  
  factory ComplianceRequirements.ccpa() {
    return ComplianceRequirements(
      requiresExplicitConsent: false,
      allowsOptOut: true,
      requiresDataDeletion: true,
      requiredDisclosures: [
        'Categories of data collected',
        'Business purposes for collection',
        'Right to opt-out',
        'Right to delete',
      ],
    );
  }
  
  factory ComplianceRequirements.minimal() {
    return ComplianceRequirements(
      requiresExplicitConsent: false,
      allowsOptOut: true,
      requiresDataDeletion: false,
      requiredDisclosures: [
        'Data collection notice',
      ],
    );
  }
}

Consent Persistence and Migration

Consent Data Migration

class ConsentMigrationManager {
  static Future<void> migrateConsentData() async {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final currentVersion = prefs.getInt('consent_data_version') ?? 0;
    
    if (currentVersion < 1) {
      await _migrateToV1();
    }
    if (currentVersion < 2) {
      await _migrateToV2();
    }
    
    // Update version
    await prefs.setInt('consent_data_version', 2);
  }
  
  static Future<void> _migrateToV1() async {
    // Migration from legacy boolean consent to granular consent
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final legacyConsent = prefs.getBool('uxcam_user_consent') ?? false;
    
    if (legacyConsent) {
      // Migrate to new granular system with all permissions
      await GranularConsentManager.setConsents({
        GranularConsentManager.ConsentType.analytics: true,
        GranularConsentManager.ConsentType.crashReporting: true,
        GranularConsentManager.ConsentType.performanceMonitoring: true,
      });
    }
    
    // Remove legacy key
    await prefs.remove('uxcam_user_consent');
  }
  
  static Future<void> _migrateToV2() async {
    // Add new consent types or update existing ones
    // This is where you'd handle future consent migrations
  }
}

Settings Integration

Privacy Settings Screen

class PrivacySettingsScreen extends StatefulWidget {
  @override
  _PrivacySettingsScreenState createState() => _PrivacySettingsScreenState();
}

class _PrivacySettingsScreenState extends State<PrivacySettingsScreen> {
  Map<GranularConsentManager.ConsentType, bool> _consents = {};
  bool _loading = true;
  
  @override
  void initState() {
    super.initState();
    _loadConsents();
  }
  
  Future<void> _loadConsents() async {
    final consents = await GranularConsentManager.getAllConsents();
    setState(() {
      _consents = consents;
      _loading = false;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Privacy Settings'),
      ),
      body: _loading
          ? Center(child: CircularProgressIndicator())
          : ListView(
              padding: EdgeInsets.all(16),
              children: [
                Text(
                  'Data Collection Preferences',
                  style: Theme.of(context).textTheme.headline6,
                ),
                SizedBox(height: 16),
                _buildConsentTile(
                  GranularConsentManager.ConsentType.analytics,
                  'Analytics & Usage Data',
                  'Help us understand how you use the app to improve your experience',
                  Icons.analytics,
                ),
                _buildConsentTile(
                  GranularConsentManager.ConsentType.crashReporting,
                  'Crash Reports',
                  'Automatically report crashes to help us fix bugs',
                  Icons.bug_report,
                ),
                _buildConsentTile(
                  GranularConsentManager.ConsentType.performanceMonitoring,
                  'Performance Monitoring',
                  'Monitor app performance to optimize your experience',
                  Icons.speed,
                ),
                SizedBox(height: 32),
                _buildActionButtons(),
              ],
            ),
    );
  }
  
  Widget _buildConsentTile(
    GranularConsentManager.ConsentType type,
    String title,
    String description,
    IconData icon,
  ) {
    return Card(
      child: SwitchListTile(
        title: Text(title),
        subtitle: Text(description),
        secondary: Icon(icon),
        value: _consents[type] ?? false,
        onChanged: (bool value) async {
          setState(() {
            _consents[type] = value;
          });
          
          await GranularConsentManager.setConsentFor(type, value);
        },
      ),
    );
  }
  
  Widget _buildActionButtons() {
    return Column(
      children: [
        SizedBox(
          width: double.infinity,
          child: ElevatedButton(
            onPressed: _exportData,
            child: Text('Export My Data'),
          ),
        ),
        SizedBox(height: 8),
        SizedBox(
          width: double.infinity,
          child: ElevatedButton(
            onPressed: _deleteData,
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            child: Text('Delete My Data'),
          ),
        ),
      ],
    );
  }
  
  Future<void> _exportData() async {
    // Implement data export functionality
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Data export requested')),
    );
  }
  
  Future<void> _deleteData() async {
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Delete All Data'),
        content: Text('This will permanently delete all your data. This cannot be undone.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.of(context).pop(true),
            style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
            child: Text('Delete'),
          ),
        ],
      ),
    );
    
    if (confirmed == true) {
      await GranularConsentManager.clearAllConsents();
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('All data deleted')),
      );
    }
  }
}

Best Practices

Do's

  • ✅ Always check consent before initializing UXCam
  • ✅ Provide clear, understandable consent language
  • ✅ Allow users to change their preferences anytime
  • ✅ Implement granular consent controls
  • ✅ Handle consent migration properly
  • ✅ Respect regional compliance requirements

Don'ts

  • ❌ Don't collect data without proper consent
  • ❌ Don't make consent mandatory for app functionality
  • ❌ Don't use confusing or misleading consent language
  • ❌ Don't ignore user consent preferences
  • ❌ Don't forget to handle consent expiration
  • ❌ Don't skip regional compliance considerations

Next Steps


Proper consent management builds user trust while keeping you compliant with privacy regulations.