Flutter SDK - 数据隐私

文件

遵守数据隐私法

通过通知Singular用户同意选择GDPR、CCPA、COPPA和其他消费者隐私法规,实施符合隐私法规的数据收集。

当用户同意或拒绝与第三方共享其信息时,使用 Singular 的隐私方法来传达他们的选择。这将确保遵守《加州消费者隐私法案》(CCPA)等法规,并使合作伙伴尊重用户的隐私偏好。

了解更多信息:有关Singular如何处理隐私同意的详细信息,请参阅用户隐私和限制数据共享


限制数据共享

控制第三方数据共享

通知Singular用户是否同意使用limitDataSharing() 方法与第三方合作伙伴共享个人数据。

方法签名

static void limitDataSharing(bool shouldLimitDataSharing)

参数

  • false:用户已选择并同意共享其数据
  • true:用户已退出,不同意共享其数据

重要:虽然此方法是可选的,但它会影响属性数据的共享。有些合作伙伴只有在明确通知用户已选择加入时,才会共享完整的归因信息。

有关完整的方法文档,请参阅limitDataSharing 参考资料


使用示例

根据用户隐私偏好实施数据共享控制。

Dart
import 'package:singular_flutter_sdk/singular.dart';

// User has opted in to share their data
void onUserOptedInToDataSharing() {
  Singular.limitDataSharing(false);
  print('Data sharing enabled');
}

// User has opted out and declined to share their data
void onUserOptedOutOfDataSharing() {
  Singular.limitDataSharing(true);
  print('Data sharing limited');
}

// Set based on user preference
void handlePrivacyConsent(bool userConsented) {
  // Pass inverse: false = opted in, true = opted out
  Singular.limitDataSharing(!userConsented);

  print('Data sharing: ${userConsented ? 'Enabled' : 'Limited'}');
}

如何使用

Singular在用户隐私回邮中使用此设置,并将其传递给需要遵守法规的合作伙伴。


GDPR 合规方法

管理用户跟踪同意和控制 SDK 功能,以符合 GDPR(通用数据保护条例)和其他隐私法规。

跟踪同意管理

跟踪同意

通过向 Singular 服务器发送 GDPR 选入事件,记录用户对跟踪的明确同意。

方法签名

static void trackingOptIn()

何时使用

  • GDPR 合规性:当用户明确同意在受 GDPR 监管的地区进行跟踪时调用
  • 同意记录:在 Singular 系统中将用户标记为已提供 GDPR 同意
  • 默认行为:如果没有此调用,SDK 会继续跟踪,但不会明确记录同意信息

有关完整的方法文档,请参阅trackingOptIn 参考资料


实现示例

在用户通过应用程序的同意对话框接受跟踪同意后调用trackingOptIn()

Dart
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:shared_preferences/shared_preferences.dart';

class GDPRManager extends StatefulWidget {
  @override
  _GDPRManagerState createState() => _GDPRManagerState();
}

class _GDPRManagerState extends State<GDPRManager> {
  bool? hasConsent;

  @override
  void initState() {
    super.initState();
    checkStoredConsent();
  }

  Future<void> checkStoredConsent() async {
    final prefs = await SharedPreferences.getInstance();
    final consent = prefs.getString('gdpr_consent');

    if (consent == null) {
      // Show consent dialog
      showGDPRConsentDialog();
    } else {
      setState(() {
        hasConsent = consent == 'true';
      });
    }
  }

  void showGDPRConsentDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: Text('Privacy Consent'),
        content: Text(
          'We would like your permission to track app usage and improve your experience.'
        ),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              setState(() {
                hasConsent = false;
              });
            },
            child: Text('Decline'),
          ),
          ElevatedButton(
            onPressed: () async {
              Navigator.of(context).pop();
              await onUserAcceptedTracking();
            },
            child: Text('Accept'),
          ),
        ],
      ),
    );
  }

  Future<void> onUserAcceptedTracking() async {
    // Record user consent
    Singular.trackingOptIn();
    print('User opted in to tracking');

    // Save preference
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('gdpr_consent', 'true');

    setState(() {
      hasConsent = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: hasConsent != null
            ? Text('Privacy Status: ${hasConsent! ? 'Opted In' : 'Declined'}')
            : CircularProgressIndicator(),
      ),
    );
  }
}

跟踪控制方法

停止所有跟踪

