generate method
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();
}