import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:userfront/i18n.dart'; import 'qr_scan_route.dart'; class QRScanScreen extends StatefulWidget { const QRScanScreen({super.key}); @override State createState() => _QRScanScreenState(); } class _QRScanScreenState extends State { final MobileScannerController _scannerController = MobileScannerController( autoStart: true, detectionSpeed: DetectionSpeed.noDuplicates, facing: CameraFacing.back, formats: const [BarcodeFormat.qrCode], ); final TextEditingController _manualController = TextEditingController(); bool _isProcessing = false; String? _error; String? _status; @override void initState() { super.initState(); _status = tr( 'msg.userfront.login.qr.scan_hint', fallback: 'QR 코드를 카메라 중앙에 맞춰주세요.', ); } @override void dispose() { _manualController.dispose(); _scannerController.dispose(); super.dispose(); } Future _navigateToApprove(String rawPayload) async { final payload = rawPayload.trim(); if (payload.isEmpty || _isProcessing || !mounted) { return; } setState(() { _isProcessing = true; _error = null; _status = tr( 'ui.userfront.qr.result_success', fallback: '승인 화면으로 이동 중...', ); }); try { await _scannerController.stop(); } catch (_) {} if (!mounted) { return; } context.go(buildQrApprovePath(payload)); } void _onDetect(BarcodeCapture capture) { for (final barcode in capture.barcodes) { final raw = barcode.rawValue?.trim(); if (raw != null && raw.isNotEmpty) { unawaited(_navigateToApprove(raw)); return; } } } String _toScannerErrorMessage(MobileScannerException error) { switch (error.errorCode) { case MobileScannerErrorCode.permissionDenied: return tr( 'msg.userfront.qr.permission_error', fallback: '카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.', ); case MobileScannerErrorCode.unsupported: return tr( 'msg.userfront.qr.camera_error', fallback: '카메라 오류: {{error}}', params: {'error': 'QR scanner is not supported in this browser.'}, ); default: final detail = error.errorDetails?.message; return tr( 'msg.userfront.qr.camera_error', fallback: '카메라 오류: {{error}}', params: {'error': detail ?? error.errorCode.message}, ); } } void _submitManual() { unawaited(_navigateToApprove(_manualController.text)); } Future _retry() async { setState(() { _isProcessing = false; _error = null; _status = tr( 'msg.userfront.login.qr.scan_hint', fallback: 'QR 코드를 카메라 중앙에 맞춰주세요.', ); }); try { await _scannerController.start(); } catch (e) { if (!mounted) { return; } setState(() { _error = tr( 'msg.userfront.qr.camera_error', fallback: '카메라 오류: {{error}}', params: {'error': '$e'}, ); }); } } void _handleBack() { final router = GoRouter.of(context); if (router.canPop()) { router.pop(); return; } router.go(buildQrBackFallbackPath()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(tr('ui.userfront.qr.title', fallback: 'Scan QR Code')), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: _handleBack, ), ), body: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AspectRatio( aspectRatio: 3 / 4, child: DecoratedBox( decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(12), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Stack( fit: StackFit.expand, children: [ MobileScanner( controller: _scannerController, onDetect: _onDetect, errorBuilder: (context, error) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; } setState(() { _error = _toScannerErrorMessage(error); }); }); return Center( child: Padding( padding: const EdgeInsets.all(16), child: Text( _toScannerErrorMessage(error), textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), ), ); }, ), if (_isProcessing) Container( color: Colors.black45, child: const Center( child: CircularProgressIndicator(), ), ), ], ), ), ), ), const SizedBox(height: 12), if (_status != null) Text(_status!, textAlign: TextAlign.center), if (_error != null) ...[ const SizedBox(height: 8), Text( _error!, textAlign: TextAlign.center, style: const TextStyle(color: Colors.red), ), ], const SizedBox(height: 12), FilledButton.icon( onPressed: _isProcessing ? null : _retry, icon: const Icon(Icons.refresh), label: Text(tr('ui.userfront.qr.rescan', fallback: '다시 스캔')), ), const SizedBox(height: 12), TextField( key: const ValueKey('qr_scan_manual_input'), controller: _manualController, decoration: const InputDecoration( labelText: 'QR Payload', hintText: 'https://.../ql/{ref} 또는 ref', ), onSubmitted: (_) => _submitManual(), ), const SizedBox(height: 8), FilledButton.icon( key: const ValueKey('qr_scan_submit_button'), onPressed: _isProcessing ? null : _submitManual, icon: const Icon(Icons.check_circle), label: Text( tr('ui.userfront.qr.result_success', fallback: '승인 화면으로 이동'), ), ), ], ), ), ); } }