diff --git a/userfront/lib/core/i18n/locale_storage.dart b/userfront/lib/core/i18n/locale_storage.dart index 6c757c04..596a30ae 100644 --- a/userfront/lib/core/i18n/locale_storage.dart +++ b/userfront/lib/core/i18n/locale_storage.dart @@ -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(); diff --git a/userfront/lib/core/i18n/locale_storage_web.dart b/userfront/lib/core/i18n/locale_storage_web.dart index 9d0d82c1..7c32ccff 100644 --- a/userfront/lib/core/i18n/locale_storage_web.dart +++ b/userfront/lib/core/i18n/locale_storage_web.dart @@ -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 사용. diff --git a/userfront/lib/core/services/auth_token_store.dart b/userfront/lib/core/services/auth_token_store.dart index 5ba93582..253ff99d 100644 --- a/userfront/lib/core/services/auth_token_store.dart +++ b/userfront/lib/core/services/auth_token_store.dart @@ -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(); diff --git a/userfront/lib/core/services/web_auth_integration.dart b/userfront/lib/core/services/web_auth_integration.dart index 1ffb1cf3..777c337c 100644 --- a/userfront/lib/core/services/web_auth_integration.dart +++ b/userfront/lib/core/services/web_auth_integration.dart @@ -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) { diff --git a/userfront/lib/core/services/web_auth_integration_web.dart b/userfront/lib/core/services/web_auth_integration_web.dart index 9b3b06db..061c5d2c 100644 --- a/userfront/lib/core/services/web_auth_integration_web.dart +++ b/userfront/lib/core/services/web_auth_integration_web.dart @@ -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; } diff --git a/userfront/lib/core/services/web_window_web.dart b/userfront/lib/core/services/web_window_web.dart index dbd56548..6fafbf6f 100644 --- a/userfront/lib/core/services/web_window_web.dart +++ b/userfront/lib/core/services/web_window_web.dart @@ -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.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; diff --git a/userfront/lib/features/auth/presentation/qr_scan_screen.dart b/userfront/lib/features/auth/presentation/qr_scan_screen.dart index 0d82f5d3..b9c83025 100644 --- a/userfront/lib/features/auth/presentation/qr_scan_screen.dart +++ b/userfront/lib/features/auth/presentation/qr_scan_screen.dart @@ -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 { - 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 _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 _startScannerIfNeeded() async { - if (controller.value.isRunning || controller.value.isStarting) { - return; - } - try { - await controller.start(); - } catch (e) { - _log.warning('Scanner start failed: $e'); - } - } - - Future _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 _onDetect(BarcodeCapture capture) async { - if (_isScanned) return; - - final List 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 _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 { 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.'), + ), ); } } diff --git a/userfront/lib/main.dart b/userfront/lib/main.dart index 97d7e040..677bfd76 100644 --- a/userfront/lib/main.dart +++ b/userfront/lib/main.dart @@ -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'; diff --git a/userfront/pubspec.yaml b/userfront/pubspec.yaml index 6f8e3775..ec186b30 100644 --- a/userfront/pubspec.yaml +++ b/userfront/pubspec.yaml @@ -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: