part of '../settings_modal.dart'; class _SasVerificationDialog extends ConsumerStatefulWidget { final PyramidTheme pt; final String deviceName; final DeviceKeys device; const _SasVerificationDialog({ required this.pt, required this.deviceName, required this.device, }); @override ConsumerState<_SasVerificationDialog> createState() => _SasVerificationDialogState(); } class _SasVerificationDialogState extends ConsumerState<_SasVerificationDialog> { KeyVerification? _verification; KeyVerificationState _state = KeyVerificationState.waitingAccept; bool _starting = true; String? _error; @override void initState() { super.initState(); _startVerification(); } @override void dispose() { final v = _verification; if (v != null && !v.isDone) { v.cancel('m.user').catchError((_) {}); } super.dispose(); } Future _startVerification() async { try { final client = await ref.read(matrixClientProvider.future); final deviceId = widget.device.deviceId; if (deviceId == null) throw Exception('Keine Geräte-ID'); final deviceKeys = client.userDeviceKeys[client.userID]?.deviceKeys[deviceId]; if (deviceKeys == null) throw Exception('Gerät nicht gefunden'); final verification = await deviceKeys.startVerification(); _verification = verification; verification.onUpdate = () { if (!mounted) return; setState(() => _state = verification.state); }; if (mounted) { setState(() { _state = verification.state; _starting = false; }); } } catch (e) { if (mounted) { setState(() { _error = e.toString().split('\n').first; _starting = false; }); } } } @override Widget build(BuildContext context) { final pt = widget.pt; return AlertDialog( backgroundColor: pt.bg2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(pt.rXl), side: BorderSide(color: pt.border)), contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0), title: Row( children: [ Icon(Icons.verified_user_outlined, size: 20, color: pt.accent), const SizedBox(width: 10), Expanded( child: Text( 'Gerät verifizieren', style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700), ), ), ], ), content: SizedBox( width: 380, child: _buildContent(pt), ), actions: _buildActions(pt), ); } Widget _buildContent(PyramidTheme pt) { if (_error != null) { return Padding( padding: const EdgeInsets.only(bottom: 16), child: _StatusCard( pt: pt, icon: Icons.error_outline_rounded, color: pt.danger, text: _error!), ); } if (_starting) { return const Padding( padding: EdgeInsets.symmetric(vertical: 24), child: Center(child: CircularProgressIndicator.adaptive()), ); } final v = _verification; return switch (_state) { KeyVerificationState.waitingAccept || KeyVerificationState.askAccept => Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator.adaptive(), const SizedBox(height: 16), Text( 'Warte auf Bestätigung von „${widget.deviceName}"…', textAlign: TextAlign.center, style: TextStyle(color: pt.fgMuted, fontSize: 13), ), const SizedBox(height: 8), Text( 'Öffne Pyramid auf dem anderen Gerät und bestätige die Verifikationsanfrage.', textAlign: TextAlign.center, style: TextStyle(color: pt.fgDim, fontSize: 12), ), ], ), ), KeyVerificationState.askChoice => Padding( padding: const EdgeInsets.only(bottom: 8), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Das andere Gerät hat die Anfrage akzeptiert.\nWähle eine Verifikationsmethode:', textAlign: TextAlign.center, style: TextStyle(color: pt.fgMuted, fontSize: 13), ), const SizedBox(height: 16), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: pt.accent, foregroundColor: pt.accentFg, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(vertical: 12), ), onPressed: () => v?.continueVerification('m.sas.v1').catchError((_) {}), icon: const Icon(Icons.tag_faces_rounded, size: 18), label: const Text('Emoji-Verifikation'), ), const SizedBox(height: 8), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: pt.bg3, foregroundColor: pt.fg, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(vertical: 12), ), onPressed: () => v?.continueVerification('m.qr_code.show.v1').catchError((_) {}), icon: const Icon(Icons.qr_code_rounded, size: 18), label: const Text('QR-Code anzeigen'), ), const SizedBox(height: 8), ], ), ), KeyVerificationState.confirmQRScan => _OutgoingQrCodeDisplay( pt: pt, request: v, ), KeyVerificationState.showQRSuccess => Padding( padding: const EdgeInsets.only(bottom: 16), child: _StatusCard( pt: pt, icon: Icons.verified_rounded, color: PyramidColors.success, text: 'QR-Code erfolgreich gescannt. Verifikation abgeschlossen.'), ), KeyVerificationState.askSas => _SasEmojiContent( pt: pt, verification: v!, onConfirm: () => v.acceptSas().catchError((_) {}), onReject: () { v.rejectSas().catchError((_) {}); Navigator.of(context).pop(); }, ), KeyVerificationState.waitingSas => Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator.adaptive(), const SizedBox(height: 16), Text( 'Warte auf Bestätigung des anderen Geräts…', textAlign: TextAlign.center, style: TextStyle(color: pt.fgMuted, fontSize: 13), ), ], ), ), KeyVerificationState.done => Padding( padding: const EdgeInsets.only(bottom: 16), child: _StatusCard( pt: pt, icon: Icons.verified_rounded, color: PyramidColors.success, text: '„${widget.deviceName}" wurde erfolgreich verifiziert.'), ), _ => Padding( padding: const EdgeInsets.only(bottom: 16), child: _StatusCard( pt: pt, icon: Icons.error_outline_rounded, color: pt.danger, text: 'Verifizierung fehlgeschlagen oder abgebrochen.'), ), }; } List _buildActions(PyramidTheme pt) { if (_state == KeyVerificationState.done || _state == KeyVerificationState.showQRSuccess || _error != null || _state == KeyVerificationState.error) { return [ ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: pt.accent, foregroundColor: pt.accentFg, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), onPressed: () => Navigator.pop(context), child: const Text('Schließen'), ), ]; } if (_state == KeyVerificationState.confirmQRScan) { return [ TextButton( onPressed: () { _verification?.cancel('m.user').catchError((_) {}); Navigator.pop(context); }, child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted)), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF22C55E), foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), onPressed: () => _verification?.acceptQRScanConfirmation().catchError((_) {}), child: const Text('Gescannt bestätigen'), ), ]; } if (_state == KeyVerificationState.askSas || _state == KeyVerificationState.askChoice) { return []; } return [ TextButton( onPressed: () { _verification?.cancel('m.user').catchError((_) {}); Navigator.pop(context); }, child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted)), ), ]; } } class _OutgoingQrCodeDisplay extends StatelessWidget { final PyramidTheme pt; final KeyVerification? request; const _OutgoingQrCodeDisplay({required this.pt, required this.request}); @override Widget build(BuildContext context) { final qrCode = request?.qrCode; if (qrCode == null) { return Padding( padding: const EdgeInsets.symmetric(vertical: 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator.adaptive(), const SizedBox(height: 12), Text('QR-Code wird generiert…', style: TextStyle(color: pt.fgMuted, fontSize: 13)), ], ), ); } try { final rawBytes = Uint8List.fromList(qrCode.qrDataRawBytes.toList()); final qr = QrCode.fromUint8List(data: rawBytes, errorCorrectLevel: QrErrorCorrectLevel.L); final qrImage = QrImage(qr); return Padding( padding: const EdgeInsets.only(bottom: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Scanne diesen QR-Code mit dem anderen Gerät', textAlign: TextAlign.center, style: TextStyle(color: pt.fgMuted, fontSize: 13), ), const SizedBox(height: 16), Center( child: Container( width: 220, height: 220, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: PrettyQrView(qrImage: qrImage), ), ), const SizedBox(height: 12), Text( 'Sobald das andere Gerät den Code gescannt hat, tippe auf „Gescannt bestätigen".', textAlign: TextAlign.center, style: TextStyle(color: pt.fgDim, fontSize: 12), ), const SizedBox(height: 8), ], ), ); } catch (_) { return Padding( padding: const EdgeInsets.only(bottom: 16), child: _StatusCard( pt: pt, icon: Icons.error_outline_rounded, color: pt.danger, text: 'QR-Code konnte nicht generiert werden.', ), ); } } } class _SasEmojiContent extends StatelessWidget { final PyramidTheme pt; final KeyVerification verification; // ignore: library_private_types_in_public_api final VoidCallback onConfirm; final VoidCallback onReject; const _SasEmojiContent({ required this.pt, required this.verification, required this.onConfirm, required this.onReject, }); @override Widget build(BuildContext context) { final emojis = verification.sasEmojis; return Padding( padding: const EdgeInsets.only(bottom: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Vergleiche die Emojis auf beiden Geräten', textAlign: TextAlign.center, style: TextStyle(color: pt.fgMuted, fontSize: 13), ), const SizedBox(height: 20), if (emojis.isNotEmpty) Wrap( spacing: 12, runSpacing: 12, alignment: WrapAlignment.center, children: emojis.map((e) => _SasEmojiChip(emoji: e, pt: pt)).toList(), ) else Text('Keine Emojis verfügbar.', style: TextStyle(color: pt.fgDim, fontSize: 13)), const SizedBox(height: 24), Text( 'Stimmen alle Emojis überein?', style: TextStyle(color: pt.fg, fontSize: 14, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), Row( children: [ Expanded( child: OutlinedButton.icon( style: OutlinedButton.styleFrom( foregroundColor: pt.danger, side: BorderSide(color: pt.danger.withAlpha(120)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(vertical: 12), ), onPressed: onReject, icon: const Icon(Icons.close_rounded, size: 16), label: const Text('Stimmt nicht überein'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: PyramidColors.success, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), padding: const EdgeInsets.symmetric(vertical: 12), ), onPressed: onConfirm, icon: const Icon(Icons.check_rounded, size: 16), label: const Text('Stimmt überein'), ), ), ], ), const SizedBox(height: 8), ], ), ); } } class _SasEmojiChip extends StatelessWidget { final KeyVerificationEmoji emoji; final PyramidTheme pt; const _SasEmojiChip({required this.emoji, required this.pt}); @override Widget build(BuildContext context) { return Container( width: 68, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), decoration: BoxDecoration( color: pt.bg3, borderRadius: BorderRadius.circular(10), border: Border.all(color: pt.border), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(emoji.emoji, style: const TextStyle(fontSize: 26)), const SizedBox(height: 4), Text( emoji.name, style: TextStyle(color: pt.fgMuted, fontSize: 10), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ); } }