完全禁用当前用户在此设备上的所有 SDK 跟踪活动。

方法签名

static void stopAllTracking()

严重警告:此方法会永久禁用 SDK,直到调用resumeAllTracking() 。禁用状态会在应用程序重启时持续存在,只能通过编程方式逆转。

行为

  • 立即生效:立即停止所有跟踪、事件报告和数据收集功能
  • 持续状态:即使在应用程序关闭和重新打开后也会保持禁用状态
  • 无法自动重置:必须明确调用resumeAllTracking() 才能重新启用

有关完整的方法文档,请参阅stopAllTracking 参考资料


实施示例

当用户拒绝同意或通过隐私设置选择退出时停止跟踪。

Dart
import 'package:singular_flutter_sdk/singular.dart';
import 'package:shared_preferences/shared_preferences.dart';

// User declined all tracking
Future<void> onUserDeclinedTracking() async {
  Singular.stopAllTracking();
  print('All tracking stopped');

  // Store preference
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('tracking_enabled', false);
}

// Handle user opt-out from settings menu
void handlePrivacySettingsChange(bool trackingEnabled) {
  if (!trackingEnabled) {
    Singular.stopAllTracking();
    print('Privacy settings: Tracking disabled');
  }
}

恢复全部跟踪

在使用stopAllTracking() 停止跟踪后重新启用跟踪。

方法签名

static void resumeAllTracking()

使用案例

  • 同意变更:用户更改隐私偏好并选择恢复跟踪
  • 隐私设置:用户通过应用程序设置菜单更新同意
  • 地区合规性:当用户转移到非监管区域时重新启用跟踪功能

有关完整的方法文档,请参阅resumeAllTracking 参考资料


实施示例

当用户重新选择或更新隐私偏好时恢复跟踪。

Dart
import 'package:singular_flutter_sdk/singular.dart';
import 'package:shared_preferences/shared_preferences.dart';

// User opted back in to tracking
Future<void> onUserResumedTracking() async {
  Singular.resumeAllTracking();
  print('Tracking resumed');

  // Update stored preference
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool('tracking_enabled', true);
}

// Handle consent update from settings
void handlePrivacySettingsChange(bool trackingEnabled) {
  if (trackingEnabled) {
    Singular.resumeAllTracking();
    print('Privacy settings: Tracking enabled');
  }
}

IsAllTrackingStopped

检查当前用户的跟踪是否已被禁用。

方法签名

static Future<bool> isAllTrackingStopped()

返回

  • Future<true>(未来<true>):当前已通过stopAllTracking()停止跟踪
  • Future<false>:跟踪处于激活状态:跟踪处于活动状态(从未停止或恢复

有关该方法的完整文档,请参阅isAllTrackingStopped 参考文献


实现示例

检查跟踪状态,使 UI 状态与 SDK 跟踪状态同步。

Dart
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';

class PrivacySettingsUI extends StatefulWidget {
  @override
  _PrivacySettingsUIState createState() => _PrivacySettingsUIState();
}

class _PrivacySettingsUIState extends State<PrivacySettingsUI> {
  bool trackingEnabled = false;
  String statusText = 'Checking...';

  @override
  void initState() {
    super.initState();
    updatePrivacyUI();
  }

  // Check current tracking status
  Future<bool> isTrackingEnabled() async {
    final isStopped = await Singular.isAllTrackingStopped();
    return !isStopped;
  }

  // Display privacy status in settings
  Future<String> getPrivacyStatusText() async {
    final isStopped = await Singular.isAllTrackingStopped();
    return isStopped ? 'Tracking: Disabled' : 'Tracking: Enabled';
  }

  // Sync UI with tracking state
  Future<void> updatePrivacyUI() async {
    final isStopped = await Singular.isAllTrackingStopped();
    final status = await getPrivacyStatusText();

    setState(() {
      trackingEnabled = !isStopped;
      statusText = status;
    });

    print('Current tracking state: ${isStopped ? 'Stopped' : 'Active'}');
  }

  // Handle toggle change from UI
  Future<void> onTrackingToggleChanged(bool enabled) async {
    if (enabled) {
      Singular.resumeAllTracking();
    } else {
      Singular.stopAllTracking();
    }

    await updatePrivacyUI();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(statusText),
        Switch(
          value: trackingEnabled,
          onChanged: onTrackingToggleChanged,
        ),
      ],
    );
  }
}

