1
0
forked from baron/baron-sso
Files
baron-sso/userfront/lib/features/auth/presentation/qr_scan_screen_web.dart

248 lines
7.4 KiB
Dart

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<QRScanScreen> createState() => _QRScanScreenState();
}
class _QRScanScreenState extends State<QRScanScreen> {
final MobileScannerController _scannerController = MobileScannerController(
autoStart: true,
detectionSpeed: DetectionSpeed.noDuplicates,
facing: CameraFacing.back,
formats: const <BarcodeFormat>[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<void> _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<void> _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: '승인 화면으로 이동'),
),
),
],
),
),
);
}
}