import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import '../../core/api/api_config.dart';
import '../../core/text/wp_text.dart';
import '../../core/localization/app_localizations.dart';
import '../../services/analytics_service.dart';
import '../../widgets/analytics_view.dart';
enum LegalDocumentType { privacy, imprint }
class LegalDocumentPage extends StatefulWidget {
const LegalDocumentPage({super.key, required this.type});
final LegalDocumentType type;
@override
State<LegalDocumentPage> createState() => _LegalDocumentPageState();
}
class _LegalDocumentPageState extends State<LegalDocumentPage> {
late Future<_LegalDocument> _document;
@override
void initState() {
super.initState();
_document = _load();
}
String get _slug =>
widget.type == LegalDocumentType.privacy ? 'privacy' : 'imprint';
Future<_LegalDocument> _load() async {
final uri = Uri.parse('${ApiConfig.baseUrl}/legal/$_slug');
final response = await http.get(uri).timeout(ApiConfig.requestTimeout);
if (response.statusCode < 200 || response.statusCode >= 300) {
throw Exception('HTTP ${response.statusCode}');
}
final decoded = jsonDecode(utf8.decode(response.bodyBytes));
if (decoded is! Map<String, dynamic>) {
throw const FormatException('Invalid legal payload');
}
return _LegalDocument.fromJson(decoded);
}
void _retry() => setState(() => _document = _load());
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final title = widget.type == LegalDocumentType.privacy
? l10n.t('legalPrivacyTitle')
: l10n.t('legalImprintTitle');
return AnalyticsView(
event: widget.type == LegalDocumentType.privacy
? 'privacy_view'
: 'imprint_view',
child: Scaffold(
appBar: AppBar(
title: Text(title,
style: const TextStyle(fontWeight: FontWeight.w800))),
body: FutureBuilder<_LegalDocument>(
future: _document,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || !snapshot.hasData) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off_outlined, size: 42),
const SizedBox(height: 12),
Text(l10n.t('legalLoadError'),
textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: _retry,
icon: const Icon(Icons.refresh_rounded),
label: Text(l10n.t('retry')),
),
],
),
),
);
}
final doc = snapshot.data!;
return ListView(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 40),
children: [
...doc.blocks.map((block) => _LegalBlockWidget(block: block)),
if (doc.links.isNotEmpty) ...[
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 8),
...doc.links.map((link) => ListTile(
contentPadding: EdgeInsets.zero,
dense: true,
leading:
const Icon(Icons.open_in_new_rounded, size: 20),
title: Text(link.label),
onTap: () async {
final uri = Uri.tryParse(link.url);
if (uri == null) return;
AnalyticsService.instance
.track('legal_link_click', properties: {
'document': _slug,
'url': link.url,
});
await launchUrl(uri,
mode: LaunchMode.externalApplication);
},
)),
],
const SizedBox(height: 18),
Text(
l10n.t('legalSourceNote'),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: Colors.black54),
),
],
);
},
),
),
);
}
}
class _LegalBlockWidget extends StatelessWidget {
const _LegalBlockWidget({required this.block});
final _LegalBlock block;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (block.type == 'heading') {
final style = switch (block.level) {
1 => theme.textTheme.headlineMedium,
2 => theme.textTheme.headlineSmall,
_ => theme.textTheme.titleLarge,
};
return Padding(
padding: const EdgeInsets.only(top: 18, bottom: 8),
child: SelectableText(block.text,
style: style?.copyWith(fontWeight: FontWeight.w800)),
);
}
if (block.type == 'list_item') {
return Padding(
padding: const EdgeInsets.only(bottom: 7),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(padding: EdgeInsets.only(top: 1), child: Text('• ')),
Expanded(
child: SelectableText(block.text,
style: theme.textTheme.bodyMedium?.copyWith(height: 1.5))),
],
),
);
}
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: SelectableText(block.text,
style: theme.textTheme.bodyMedium?.copyWith(height: 1.55)),
);
}
}
class _LegalDocument {
const _LegalDocument({required this.blocks, required this.links});
final List<_LegalBlock> blocks;
final List<_LegalLink> links;
factory _LegalDocument.fromJson(Map<String, dynamic> json) {
final rawBlocks = json['blocks'];
final rawLinks = json['links'];
return _LegalDocument(
blocks: rawBlocks is List
? rawBlocks
.whereType<Map>()
.map((e) => _LegalBlock.fromJson(Map<String, dynamic>.from(e)))
.toList()
: const [],
links: rawLinks is List
? rawLinks
.whereType<Map>()
.map((e) => _LegalLink.fromJson(Map<String, dynamic>.from(e)))
.toList()
: const [],
);
}
}
class _LegalBlock {
const _LegalBlock({required this.type, required this.text, this.level = 0});
final String type;
final String text;
final int level;
factory _LegalBlock.fromJson(Map<String, dynamic> json) => _LegalBlock(
type: json['type']?.toString() ?? 'paragraph',
text: WpText.plain(json['text']),
level: int.tryParse(json['level']?.toString() ?? '') ?? 0,
);
}
class _LegalLink {
const _LegalLink({required this.label, required this.url});
final String label;
final String url;
factory _LegalLink.fromJson(Map<String, dynamic> json) => _LegalLink(
label: WpText.plain(json['label']),
url: WpText.url(json['url']) ?? '',
);
}