儿童隐私保护

跟踪13岁以下儿童

通知 Singular 用户未满 13 周岁,以遵守 COPPA(儿童在线隐私保护法案)和其他儿童隐私法规。

方法签名

static void trackingUnder13()

合规要求

  • COPPA 合规性:在美国收集 13 岁以下儿童数据的应用程序必须遵守
  • 有年龄限制的内容:当用户在注册或年龄验证时表明自己未满 13 岁时使用
  • 限制跟踪:限制数据收集,以遵守儿童隐私保护法

有关完整的方法文档,请参阅trackingUnder13 参考资料


实施示例

通过年龄验证确定用户未满 13 岁后,立即调用trackingUnder13()

Dart
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:shared_preferences/shared_preferences.dart';

class COPPAManager extends StatefulWidget {
  @override
  _COPPAManagerState createState() => _COPPAManagerState();
}

class _COPPAManagerState extends State<COPPAManager> {
  final TextEditingController ageController = TextEditingController();

  // User identified as under 13
  Future<void> onUserUnder13() async {
    Singular.trackingUnder13();
    print('COPPA mode enabled for user under 13');

    // Store age category for app restart
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('user_age_category', 'under_13');
  }

  // Call after age verification
  Future<void> onAgeVerified(int userAge) async {
    final prefs = await SharedPreferences.getInstance();

    if (userAge < 13) {
      Singular.trackingUnder13();
      print('COPPA restrictions applied');

      // Also limit advertising identifiers
      Singular.limitDataSharing(true);

      await prefs.setString('user_age_category', 'under_13');

      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('Child Account'),
          content: Text('Special privacy protections have been applied.'),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text('OK'),
            ),
          ],
        ),
      );
    } else {
      await prefs.setString('user_age_category', 'adult');
      print('Adult account - standard tracking');
    }
  }

  void handleAgeSubmit() {
    final userAge = int.tryParse(ageController.text);

    if (userAge == null || userAge < 1 || userAge > 120) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: Text('Invalid Age'),
          content: Text('Please enter a valid age.'),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: Text('OK'),
            ),
          ],
        ),
      );
      return;
    }

    onAgeVerified(userAge);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: ageController,
          decoration: InputDecoration(
            hintText: 'Enter your age',
          ),
          keyboardType: TextInputType.number,
        ),
        ElevatedButton(
          onPressed: handleAgeSubmit,
          child: Text('Submit'),
        ),
      ],
    );
  }

  @override
  void dispose() {
    ageController.dispose();
    super.dispose();
  }
}

重要:在确定用户未满 13 岁后,应尽早调用此方法,最好是在应用程序初始化期间或年龄验证后立即调用。这样可确保所有后续跟踪都遵守儿童隐私法规。


限制广告标识符配置

在 SDK 初始化期间,限制收集和使用服务于儿童的应用程序的广告标识符(Android 上为 GAID,iOS 上为 IDFA)。

配置属性

bool? limitAdvertisingIdentifiers

