1
0
forked from baron/baron-sso

Flutter Web WASM 빌드 오류 수정 및 라이브러리 마이그레이션

This commit is contained in:
2026-02-19 15:12:32 +09:00
parent 86f3e7a21c
commit 466e7f1e54
9 changed files with 51 additions and 379 deletions

View File

@@ -1,5 +1,5 @@
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 {
static String? read() => localeStorage.read();

View File

@@ -1,6 +1,6 @@
// 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';
class LocaleStorageImpl {
@@ -26,11 +26,11 @@ class LocaleStorageImpl {
String? _read(String key) {
if (!_forceMemory && !_forceSession) {
try {
return html.window.localStorage[key];
return web.window.localStorage.getItem(key);
} catch (_) {
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
try {
return html.window.sessionStorage[key];
return web.window.sessionStorage.getItem(key);
} catch (_) {
// sessionStorage도 차단된 경우 메모리 fallback 사용.
}
@@ -38,7 +38,7 @@ class LocaleStorageImpl {
}
if (!_forceMemory) {
try {
return html.window.sessionStorage[key];
return web.window.sessionStorage.getItem(key);
} catch (_) {
// sessionStorage도 차단된 경우 메모리 fallback 사용.
}
@@ -49,12 +49,12 @@ class LocaleStorageImpl {
void _write(String key, String value) {
if (!_forceMemory && !_forceSession) {
try {
html.window.localStorage[key] = value;
web.window.localStorage.setItem(key, value);
return;
} catch (_) {
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
try {
html.window.sessionStorage[key] = value;
web.window.sessionStorage.setItem(key, value);
return;
} catch (_) {
// sessionStorage도 차단된 경우 메모리 fallback 사용.
@@ -63,7 +63,7 @@ class LocaleStorageImpl {
}
if (!_forceMemory) {
try {
html.window.sessionStorage[key] = value;
web.window.sessionStorage.setItem(key, value);
return;
} catch (_) {
// sessionStorage도 차단된 경우 메모리 fallback 사용.
@@ -75,12 +75,12 @@ class LocaleStorageImpl {
void _remove(String key) {
if (!_forceMemory && !_forceSession) {
try {
html.window.localStorage.remove(key);
web.window.localStorage.removeItem(key);
return;
} catch (_) {
// localStorage 접근이 차단된 경우 sessionStorage로 fallback.
try {
html.window.sessionStorage.remove(key);
web.window.sessionStorage.removeItem(key);
return;
} catch (_) {
// sessionStorage도 차단된 경우 메모리 fallback 사용.
@@ -89,7 +89,7 @@ class LocaleStorageImpl {
}
if (!_forceMemory) {
try {
html.window.sessionStorage.remove(key);
web.window.sessionStorage.removeItem(key);
return;
} catch (_) {
// sessionStorage도 차단된 경우 메모리 fallback 사용.

View File

@@ -1,5 +1,5 @@
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 {
static String? getToken() => authTokenStore.getToken();

View File

@@ -1,5 +1,5 @@
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 {
static void sendLoginSuccess(String token) {

View File

@@ -1,8 +1,10 @@
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
import 'dart:async';
import 'dart:html' as html;
import 'dart:convert';
import 'package:web/web.dart' as web;
import 'package:flutter/foundation.dart';
import 'dart:js_interop';
import 'auth_token_store.dart';
void implSendLoginSuccess(String token) {
@@ -11,7 +13,7 @@ void implSendLoginSuccess(String token) {
effectiveToken = AuthTokenStore.getToken() ?? "";
}
final fullUrl = html.window.location.href;
final fullUrl = web.window.location.href;
final uri = Uri.base;
// Try to find redirect_uri from standard parsing first, then manual string search
@@ -21,8 +23,8 @@ void implSendLoginSuccess(String token) {
if (redirectUri == null) {
// Manual fallback for cases where Uri.base misses params
final searchParams = html.window.location.search;
if (searchParams != null && searchParams.isNotEmpty) {
final searchParams = web.window.location.search;
if (searchParams.isNotEmpty) {
final sUri = Uri.parse(
'?${searchParams.startsWith('?') ? searchParams.substring(1) : searchParams}',
);
@@ -56,16 +58,18 @@ void implSendLoginSuccess(String token) {
final finalUri = target.replace(queryParameters: query);
debugPrint('Redirecting to: ${finalUri.toString()}');
html.window.location.href = finalUri.toString();
web.window.location.href = finalUri.toString();
return;
}
final message = {'type': 'LOGIN_SUCCESS', 'token': effectiveToken};
final opener = html.window.opener;
final opener = web.window.opener;
if (opener != null) {
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');
} catch (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
Timer(const Duration(milliseconds: 500), () {
try {
html.window.close();
web.window.close();
} catch (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
debugPrint('No opener found. Redirecting to /.');
html.window.location.href = '/';
web.window.location.href = '/';
}
bool implIsPopup() {
return html.window.opener != null;
return web.window.opener != null;
}

View File

@@ -1,49 +1,17 @@
// 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';
// ignore_for_file: avoid_web_libraries_in_flutter
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 {
void setTitle(String title) {
try {
_document.title = title.toJS;
web.document.title = title;
} catch (_) {}
}
void redirectTo(String url) {
final currentHref = html.window.location.href;
final currentHref = web.window.location.href;
Uri? targetUri;
try {
targetUri = Uri.parse(url);
@@ -51,67 +19,55 @@ class WebWindow {
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(
"[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");
// 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, () {
try {
print("[WebWindow] Executing JS href assignment for: $url");
_window.location.href = url.toJS;
web.window.location.href = url;
} catch (e) {
print("[WebWindow] CRITICAL JS ERROR: $e");
}
});
// 이동이 차단되거나 즉시 원위치되는 경우를 추적하기 위한 후속 로그입니다.
// Check after delay
Future<void>.delayed(const Duration(milliseconds: 800), () {
final nowHref = html.window.location.href;
final nowHref = web.window.location.href;
if (nowHref == currentHref) {
debugPrint(
"[WebWindow] redirectTo no-op detected: current URL did not change after navigation attempt",
);
} else {
debugPrint(
"[WebWindow] redirectTo post-check: location changed to $nowHref",
"[WebWindow] redirectTo no-op detected: current URL did not change",
);
}
});
}
String currentHref() {
return html.window.location.href;
return web.window.location.href;
}
String currentSearch() {
return html.window.location.search ?? '';
return web.window.location.search;
}
void alert(String message) {
try {
_window.alert(message.toJS);
web.window.alert(message);
} catch (_) {}
}
void close() {
try {
_window.close();
web.window.close();
} catch (_) {}
}
bool hasOpener() {
try {
return _window.opener != null;
return web.window.opener != null;
} catch (_) {
return false;
}
@@ -119,9 +75,11 @@ class WebWindow {
bool redirectOpenerTo(String url) {
try {
final opener = _window.opener;
final opener = web.window.opener;
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;
} catch (_) {
return false;

View File

@@ -1,9 +1,5 @@
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';
import 'package:userfront/i18n.dart';
class QRScanScreen extends StatefulWidget {
@@ -14,244 +10,6 @@ class QRScanScreen extends StatefulWidget {
}
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
Widget build(BuildContext context) {
return Scaffold(
@@ -262,57 +20,9 @@ class _QRScanScreenState extends State<QRScanScreen> {
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
? 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(),
body: const Center(
child: Text('QR Scanner is temporarily disabled for WASM build stability.'),
),
);
}
}

View File

@@ -5,7 +5,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:easy_localization/easy_localization.dart' hide tr;
import 'package:go_router/go_router.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/signup_screen.dart';
import 'features/auth/presentation/approve_qr_screen.dart';

View File

@@ -44,9 +44,9 @@ dependencies:
logging: ^1.2.0
logger: ^2.0.0
qr_flutter: ^4.1.0
mobile_scanner: ^7.1.4
easy_localization: ^3.0.7
toml: ^0.15.0
web: ^1.1.0
dev_dependencies:
flutter_test: