import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import '../../../core/services/auth_proxy_service.dart'; import '../../../core/services/auth_token_store.dart'; class QRScanScreen extends StatefulWidget { const QRScanScreen({super.key}); @override State createState() => _QRScanScreenState(); } class _QRScanScreenState extends State { final _log = Logger('QRScanScreen'); final MobileScannerController controller = MobileScannerController( detectionSpeed: DetectionSpeed.noDuplicates, ); bool _isScanned = false; bool _isCheckingSession = false; bool _isProcessing = false; bool _isRequestingCamera = false; bool? _isSuccess; String? _resultMessage; @override void initState() { super.initState(); _bootstrapCookieSession(); } Future _bootstrapCookieSession() async { if (AuthTokenStore.usesCookie()) { return true; } if (_isCheckingSession) { return false; } setState(() => _isCheckingSession = true); try { await AuthProxyService.checkCookieSession(); AuthTokenStore.setCookieMode(provider: 'ory'); return true; } catch (e) { _log.info('Cookie session check failed: $e'); return false; } finally { if (mounted) { setState(() => _isCheckingSession = false); } } } @override void dispose() { controller.dispose(); super.dispose(); } Future _onDetect(BarcodeCapture capture) async { if (_isScanned) return; final List barcodes = capture.barcodes; for (final barcode in barcodes) { if (barcode.rawValue != null) { _isScanned = true; if (mounted) { setState(() => _isProcessing = true); } String qrData = barcode.rawValue!; String pendingRef = qrData; // URL 형식이라면 'ref' 파라미터 추출 시도 if (qrData.startsWith('http')) { try { final uri = Uri.parse(qrData); if (uri.queryParameters.containsKey('ref')) { pendingRef = uri.queryParameters['ref']!; } else if (uri.pathSegments.isNotEmpty) { final segments = uri.pathSegments; final qlIndex = segments.indexOf('ql'); if (qlIndex != -1 && qlIndex + 1 < segments.length) { pendingRef = segments[qlIndex + 1]; } } } catch (e) { _log.warning('Failed to parse QR URL: $qrData', e); } } _log.info('QR Code detected raw: $qrData, ref: $pendingRef'); final approveRef = qrData; final storedToken = AuthTokenStore.getToken(); final sessionToken = storedToken; var usesCookie = AuthTokenStore.usesCookie(); if (sessionToken == null && !usesCookie) { usesCookie = await _bootstrapCookieSession(); } if (sessionToken == null && !usesCookie) { if (mounted) { context.go('/signin?notice=qr_login_required'); } return; } try { // Call backend API to approve login with clean ref await AuthProxyService.approveQrLogin( approveRef, token: sessionToken, withCredentials: usesCookie, ); if (mounted) { setState(() { _isSuccess = true; _resultMessage = 'QR 승인 완료! PC 화면에서 로그인이 진행됩니다.'; _isProcessing = false; }); } } catch (e) { _log.severe("QR Approval Failed", e); if (mounted) { setState(() { _isSuccess = false; _resultMessage = 'QR 승인 실패: $e'; _isProcessing = false; }); } } break; } } } void _resetScan() { setState(() { _isScanned = false; _isProcessing = false; _isSuccess = null; _resultMessage = null; }); controller.start(); } Future _requestCameraPermission() async { if (_isRequestingCamera) return; setState(() => _isRequestingCamera = true); try { await controller.start(); } catch (e) { _log.warning('Camera permission request failed: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() => _isRequestingCamera = false); } } } Widget _buildResultView() { final success = _isSuccess == true; final icon = success ? Icons.check_circle_outline : Icons.error_outline; final color = success ? Colors.green : Colors.red; final title = success ? '승인 완료' : '승인 실패'; final message = _resultMessage ?? ''; return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color, size: 72), const SizedBox(height: 16), Text( title, style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: color), ), const SizedBox(height: 12), Text( message, textAlign: TextAlign.center, style: const TextStyle(color: Colors.black54), ), const SizedBox(height: 24), if (!success) FilledButton( onPressed: _resetScan, child: const Text('다시 스캔'), ), if (success) FilledButton( onPressed: () => context.pop(), child: const Text('닫기'), ), ], ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Scan QR Code'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.pop(), ), ), body: _isSuccess == null ? Stack( children: [ MobileScanner( controller: controller, onDetect: _onDetect, errorBuilder: (context, error) { final isPermissionDenied = error.errorCode == MobileScannerErrorCode.permissionDenied; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error, color: Colors.red, size: 50), const SizedBox(height: 10), Text( isPermissionDenied ? '카메라 권한이 필요합니다.' : '카메라 오류: ${error.errorCode}', ), const SizedBox(height: 12), FilledButton( onPressed: _isRequestingCamera ? null : _requestCameraPermission, child: Text( _isRequestingCamera ? '요청 중...' : '카메라 권한 요청하기', ), ), ], ), ); }, ), if (_isProcessing || _isCheckingSession) const Center(child: CircularProgressIndicator()), ], ) : _buildResultView(), ); } }