generate method

String generate(
  1. List<SdkEntry> sdks, {
  2. ProjectInfo? project,
})

Implementation

String generate(List<SdkEntry> sdks, {ProjectInfo? project}) {
  final buffer = StringBuffer();
  buffer.writeln('# Privacy summary');
  buffer.writeln();
  if (project != null) {
    buffer.writeln('**App:** ${project.name} `${project.version}`');
    if (project.description != null &&
        project.description!.trim().isNotEmpty) {
      buffer.writeln();
      buffer.writeln(project.description!.trim());
    }
    buffer.writeln();
  }

  if (sdks.isEmpty) {
    buffer.writeln(
      'This app does not currently declare third-party SDK data '
      'collection through privacy_ledger. Review your own code and any '
      'SDKs not yet in the database before publishing a privacy policy.',
    );
    return buffer.toString();
  }

  // Group data types by purpose (not by SDK).
  final byPurpose = <String, Set<String>>{};
  final allTypes = <String>{};
  var anyShared = false;
  var anyEncrypted = true;
  var anyDeletion = true;

  for (final sdk in sdks) {
    if (!sdk.encryptedInTransit) anyEncrypted = false;
    if (!sdk.userCanRequestDeletion) anyDeletion = false;
    for (final item in sdk.dataCollected) {
      allTypes.add(item.type);
      byPurpose.putIfAbsent(item.purpose, () => <String>{}).add(item.type);
      if (item.sharedWithThirdParties) anyShared = true;
    }
  }

  buffer.write(
    'This app uses third-party services that help it run, measure usage, '
    'and (where applicable) show ads or process in-app purchases. ',
  );

  final purposeOrder = [
    'app_functionality',
    'analytics',
    'advertising',
    'fraud_prevention',
    'personalization',
    'developer_communications',
    'account_management',
  ];

  final sentences = <String>[];
  for (final purpose in purposeOrder) {
    final types = byPurpose[purpose];
    if (types == null || types.isEmpty) continue;
    final label = _purposeGroupLabels[purpose] ?? purpose;
    final phrases = types.map((t) => _dataTypePhrases[t] ?? t).toList()
      ..sort();
    sentences.add('For $label, it may collect ${_joinAnd(phrases)}');
  }
  // Any purposes not in the ordered list.
  for (final entry in byPurpose.entries) {
    if (purposeOrder.contains(entry.key)) continue;
    final label = _purposeGroupLabels[entry.key] ?? entry.key;
    final phrases = entry.value.map((t) => _dataTypePhrases[t] ?? t).toList()
      ..sort();
    sentences.add('For $label, it may collect ${_joinAnd(phrases)}');
  }

  if (sentences.isNotEmpty) {
    buffer.write(sentences.join('. '));
    buffer.write('. ');
  }

  if (anyShared) {
    buffer.write(
      'Some of this information is processed by third-party providers '
      'that power those features. ',
    );
  }

  if (anyEncrypted) {
    buffer.write('Data is transmitted using encryption in transit. ');
  } else {
    buffer.write(
      'Confirm with each provider whether data is encrypted in transit. ',
    );
  }

  if (anyDeletion) {
    buffer.write(
      'Where the providers support it, you can request deletion of data '
      'associated with your use of the app. ',
    );
  } else {
    buffer.write(
      'Deletion support varies by provider — see the full privacy policy '
      'for how to request deletion. ',
    );
  }

  buffer.writeln();
  buffer.writeln();
  buffer.writeln(
    '_This paragraph was generated by privacy_ledger from known SDK '
    'database entries. It is a starting draft for a hosted privacy policy, '
    'not a substitute for legal review._',
  );
  buffer.writeln();

  // Ensure we have a machine-checkable list of distinct types for tests.
  final sortedTypes = allTypes.toList()..sort();
  buffer.writeln('<!-- data_types: ${sortedTypes.join(', ')} -->');

  return buffer.toString();
}