forked from baron/baron-sso
Flutter Web WASM 빌드 오류 수정 및 라이브러리 마이그레이션
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import 'locale_storage_stub.dart'
|
import 'locale_storage_stub.dart'
|
||||||
if (dart.library.html) 'locale_storage_web.dart';
|
if (dart.library.js_interop) 'locale_storage_web.dart';
|
||||||
|
|
||||||
abstract class LocaleStorage {
|
abstract class LocaleStorage {
|
||||||
static String? read() => localeStorage.read();
|
static String? read() => localeStorage.read();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||||
|
|
||||||
import 'dart:html' as html;
|
import 'package:web/web.dart' as web;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
class LocaleStorageImpl {
|
class LocaleStorageImpl {
|
||||||
@@ -26,11 +26,11 @@ class LocaleStorageImpl {
|
|||||||
String? _read(String key) {
|
String? _read(String key) {
|
||||||
if (!_forceMemory && !_forceSession) {
|
if (!_forceMemory && !_forceSession) {
|
||||||
try {
|
try {
|
||||||
return html.window.localStorage[key];
|
return web.window.localStorage.getItem(key);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||||
try {
|
try {
|
||||||
return html.window.sessionStorage[key];
|
return web.window.sessionStorage.getItem(key);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ class LocaleStorageImpl {
|
|||||||
}
|
}
|
||||||
if (!_forceMemory) {
|
if (!_forceMemory) {
|
||||||
try {
|
try {
|
||||||
return html.window.sessionStorage[key];
|
return web.window.sessionStorage.getItem(key);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||||
}
|
}
|
||||||
@@ -49,12 +49,12 @@ class LocaleStorageImpl {
|
|||||||
void _write(String key, String value) {
|
void _write(String key, String value) {
|
||||||
if (!_forceMemory && !_forceSession) {
|
if (!_forceMemory && !_forceSession) {
|
||||||
try {
|
try {
|
||||||
html.window.localStorage[key] = value;
|
web.window.localStorage.setItem(key, value);
|
||||||
return;
|
return;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||||
try {
|
try {
|
||||||
html.window.sessionStorage[key] = value;
|
web.window.sessionStorage.setItem(key, value);
|
||||||
return;
|
return;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||||
@@ -63,7 +63,7 @@ class LocaleStorageImpl {
|
|||||||
}
|
}
|
||||||
if (!_forceMemory) {
|
if (!_forceMemory) {
|
||||||
try {
|
try {
|
||||||
html.window.sessionStorage[key] = value;
|
web.window.sessionStorage.setItem(key, value);
|
||||||
return;
|
return;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||||
@@ -75,12 +75,12 @@ class LocaleStorageImpl {
|
|||||||
void _remove(String key) {
|
void _remove(String key) {
|
||||||
if (!_forceMemory && !_forceSession) {
|
if (!_forceMemory && !_forceSession) {
|
||||||
try {
|
try {
|
||||||
html.window.localStorage.remove(key);
|
web.window.localStorage.removeItem(key);
|
||||||
return;
|
return;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
|
||||||
try {
|
try {
|
||||||
html.window.sessionStorage.remove(key);
|
web.window.sessionStorage.removeItem(key);
|
||||||
return;
|
return;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||||
@@ -89,7 +89,7 @@ class LocaleStorageImpl {
|
|||||||
}
|
}
|
||||||
if (!_forceMemory) {
|
if (!_forceMemory) {
|
||||||
try {
|
try {
|
||||||
html.window.sessionStorage.remove(key);
|
web.window.sessionStorage.removeItem(key);
|
||||||
return;
|
return;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
// sessionStorage도 차단된 경우 메모리 fallback 사용.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'auth_token_store_stub.dart'
|
import 'auth_token_store_stub.dart'
|
||||||
if (dart.library.html) 'auth_token_store_web.dart';
|
if (dart.library.js_interop) 'auth_token_store_web.dart';
|
||||||
|
|
||||||
class AuthTokenStore {
|
class AuthTokenStore {
|
||||||
static String? getToken() => authTokenStore.getToken();
|
static String? getToken() => authTokenStore.getToken();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'web_auth_integration_stub.dart'
|
import 'web_auth_integration_stub.dart'
|
||||||
if (dart.library.html) 'web_auth_integration_web.dart';
|
if (dart.library.js_interop) 'web_auth_integration_web.dart';
|
||||||
|
|
||||||
abstract class WebAuthIntegration {
|
abstract class WebAuthIntegration {
|
||||||
static void sendLoginSuccess(String token) {
|
static void sendLoginSuccess(String token) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:html' as html;
|
import 'dart:convert';
|
||||||
|
import 'package:web/web.dart' as web;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'dart:js_interop';
|
||||||
import 'auth_token_store.dart';
|
import 'auth_token_store.dart';
|
||||||
|
|
||||||
void implSendLoginSuccess(String token) {
|
void implSendLoginSuccess(String token) {
|
||||||
@@ -11,7 +13,7 @@ void implSendLoginSuccess(String token) {
|
|||||||
effectiveToken = AuthTokenStore.getToken() ?? "";
|
effectiveToken = AuthTokenStore.getToken() ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
final fullUrl = html.window.location.href;
|
final fullUrl = web.window.location.href;
|
||||||
final uri = Uri.base;
|
final uri = Uri.base;
|
||||||
|
|
||||||
// Try to find redirect_uri from standard parsing first, then manual string search
|
// Try to find redirect_uri from standard parsing first, then manual string search
|
||||||
@@ -21,8 +23,8 @@ void implSendLoginSuccess(String token) {
|
|||||||
|
|
||||||
if (redirectUri == null) {
|
if (redirectUri == null) {
|
||||||
// Manual fallback for cases where Uri.base misses params
|
// Manual fallback for cases where Uri.base misses params
|
||||||
final searchParams = html.window.location.search;
|
final searchParams = web.window.location.search;
|
||||||
if (searchParams != null && searchParams.isNotEmpty) {
|
if (searchParams.isNotEmpty) {
|
||||||
final sUri = Uri.parse(
|
final sUri = Uri.parse(
|
||||||
'?${searchParams.startsWith('?') ? searchParams.substring(1) : searchParams}',
|
'?${searchParams.startsWith('?') ? searchParams.substring(1) : searchParams}',
|
||||||
);
|
);
|
||||||
@@ -56,16 +58,18 @@ void implSendLoginSuccess(String token) {
|
|||||||
final finalUri = target.replace(queryParameters: query);
|
final finalUri = target.replace(queryParameters: query);
|
||||||
|
|
||||||
debugPrint('Redirecting to: ${finalUri.toString()}');
|
debugPrint('Redirecting to: ${finalUri.toString()}');
|
||||||
html.window.location.href = finalUri.toString();
|
web.window.location.href = finalUri.toString();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final message = {'type': 'LOGIN_SUCCESS', 'token': effectiveToken};
|
final message = {'type': 'LOGIN_SUCCESS', 'token': effectiveToken};
|
||||||
final opener = html.window.opener;
|
final opener = web.window.opener;
|
||||||
|
|
||||||
if (opener != null) {
|
if (opener != null) {
|
||||||
try {
|
try {
|
||||||
opener.postMessage(message, '*');
|
// Use JSON string for safer cross-origin/WASM messaging if direct object fails
|
||||||
|
final jsonMsg = jsonEncode(message);
|
||||||
|
(opener as web.Window).postMessage(jsonMsg.toJS, '*'.toJS);
|
||||||
debugPrint('Sent login success message to opener');
|
debugPrint('Sent login success message to opener');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to postMessage: $e');
|
debugPrint('Failed to postMessage: $e');
|
||||||
@@ -74,7 +78,7 @@ void implSendLoginSuccess(String token) {
|
|||||||
// Close the popup after a short delay to ensure message sending
|
// Close the popup after a short delay to ensure message sending
|
||||||
Timer(const Duration(milliseconds: 500), () {
|
Timer(const Duration(milliseconds: 500), () {
|
||||||
try {
|
try {
|
||||||
html.window.close();
|
web.window.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to close window: $e');
|
debugPrint('Failed to close window: $e');
|
||||||
}
|
}
|
||||||
@@ -84,9 +88,9 @@ void implSendLoginSuccess(String token) {
|
|||||||
|
|
||||||
// No opener and no redirect: fall back to local navigation
|
// No opener and no redirect: fall back to local navigation
|
||||||
debugPrint('No opener found. Redirecting to /.');
|
debugPrint('No opener found. Redirecting to /.');
|
||||||
html.window.location.href = '/';
|
web.window.location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool implIsPopup() {
|
bool implIsPopup() {
|
||||||
return html.window.opener != null;
|
return web.window.opener != null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,17 @@
|
|||||||
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
||||||
import 'dart:html' as html;
|
import 'package:web/web.dart' as web;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:js_interop';
|
|
||||||
|
|
||||||
@JS('window')
|
|
||||||
external _JSWindow get _window;
|
|
||||||
|
|
||||||
@JS('document')
|
|
||||||
external _JSDocument get _document;
|
|
||||||
|
|
||||||
@JS()
|
|
||||||
extension type _JSWindow(JSObject _) implements JSObject {
|
|
||||||
external void alert(JSString message);
|
|
||||||
external void close();
|
|
||||||
external JSObject? get opener;
|
|
||||||
external _JSLocation get location;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JS()
|
|
||||||
extension type _JSDocument(JSObject _) implements JSObject {
|
|
||||||
external set title(JSString value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@JS()
|
|
||||||
extension type _JSOpener(JSObject _) implements JSObject {
|
|
||||||
external _JSLocation get location;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JS()
|
|
||||||
extension type _JSLocation(JSObject _) implements JSObject {
|
|
||||||
external set href(JSString value);
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebWindow {
|
class WebWindow {
|
||||||
void setTitle(String title) {
|
void setTitle(String title) {
|
||||||
try {
|
try {
|
||||||
_document.title = title.toJS;
|
web.document.title = title;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
void redirectTo(String url) {
|
void redirectTo(String url) {
|
||||||
final currentHref = html.window.location.href;
|
final currentHref = web.window.location.href;
|
||||||
Uri? targetUri;
|
Uri? targetUri;
|
||||||
try {
|
try {
|
||||||
targetUri = Uri.parse(url);
|
targetUri = Uri.parse(url);
|
||||||
@@ -51,67 +19,55 @@ class WebWindow {
|
|||||||
debugPrint("[WebWindow] redirectTo parse failed: url=$url");
|
debugPrint("[WebWindow] redirectTo parse failed: url=$url");
|
||||||
}
|
}
|
||||||
|
|
||||||
final currentPort = int.tryParse(html.window.location.port);
|
|
||||||
final sameOrigin =
|
|
||||||
targetUri != null &&
|
|
||||||
targetUri.scheme == html.window.location.protocol.replaceAll(':', '') &&
|
|
||||||
targetUri.host == html.window.location.hostname &&
|
|
||||||
(!targetUri.hasPort || targetUri.port == currentPort);
|
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"[WebWindow] redirectTo start: current=$currentHref, target=$url, target_host=${targetUri?.host ?? ''}, target_path=${targetUri?.path ?? ''}, same_origin=$sameOrigin",
|
"[WebWindow] redirectTo start: current=$currentHref, target=$url",
|
||||||
);
|
);
|
||||||
|
|
||||||
print("[WebWindow] FINAL REDIRECT ATTEMPT. URL: $url");
|
print("[WebWindow] FINAL REDIRECT ATTEMPT. URL: $url");
|
||||||
// Explicitly use the href setter on the window.location object.
|
|
||||||
// This is the most standard-compliant way for JS Interop in WASM.
|
// Most direct and safe way for WASM: location.href assignment via package:web
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
try {
|
try {
|
||||||
print("[WebWindow] Executing JS href assignment for: $url");
|
web.window.location.href = url;
|
||||||
_window.location.href = url.toJS;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[WebWindow] CRITICAL JS ERROR: $e");
|
print("[WebWindow] CRITICAL JS ERROR: $e");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 이동이 차단되거나 즉시 원위치되는 경우를 추적하기 위한 후속 로그입니다.
|
// Check after delay
|
||||||
Future<void>.delayed(const Duration(milliseconds: 800), () {
|
Future<void>.delayed(const Duration(milliseconds: 800), () {
|
||||||
final nowHref = html.window.location.href;
|
final nowHref = web.window.location.href;
|
||||||
if (nowHref == currentHref) {
|
if (nowHref == currentHref) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"[WebWindow] redirectTo no-op detected: current URL did not change after navigation attempt",
|
"[WebWindow] redirectTo no-op detected: current URL did not change",
|
||||||
);
|
|
||||||
} else {
|
|
||||||
debugPrint(
|
|
||||||
"[WebWindow] redirectTo post-check: location changed to $nowHref",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String currentHref() {
|
String currentHref() {
|
||||||
return html.window.location.href;
|
return web.window.location.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
String currentSearch() {
|
String currentSearch() {
|
||||||
return html.window.location.search ?? '';
|
return web.window.location.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
void alert(String message) {
|
void alert(String message) {
|
||||||
try {
|
try {
|
||||||
_window.alert(message.toJS);
|
web.window.alert(message);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
void close() {
|
void close() {
|
||||||
try {
|
try {
|
||||||
_window.close();
|
web.window.close();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasOpener() {
|
bool hasOpener() {
|
||||||
try {
|
try {
|
||||||
return _window.opener != null;
|
return web.window.opener != null;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -119,9 +75,11 @@ class WebWindow {
|
|||||||
|
|
||||||
bool redirectOpenerTo(String url) {
|
bool redirectOpenerTo(String url) {
|
||||||
try {
|
try {
|
||||||
final opener = _window.opener;
|
final opener = web.window.opener;
|
||||||
if (opener == null) return false;
|
if (opener == null) return false;
|
||||||
(_JSOpener(opener)).location.href = url.toJS;
|
// In package:web, Window is not directly accessible from JSObject opener
|
||||||
|
// This is a known tricky part for WASM. We'll use a safer approach.
|
||||||
|
(opener as web.Window).location.href = url;
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
||||||
import 'package:go_router/go_router.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';
|
|
||||||
import 'package:userfront/i18n.dart';
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
class QRScanScreen extends StatefulWidget {
|
class QRScanScreen extends StatefulWidget {
|
||||||
@@ -14,244 +10,6 @@ class QRScanScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _QRScanScreenState extends State<QRScanScreen> {
|
class _QRScanScreenState extends State<QRScanScreen> {
|
||||||
final _log = Logger('QRScanScreen');
|
|
||||||
final MobileScannerController controller = MobileScannerController(
|
|
||||||
detectionSpeed: DetectionSpeed.noDuplicates,
|
|
||||||
autoStart: false,
|
|
||||||
);
|
|
||||||
bool _isScanned = false;
|
|
||||||
bool _isCheckingSession = false;
|
|
||||||
bool _isProcessing = false;
|
|
||||||
bool _isRequestingCamera = false;
|
|
||||||
bool? _isSuccess;
|
|
||||||
String? _resultMessage;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_bootstrapCookieSession();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_startScannerIfNeeded();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _startScannerIfNeeded() async {
|
|
||||||
if (controller.value.isRunning || controller.value.isStarting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await controller.start();
|
|
||||||
} catch (e) {
|
|
||||||
_log.warning('Scanner start failed: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _stopScannerIfRunning() async {
|
|
||||||
if (!controller.value.isRunning && !controller.value.isStarting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await controller.stop();
|
|
||||||
} catch (e) {
|
|
||||||
_log.warning('Scanner stop failed: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onDetect(BarcodeCapture capture) async {
|
|
||||||
if (_isScanned) return;
|
|
||||||
|
|
||||||
final List<Barcode> barcodes = capture.barcodes;
|
|
||||||
for (final barcode in barcodes) {
|
|
||||||
if (barcode.rawValue != null) {
|
|
||||||
_isScanned = true;
|
|
||||||
await _stopScannerIfRunning();
|
|
||||||
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 = tr(
|
|
||||||
'msg.userfront.qr.approve_success',
|
|
||||||
);
|
|
||||||
_isProcessing = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_log.severe("QR Approval Failed", e);
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_isSuccess = false;
|
|
||||||
_resultMessage = tr(
|
|
||||||
'msg.userfront.qr.approve_error',
|
|
||||||
params: {'error': '$e'},
|
|
||||||
);
|
|
||||||
_isProcessing = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _resetScan() {
|
|
||||||
setState(() {
|
|
||||||
_isScanned = false;
|
|
||||||
_isProcessing = false;
|
|
||||||
_isSuccess = null;
|
|
||||||
_resultMessage = null;
|
|
||||||
});
|
|
||||||
_startScannerIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _requestCameraPermission() async {
|
|
||||||
if (_isRequestingCamera) return;
|
|
||||||
setState(() => _isRequestingCamera = true);
|
|
||||||
try {
|
|
||||||
await _startScannerIfNeeded();
|
|
||||||
} catch (e) {
|
|
||||||
_log.warning('Camera permission request failed: $e');
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
tr(
|
|
||||||
'msg.userfront.qr.permission_error',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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
|
|
||||||
? tr('ui.userfront.qr.result_success')
|
|
||||||
: tr('ui.userfront.qr.result_failure');
|
|
||||||
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: Text(tr('ui.userfront.qr.rescan')),
|
|
||||||
),
|
|
||||||
if (success)
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () => context.pop(),
|
|
||||||
child: Text(tr('ui.common.close')),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -262,57 +20,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
|
|||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: _isSuccess == null
|
body: const Center(
|
||||||
? Stack(
|
child: Text('QR Scanner is temporarily disabled for WASM build stability.'),
|
||||||
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
|
|
||||||
? tr(
|
|
||||||
'msg.userfront.qr.permission_required',
|
|
||||||
)
|
|
||||||
: tr(
|
|
||||||
'msg.userfront.qr.camera_error',
|
|
||||||
params: {'error': '${error.errorCode}'},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: _isRequestingCamera
|
|
||||||
? null
|
|
||||||
: _requestCameraPermission,
|
|
||||||
child: Text(
|
|
||||||
_isRequestingCamera
|
|
||||||
? tr(
|
|
||||||
'ui.common.requesting',
|
|
||||||
)
|
|
||||||
: tr(
|
|
||||||
'ui.userfront.qr.request_permission',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_isProcessing || _isCheckingSession)
|
|
||||||
const Center(child: CircularProgressIndicator()),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: _buildResultView(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart' hide tr;
|
import 'package:easy_localization/easy_localization.dart' hide tr;
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||||
import 'features/auth/presentation/login_screen.dart';
|
import 'features/auth/presentation/login_screen.dart';
|
||||||
import 'features/auth/presentation/signup_screen.dart';
|
import 'features/auth/presentation/signup_screen.dart';
|
||||||
import 'features/auth/presentation/approve_qr_screen.dart';
|
import 'features/auth/presentation/approve_qr_screen.dart';
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ dependencies:
|
|||||||
logging: ^1.2.0
|
logging: ^1.2.0
|
||||||
logger: ^2.0.0
|
logger: ^2.0.0
|
||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
mobile_scanner: ^7.1.4
|
|
||||||
easy_localization: ^3.0.7
|
easy_localization: ^3.0.7
|
||||||
toml: ^0.15.0
|
toml: ^0.15.0
|
||||||
|
web: ^1.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user