参数

  • true:启用限制广告标识符模式(限制收集
  • false:禁用限制广告标识符模式:禁用有限广告标识符模式(正常收集

使用案例

  • 儿童应用程序:主要为 13 岁以下用户设计的应用程序
  • 混合受众应用程序:同时为成人和儿童服务的应用程序,年龄在 SDK 初始化前确定
  • 隐私优先应用程序:从一开始就应用广告标识符限制

有关完整的配置文档,请参阅limitAdvertisingIdentifiers 参考资料


配置示例

在 SDK 初始化期间为预先知道隐私要求的应用程序设置广告标识符限制。

Dart
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';

// Limit advertising identifiers at initialization
void initializeSingular() {
  final config = SingularConfig(
    'YOUR_SDK_KEY',
    'YOUR_SDK_SECRET'
  );

  // Enable limited advertising identifiers mode
  config.limitAdvertisingIdentifiers = true;

  Singular.start(config);
  print('SDK initialized with advertising identifier restrictions');
}

组合方法:对于为儿童提供服务的应用程序,将trackingUnder13()limitAdvertisingIdentifiers = true 结合使用,以确保全面遵守 COPPA。


实施最佳实践

完整隐私管理示例

实施尊重用户偏好并符合法规的全面隐私控制。

Dart
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:shared_preferences/shared_preferences.dart';

const String PREF_USER_CONSENT = 'privacy_user_consent';
const String PREF_DATA_SHARING = 'privacy_data_sharing';
const String PREF_AGE_CATEGORY = 'user_age_category';

/// Initialize privacy settings on app startup based on stored preferences
Future<void> initializePrivacySettings() async {
  final hasUserConsent = await getUserConsent();
  final allowDataSharing = await getDataSharingPreference();
  final ageCategory = await getAgeCategory();

  // Apply age-based restrictions first
  if (ageCategory == 'under_13') {
    Singular.trackingUnder13();
    print('COPPA restrictions applied on startup');
  }

  // Apply stored tracking preference
  if (hasUserConsent) {
    Singular.trackingOptIn();
    Singular.resumeAllTracking();
    print('Privacy initialized: Tracking enabled with consent');
  } else {
    Singular.stopAllTracking();
    print('Privacy initialized: Tracking disabled');
  }

  // Set data sharing preference (inverse logic)
  Singular.limitDataSharing(!allowDataSharing);

  print('Privacy initialized: consent=$hasUserConsent, sharing=$allowDataSharing');
}

/// User accepts tracking via consent dialog
Future<void> onUserAcceptedTracking() async {
  await saveUserConsent(true);

  Singular.trackingOptIn();
  Singular.resumeAllTracking();

  print('User accepted tracking');
}

/// User declines tracking
Future<void> onUserDeclinedTracking() async {
  await saveUserConsent(false);

  Singular.stopAllTracking();

  print('User declined tracking');
}

/// User updates data sharing preference
Future<void> setDataSharingEnabled(bool enabled) async {
  await saveDataSharingPreference(enabled);

  // Note: limitDataSharing uses inverse logic
  // false = data sharing enabled, true = data sharing limited
  Singular.limitDataSharing(!enabled);

  print('Data sharing: ${enabled ? 'Enabled' : 'Limited'}');
}

/// Check if tracking is currently enabled
Future<bool> isTrackingEnabled() async {
  final isStopped = await Singular.isAllTrackingStopped();
  return !isStopped;
}

/// Get current privacy status as readable text
Future<String> getPrivacyStatus() async {
  final isEnabled = await isTrackingEnabled();
  final dataSharingEnabled = await getDataSharingPreference();
  final ageCategory = await getAgeCategory();

  String status = 'Tracking: ${isEnabled ? 'Enabled' : 'Disabled'}
';
  status += 'Data Sharing: ${dataSharingEnabled ? 'Enabled' : 'Limited'}';

  if (ageCategory == 'under_13') {
    status += '
COPPA: Restrictions Applied';
  }

  return status;
}

// Private helper methods for SharedPreferences

Future<bool> getUserConsent() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getBool(PREF_USER_CONSENT) ?? false;
}

Future<void> saveUserConsent(bool consent) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool(PREF_USER_CONSENT, consent);
}

Future<bool> getDataSharingPreference() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getBool(PREF_DATA_SHARING) ?? false;
}

Future<void> saveDataSharingPreference(bool enabled) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setBool(PREF_DATA_SHARING, enabled);
}

Future<String?> getAgeCategory() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString(PREF_AGE_CATEGORY);
}

// Flutter widget to initialize privacy on app startup
class PrivacyInitializer extends StatefulWidget {
  final Widget child;

  const PrivacyInitializer({Key? key, required this.child}) : super(key: key);

  @override
  _PrivacyInitializerState createState() => _PrivacyInitializerState();
}

class _PrivacyInitializerState extends State<PrivacyInitializer> {
  @override
  void initState() {
    super.initState();
    initializePrivacySettings();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

关键实施指南

最佳实践:

  • 持久存储:使用共享首选项或安全存储解决方案保存用户首选项
  • 尽早初始化:尽可能在 SDK 初始化之前应用隐私设置
  • UI 同步:使用isAllTrackingStopped()保持设置 UI 与实际 SDK 状态同步
  • 清晰沟通:在应用设置中提供清晰、易用的隐私控制
  • 反向逻辑:请记住,limitDataSharing(false) 意味着启用数据共享,而true 则意味着限制数据共享
  • COPPA 优先级:在其他隐私设置之前应用儿童隐私保护 (trackingUnder13())
  • 合规文档:保留用户何时以及如何提供或撤销同意的记录