6177214e-ce7c-49e3-99de-ff9721b26f63 — Commit 33ac7401

AuthorMikkel Thygesen<mikkelet@gmail.com>
Date2026-02-12 14:07:29 +0100
Added webview

Changed files

.../webview/bloc/webview_state.freezed.dart        | 274 +++++++++++++++++++++
 .../screens/webview/bloc/webview_cubit.dart        |  43 ++++
 .../screens/webview/bloc/webview_state.dart        |  11 +
 .../screens/webview/webview_route.dart             |  20 ++
 .../screens/webview/webview_screen.dart            |  88 +++++++
 comwell_key_app/lib/routing/app_router.dart        |   9 +
 comwell_key_app/lib/routing/app_routes.dart        |   1 +
 comwell_key_app/pubspec.yaml                       |   3 +-
 8 files changed, 448 insertions(+), 1 deletion(-)

Diff

diff --git a/comwell_key_app/lib/.generated/presentation/screens/webview/bloc/webview_state.freezed.dart b/comwell_key_app/lib/.generated/presentation/screens/webview/bloc/webview_state.freezed.dart
new file mode 100644
index 00000000..0bcd97f9
--- /dev/null
+++ b/comwell_key_app/lib/.generated/presentation/screens/webview/bloc/webview_state.freezed.dart
@@ -0,0 +1,274 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// coverage:ignore-file
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of '../../../../../presentation/screens/webview/bloc/webview_state.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+// dart format off
+T _$identity<T>(T value) => value;
+/// @nodoc
+mixin _$WebviewState {
+
+ bool get isLoading; String get errorMessage;
+/// Create a copy of WebviewState
+/// with the given fields replaced by the non-null parameter values.
+@JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+$WebviewStateCopyWith<WebviewState> get copyWith => _$WebviewStateCopyWithImpl<WebviewState>(this as WebviewState, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is WebviewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,isLoading,errorMessage);
+
+@override
+String toString() {
+ return 'WebviewState(isLoading: $isLoading, errorMessage: $errorMessage)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class $WebviewStateCopyWith<$Res> {
+ factory $WebviewStateCopyWith(WebviewState value, $Res Function(WebviewState) _then) = _$WebviewStateCopyWithImpl;
+@useResult
+$Res call({
+ bool isLoading, String errorMessage
+});
+
+
+
+
+}
+/// @nodoc
+class _$WebviewStateCopyWithImpl<$Res>
+ implements $WebviewStateCopyWith<$Res> {
+ _$WebviewStateCopyWithImpl(this._self, this._then);
+
+ final WebviewState _self;
+ final $Res Function(WebviewState) _then;
+
+/// Create a copy of WebviewState
+/// with the given fields replaced by the non-null parameter values.
+@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? errorMessage = null,}) {
+ return _then(_self.copyWith(
+isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
+as String,
+ ));
+}
+
+}
+
+
+/// Adds pattern-matching-related methods to [WebviewState].
+extension WebviewStatePatterns on WebviewState {
+/// A variant of `map` that fallback to returning `orElse`.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case _:
+/// return orElse();
+/// }
+/// ```
+
+@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _WebviewState value)? $default,{required TResult orElse(),}){
+final _that = this;
+switch (_that) {
+case _WebviewState() when $default != null:
+return $default(_that);case _:
+ return orElse();
+
+}
+}
+/// A `switch`-like method, using callbacks.
+///
+/// Callbacks receives the raw object, upcasted.
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case final Subclass2 value:
+/// return ...;
+/// }
+/// ```
+
+@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _WebviewState value) $default,){
+final _that = this;
+switch (_that) {
+case _WebviewState():
+return $default(_that);case _:
+ throw StateError('Unexpected subclass');
+
+}
+}
+/// A variant of `map` that fallback to returning `null`.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case _:
+/// return null;
+/// }
+/// ```
+
+@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _WebviewState value)? $default,){
+final _that = this;
+switch (_that) {
+case _WebviewState() when $default != null:
+return $default(_that);case _:
+ return null;
+
+}
+}
+/// A variant of `when` that fallback to an `orElse` callback.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case _:
+/// return orElse();
+/// }
+/// ```
+
+@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
+switch (_that) {
+case _WebviewState() when $default != null:
+return $default(_that.isLoading,_that.errorMessage);case _:
+ return orElse();
+
+}
+}
+/// A `switch`-like method, using callbacks.
+///
+/// As opposed to `map`, this offers destructuring.
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case Subclass2(:final field2):
+/// return ...;
+/// }
+/// ```
+
+@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, String errorMessage) $default,) {final _that = this;
+switch (_that) {
+case _WebviewState():
+return $default(_that.isLoading,_that.errorMessage);case _:
+ throw StateError('Unexpected subclass');
+
+}
+}
+/// A variant of `when` that fallback to returning `null`
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case _:
+/// return null;
+/// }
+/// ```
+
+@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, String errorMessage)? $default,) {final _that = this;
+switch (_that) {
+case _WebviewState() when $default != null:
+return $default(_that.isLoading,_that.errorMessage);case _:
+ return null;
+
+}
+}
+
+}
+
+/// @nodoc
+
+
+class _WebviewState implements WebviewState {
+ const _WebviewState({this.isLoading = false, this.errorMessage = ""});
+
+
+@override@JsonKey() final bool isLoading;
+@override@JsonKey() final String errorMessage;
+
+/// Create a copy of WebviewState
+/// with the given fields replaced by the non-null parameter values.
+@override @JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+_$WebviewStateCopyWith<_WebviewState> get copyWith => __$WebviewStateCopyWithImpl<_WebviewState>(this, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebviewState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,isLoading,errorMessage);
+
+@override
+String toString() {
+ return 'WebviewState(isLoading: $isLoading, errorMessage: $errorMessage)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class _$WebviewStateCopyWith<$Res> implements $WebviewStateCopyWith<$Res> {
+ factory _$WebviewStateCopyWith(_WebviewState value, $Res Function(_WebviewState) _then) = __$WebviewStateCopyWithImpl;
+@override @useResult
+$Res call({
+ bool isLoading, String errorMessage
+});
+
+
+
+
+}
+/// @nodoc
+class __$WebviewStateCopyWithImpl<$Res>
+ implements _$WebviewStateCopyWith<$Res> {
+ __$WebviewStateCopyWithImpl(this._self, this._then);
+
+ final _WebviewState _self;
+ final $Res Function(_WebviewState) _then;
+
+/// Create a copy of WebviewState
+/// with the given fields replaced by the non-null parameter values.
+@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? errorMessage = null,}) {
+ return _then(_WebviewState(
+isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
+as String,
+ ));
+}
+
+
+}
+
+// dart format on
diff --git a/comwell_key_app/lib/presentation/screens/webview/bloc/webview_cubit.dart b/comwell_key_app/lib/presentation/screens/webview/bloc/webview_cubit.dart
new file mode 100644
index 00000000..d56b00bc
--- /dev/null
+++ b/comwell_key_app/lib/presentation/screens/webview/bloc/webview_cubit.dart
@@ -0,0 +1,43 @@
+import 'package:aad_b2c_webview/aad_b2c_webview.dart';
+import 'package:comwell_key_app/authentication/authentication_repository.dart';
+import 'package:comwell_key_app/base/base_cubit.dart';
+import 'package:comwell_key_app/presentation/screens/webview/bloc/webview_state.dart';
+
+class WebviewCubit extends BaseCubit<WebviewState> {
+ late WebViewController controller;
+ final String url;
+ final String title;
+ final AuthenticationRepository _authRepository;
+
+ WebviewCubit(this._authRepository, {required this.url, this.title = ""}) : super(const WebviewState()) {
+ init();
+ }
+
+ void init() {
+ controller = WebViewController()
+ ..setJavaScriptMode(JavaScriptMode.unrestricted)
+ ..setNavigationDelegate(
+ NavigationDelegate(
+ onPageStarted: (String url) {
+ safeEmit(state.copyWith(isLoading: true));
+ },
+ onPageFinished: (String url) {
+ safeEmit(state.copyWith(isLoading: false));
+ },
+ onWebResourceError: (WebResourceError error) {
+ print('WebView error: ${error.description}');
+ },
+ ),
+ );
+
+ // Load with headers
+ loadPage(url, "");
+ }
+
+ void loadPage(String url, String authToken) {
+ controller.loadRequest(
+ Uri.parse(url),
+ headers: {},
+ );
+ }
+}
diff --git a/comwell_key_app/lib/presentation/screens/webview/bloc/webview_state.dart b/comwell_key_app/lib/presentation/screens/webview/bloc/webview_state.dart
new file mode 100644
index 00000000..bfe14a2b
--- /dev/null
+++ b/comwell_key_app/lib/presentation/screens/webview/bloc/webview_state.dart
@@ -0,0 +1,11 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part '../../../../.generated/presentation/screens/webview/bloc/webview_state.freezed.dart';
+
+@freezed
+abstract class WebviewState with _$WebviewState {
+ const factory WebviewState({
+ @Default(false) bool isLoading,
+ @Default("") String errorMessage,
+ }) = _WebviewState;
+}
\ No newline at end of file
diff --git a/comwell_key_app/lib/presentation/screens/webview/webview_route.dart b/comwell_key_app/lib/presentation/screens/webview/webview_route.dart
new file mode 100644
index 00000000..bae6c900
--- /dev/null
+++ b/comwell_key_app/lib/presentation/screens/webview/webview_route.dart
@@ -0,0 +1,20 @@
+import 'package:comwell_key_app/presentation/screens/webview/bloc/webview_cubit.dart';
+import 'package:comwell_key_app/presentation/screens/webview/webview_screen.dart';
+import 'package:comwell_key_app/routing/app_routes.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
+
+import '../../../utils/locator.dart';
+
+final webviewRoute = GoRoute(
+ path: AppRoutes.webview,
+ builder: (context, state) {
+ final url = state.uri.queryParameters["url"] ?? "";
+ final title = state.uri.queryParameters["title"] ?? "";
+ return BlocProvider(
+ create: (context) => WebviewCubit(locator(), url: url, title: title),
+ child: const WebViewScreen(),
+ );
+ },
+);
+
diff --git a/comwell_key_app/lib/presentation/screens/webview/webview_screen.dart b/comwell_key_app/lib/presentation/screens/webview/webview_screen.dart
new file mode 100644
index 00000000..bcd4efbc
--- /dev/null
+++ b/comwell_key_app/lib/presentation/screens/webview/webview_screen.dart
@@ -0,0 +1,88 @@
+import 'package:comwell_key_app/.generated/assets/assets.gen.dart';
+import 'package:comwell_key_app/presentation/screens/webview/bloc/webview_cubit.dart';
+import 'package:comwell_key_app/presentation/screens/webview/bloc/webview_state.dart';
+import 'package:comwell_key_app/themes/comwell_colors.dart';
+import 'package:comwell_key_app/utils/context_utils.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
+
+import 'package:webview_flutter/webview_flutter.dart';
+
+class WebViewScreen extends StatelessWidget {
+ const WebViewScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final cubit = context.read<WebviewCubit>();
+ return BlocBuilder<WebviewCubit, WebviewState>(
+ builder: (BuildContext context, WebviewState state) {
+ final webviewCubit = context.read<WebviewCubit>();
+ if (state.errorMessage.isNotEmpty) {
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ onPressed: () {
+ context.pop();
+ },
+ icon: Assets.icons.arrowLeft.svg(
+ width: 24,
+ height: 24,
+ colorFilter: const ColorFilter.mode(
+ sandColor,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ title: Text(cubit.title, style: context.textStyles.headingSmall),
+ backgroundColor: Colors.white,
+ elevation: 0,
+ scrolledUnderElevation: 0,
+ surfaceTintColor: Colors.transparent,
+ ),
+ backgroundColor: Colors.white,
+ body: Center(
+ child: Text(state.errorMessage),
+ ),
+ );
+ }
+ return Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ onPressed: () {
+ context.pop();
+ },
+ icon: Assets.icons.arrowLeft.svg(
+ width: 24,
+ height: 24,
+ colorFilter: ColorFilter.mode(
+ sandColor,
+ BlendMode.srcIn,
+ ),
+ ),
+ ),
+ title: Text(cubit.title, style: context.textStyles.bodySmall),
+ backgroundColor: Colors.white,
+ elevation: 0,
+ scrolledUnderElevation: 0,
+ surfaceTintColor: Colors.transparent,
+ ),
+ body: Stack(
+ children: [
+ WebViewWidget(controller: webviewCubit.controller),
+ if (state.isLoading)
+ Center(
+ child: CircularProgressIndicator(
+ color: sandColor,
+ strokeWidth: 2,
+ backgroundColor: sandColor,
+ strokeCap: StrokeCap.round,
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/comwell_key_app/lib/routing/app_router.dart b/comwell_key_app/lib/routing/app_router.dart
index 1211ed18..66a16850 100644
--- a/comwell_key_app/lib/routing/app_router.dart
+++ b/comwell_key_app/lib/routing/app_router.dart
@@ -41,6 +41,7 @@ import 'package:comwell_key_app/overview/cubit/overview_cubit.dart';
import 'package:comwell_key_app/overview/repository/overview_repository.dart';
import 'package:comwell_key_app/pregistration/cubit/preregistration_cubit.dart';
import 'package:comwell_key_app/pregistration/preregistration_flow.dart';
+import 'package:comwell_key_app/presentation/screens/webview/webview_route.dart';
import 'package:comwell_key_app/profile/profile_repository.dart';
import 'package:comwell_key_app/received_shared_booking/cubit/received_shared_booking_cubit.dart';
import 'package:comwell_key_app/received_shared_booking/received_shared_booking_page.dart';
@@ -82,6 +83,8 @@ final _rootNavigatorKey = GlobalKey<NavigatorState>();
final rootNavigatorKey = _rootNavigatorKey;
final _shellNavigatorKey = GlobalKey<NavigatorState>();
+final authExceptions = [AppRoutes.webview];
+
final router = GoRouter(
initialLocation: AppRoutes.overview,
navigatorKey: _rootNavigatorKey,
@@ -90,6 +93,7 @@ final router = GoRouter(
redirect: (context, state) async {
final authRepo = locator<AuthenticationRepository>();
final isLoggedIn = await authRepo.isLoggedIn();
+ final isException = authExceptions.contains(state.matchedLocation);
if (state.uri.host == 'share-room') {
final sharingType = state.uri.queryParameters['sharingType'];
@@ -105,6 +109,10 @@ final router = GoRouter(
return uri.toString();
}
+ if (isException) {
+ return null;
+ }
+
if (!isLoggedIn) {
const forced = false;
return "${AppRoutes.login}?forced=$forced";
@@ -127,6 +135,7 @@ final router = GoRouter(
usageTrackingPermissionRoute,
overviewRoute,
redeemRoute,
+ webviewRoute,
ShellRoute(
navigatorKey: _shellNavigatorKey,
parentNavigatorKey: _rootNavigatorKey,
diff --git a/comwell_key_app/lib/routing/app_routes.dart b/comwell_key_app/lib/routing/app_routes.dart
index 4fb10b25..d2c01b3e 100644
--- a/comwell_key_app/lib/routing/app_routes.dart
+++ b/comwell_key_app/lib/routing/app_routes.dart
@@ -46,6 +46,7 @@ enum AppRoutes {
static const forceLogin = "/login?forced=true";
static const overview = "/overview";
static const redeem = "/redeem";
+ static const webview = "/webview";
static const onboardingBluetooth = "/onboarding/bluetooth";
static const onboardingNotification = "/onboarding/notification";
static const onboardingUsageTracking = "/onboarding/usage-tracking";
diff --git a/comwell_key_app/pubspec.yaml b/comwell_key_app/pubspec.yaml
index fc27ee85..9c5673cd 100644
--- a/comwell_key_app/pubspec.yaml
+++ b/comwell_key_app/pubspec.yaml
@@ -68,7 +68,8 @@ dependencies:
app_settings: ^7.0.0
internet_connection_checker_plus: ^2.9.1+2
app_tracking_transparency: ^2.0.6+1
-
+ webview_flutter: ^4.13.1
+
dependency_overrides:
#Remove override when slider button updates
vibration: 3.1.3