diff --git a/firka/lib/ui/phone/screens/login/login_screen.dart b/firka/lib/ui/phone/screens/login/login_screen.dart index 0a8cab6..f64d1ea 100644 --- a/firka/lib/ui/phone/screens/login/login_screen.dart +++ b/firka/lib/ui/phone/screens/login/login_screen.dart @@ -5,6 +5,7 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:firka/core/firka_bundle.dart'; import 'package:firka/app/app_state.dart'; import 'package:flutter/foundation.dart'; +import 'package:firka/ui/phone/widgets/domain_browser_webview.dart'; import 'package:firka/ui/phone/widgets/login_webview.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -12,7 +13,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sensors_plus/sensors_plus.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:vibration/vibration.dart'; import 'package:firka/core/bloc/theme_cubit.dart'; @@ -21,9 +21,8 @@ import 'package:firka/core/image_preloader.dart'; import 'package:firka/ui/theme/style.dart'; import 'package:firka/ui/shared/delayed_spinner.dart'; -const String _privacyUrlHungarian = - 'https://github.com/QwIT-Development/privacy-policy/blob/master/README.md'; -const String _privacyUrlOther = 'https://firka.app/privacy'; +const String _privacyUrlHungarian = 'https://firka.app/privacy-policy'; +const String _privacyUrlOther = 'https://firka.app/privacy-policy'; class LoginScreen extends StatefulWidget { final AppInitialization data; @@ -45,17 +44,27 @@ class _LoginScreenState extends FirkaState { } String _getPrivacyPolicyUrl() { - final locale = Localizations.localeOf(context).languageCode; - return locale == 'hu' ? _privacyUrlHungarian : _privacyUrlOther; + return "https://firka.app/privacy"; } - Future _launchPrivacyPolicy() async { + Future _showPrivacyPolicyWebview() async { final url = _getPrivacyPolicyUrl(); - try { - await launchUrl(Uri.parse(url)); - } catch (e) { - logger.shout('LoginScreen: Error launching privacy policy URL: $e'); - } + if (!mounted) return; + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: false, + builder: (BuildContext context) { + return SizedBox( + height: MediaQuery.sizeOf(context).height, + child: DomainBrowserWebviewWidget( + data: widget.data, + url: url, + ), + ); + }, + ); } Future _preloadImages() async { @@ -513,7 +522,7 @@ class _LoginScreenState extends FirkaState { ), const SizedBox(height: 20), GestureDetector( - onTap: _launchPrivacyPolicy, + onTap: _showPrivacyPolicyWebview, child: Text( widget.data.l10n.privacyLabel, textAlign: TextAlign.center, diff --git a/firka/lib/ui/phone/widgets/domain_browser_webview.dart b/firka/lib/ui/phone/widgets/domain_browser_webview.dart new file mode 100644 index 0000000..ee130b4 --- /dev/null +++ b/firka/lib/ui/phone/widgets/domain_browser_webview.dart @@ -0,0 +1,325 @@ +import 'dart:async'; + +import 'package:firka/app/app_state.dart'; +import 'package:firka/core/state/firka_state.dart'; +import 'package:firka/ui/theme/style.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:majesticons_flutter/majesticons_flutter.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +/// Lightweight in-app browser used outside the login flow (e.g. privacy policy). +/// +/// This deliberately contains only generic WebView behaviour, keeping all +/// login/token handling inside `login_webview.dart`. +class DomainBrowserWebviewWidget extends StatefulWidget { + final AppInitialization? data; + final String? url; + + const DomainBrowserWebviewWidget({ + super.key, + this.data, + this.url, + }); + + @override + State createState() => + _DomainBrowserWebviewWidgetState(); +} + +class _DomainBrowserWebviewWidgetState + extends FirkaState + with TickerProviderStateMixin { + late WebViewController _webViewController; + bool _isLoading = true; + AnimationController? _fadeAnimationController; + Animation? _fadeAnimation; + + @override + void initState() { + super.initState(); + + _fadeAnimationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 1.0, + end: 0.0, + ).animate(_fadeAnimationController!); + + assert(widget.data != null && widget.url != null, + 'DomainBrowserWebviewWidget requires non-null data and url'); + + _webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..loadRequest(Uri.parse(widget.url!)) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (String url) { + Timer(const Duration(milliseconds: 300), () { + if (mounted) { + setState(() { + _isLoading = false; + }); + _fadeAnimationController?.forward().then((_) { + _fadeAnimationController?.reset(); + }); + } + }); + }, + onPageStarted: (String url) { + setState(() { + _isLoading = true; + }); + _fadeAnimationController?.reset(); + }, + ), + ); + } + + @override + void dispose() { + _fadeAnimationController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.data == null || widget.url == null) { + return const SizedBox.shrink(); + } + + final data = widget.data!; + final mediaQuery = MediaQuery.of(context); + final safePadding = mediaQuery.padding; + final displayUrl = (widget.url ?? '').replaceFirst(RegExp(r'^https?://'), ''); + final displayParts = displayUrl.split('/'); + final host = displayParts.isNotEmpty ? displayParts.first : displayUrl; + final path = displayParts.length > 1 + ? '/${displayParts.sublist(1).join('/')}' + : ''; + + return Material( + color: appStyle.colors.background, + child: Padding( + padding: EdgeInsets.only( + top: 61 + safePadding.top, + left: 12, + right: 12, + bottom: safePadding.bottom, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 2), + child: SvgPicture.asset( + "assets/icons/dave.svg", + width: 24, + height: 24, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.data?.l10n.runningInDomainBrowser ?? + 'Domain Browser', + style: appStyle.fonts.B_16R.copyWith( + color: appStyle.colors.textPrimary, + ), + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + width: 36, + height: 36, + alignment: Alignment.center, + decoration: BoxDecoration( + color: appStyle.colors.buttonSecondaryFill, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: appStyle.colors.shadowColor, + offset: const Offset(0, 1), + blurRadius: appStyle.colors.shadowBlur.toDouble(), + ), + ], + ), + child: Majesticon( + Majesticon.multiplySolid, + color: appStyle.colors.accent, + size: 16, + ), + ), + ), + ], + ), + const SizedBox(height: 22), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Stack( + children: [ + WebViewWidget( + controller: _webViewController, + gestureRecognizers: { + Factory( + () => EagerGestureRecognizer(), + ), + }, + ), + if (_fadeAnimationController != null && + _fadeAnimation != null) + IgnorePointer( + ignoring: !_isLoading, + child: AnimatedBuilder( + animation: _fadeAnimationController!, + builder: (context, child) => AnimatedOpacity( + opacity: _isLoading + ? 1.0 + : _fadeAnimationController!.isAnimating + ? _fadeAnimation!.value + : 0.0, + duration: const Duration(milliseconds: 500), + child: Container( + color: appStyle.colors.background, + child: Center( + child: Image.asset( + "assets/images/logos/loading.gif", + width: 50, + height: 50, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Container( + height: 42, + decoration: BoxDecoration( + color: appStyle.colors.card, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Align( + alignment: Alignment.centerLeft, + child: RichText( + text: TextSpan( + text: host, + style: appStyle.fonts.B_14R.copyWith( + fontSize: 16, + color: appStyle.colors.textPrimary, + ), + children: [ + TextSpan( + text: path, + style: appStyle.fonts.B_14R.copyWith( + fontSize: 16, + color: appStyle.colors.textTeritary ?? + appStyle.colors.textSecondary, + ), + ), + ], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ), + const SizedBox(width: 8), + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: appStyle.colors.buttonSecondaryFill, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: appStyle.colors.shadowColor, + offset: const Offset(0, 1), + blurRadius: appStyle.colors.shadowBlur.toDouble(), + ), + ], + ), + child: Center( + child: Image.asset( + "assets/icons/button/colorwheel.png", + width: 22, + height: 22, + ), + ), + ), + const SizedBox(width: 8), + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: appStyle.colors.buttonSecondaryFill, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: appStyle.colors.shadowColor, + offset: const Offset(0, 1), + blurRadius: appStyle.colors.shadowBlur.toDouble(), + ), + ], + ), + child: Center( + child: Majesticon( + Majesticon.chevronLeftLine, + color: appStyle.colors.secondary, + size: 22, + ), + ), + ), + const SizedBox(width: 8), + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: appStyle.colors.buttonSecondaryFill, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: appStyle.colors.shadowColor, + offset: const Offset(0, 1), + blurRadius: appStyle.colors.shadowBlur.toDouble(), + ), + ], + ), + child: Center( + child: Majesticon( + Majesticon.menuLine, + color: appStyle.colors.secondary, + size: 22, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } +} diff --git a/firka/lib/ui/phone/widgets/login_webview.dart b/firka/lib/ui/phone/widgets/login_webview.dart index 388f624..ce84db7 100644 --- a/firka/lib/ui/phone/widgets/login_webview.dart +++ b/firka/lib/ui/phone/widgets/login_webview.dart @@ -5,6 +5,8 @@ import 'package:firka/data/models/app_settings_model.dart'; import 'package:firka/services/live_activity_service.dart'; import 'package:firka/app/app_state.dart'; import 'package:firka/app/initialization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar_community/isar.dart'; @@ -41,6 +43,8 @@ class _LoginWebviewWidgetState extends FirkaState bool _isLoading = true; AnimationController? _fadeAnimationController; Animation? _fadeAnimation; + late String _displayHost; + late String _displayPath; @override void initState() { @@ -65,6 +69,11 @@ class _LoginWebviewWidgetState extends FirkaState ); } + final trimmed = loginUrl.replaceFirst(RegExp(r'^https?://'), ''); + final parts = trimmed.split('/'); + _displayHost = parts.isNotEmpty ? parts.first : trimmed; + _displayPath = parts.length > 1 ? '/${parts.sublist(1).join('/')}' : ''; + logger.info("Using loginUrl: $loginUrl"); _webViewController = WebViewController() @@ -259,7 +268,14 @@ class _LoginWebviewWidgetState extends FirkaState borderRadius: BorderRadius.circular(20), child: Stack( children: [ - WebViewWidget(controller: _webViewController), + WebViewWidget( + controller: _webViewController, + gestureRecognizers: { + Factory( + () => EagerGestureRecognizer(), + ), + }, + ), if (_fadeAnimationController != null && _fadeAnimation != null) IgnorePointer( @@ -302,16 +318,29 @@ class _LoginWebviewWidgetState extends FirkaState ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - Text( - "eKréta/Bejelentkezés", + child: Align( + alignment: Alignment.centerLeft, + child: RichText( + text: TextSpan( + text: _displayHost, style: appStyle.fonts.B_14R.copyWith( fontSize: 16, color: appStyle.colors.textPrimary, ), + children: [ + TextSpan( + text: _displayPath, + style: appStyle.fonts.B_14R.copyWith( + fontSize: 16, + color: appStyle.colors.textTeritary ?? + appStyle.colors.textSecondary, + ), + ), + ], ), - ], + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ), ), diff --git a/firka_common/lib/ui/theme/style.dart b/firka_common/lib/ui/theme/style.dart index 691bbfd..6179d05 100644 --- a/firka_common/lib/ui/theme/style.dart +++ b/firka_common/lib/ui/theme/style.dart @@ -56,6 +56,7 @@ class FirkaColors { Color textPrimary; Color textSecondary; Color textTertiary; + Color? textTeritary; Color textPrimaryLight; Color textSecondaryLight; @@ -97,6 +98,7 @@ class FirkaColors { required this.textPrimary, required this.textSecondary, required this.textTertiary, + this.textTeritary, required this.textPrimaryLight, required this.textSecondaryLight, required this.textTertiaryLight, @@ -238,6 +240,7 @@ final FirkaStyle lightStyle = FirkaStyle( textPrimary: Color(0xFF394C0A), textSecondary: Color(0xCC394C0A), textTertiary: Color(0x80394C0A), + textTeritary: Color(0xFF97A474), textPrimaryLight: Color(0xFF394C0A), textSecondaryLight: Color(0xCC394C0A), textTertiaryLight: Color(0x80394C0A), @@ -277,6 +280,7 @@ final FirkaStyle darkStyle = FirkaStyle( textPrimary: Color(0xFFEAF7CC), textSecondary: Color(0xB3EAF7CC), textTertiary: Color(0x80EAF7CC), + textTeritary: Color(0xFF97A474), textPrimaryLight: Color(0xFF394C0A), textSecondaryLight: Color(0xCC394C0A), textTertiaryLight: Color(0x80394C0A),