6177214e-ce7c-49e3-99de-ff9721b26f63 — Commit a8d0e93c

AuthorMikkel Thygesen<mikkelet@gmail.com>
Date2026-02-11 22:41:01 +0100
3770: Updated structure around auth

Changed files

comwell_key_app/assets/msal/msal_config_dev.json   |   2 +-
 comwell_key_app/assets/msal/msal_config_prod.json  |   2 +-
 comwell_key_app/assets/msal/msal_config_stage.json |   2 +-
 .../redeem_debug/bloc/redeem_cubit.freezed.dart    | 271 +++++++++++++++++++++
 .../authentication/authentication_repository.dart  | 229 ++---------------
 comwell_key_app/lib/data/remote/msal_service.dart  |  75 ++++++
 .../lib/key/repository/key_repository.dart         |  23 +-
 comwell_key_app/lib/login/cubit/login_cubit.dart   |  17 +-
 comwell_key_app/lib/login/login_route.dart         |   2 +-
 comwell_key_app/lib/main.dart                      |   5 +-
 .../lib/overview/cubit/overview_cubit.dart         |   4 +-
 comwell_key_app/lib/overview/overview_page.dart    |   2 -
 .../overview/repository/overview_repository.dart   | 158 +++++-------
 .../lib/profile/profile_repository.dart            |  31 +--
 .../lib/redeem_debug/bloc/redeem_cubit.dart        |  35 +++
 .../redeem_debug/invitation_code_formatter.dart    |  40 ---
 .../redeem_debug/invitation_code_textfield.dart    |  51 ----
 comwell_key_app/lib/redeem_debug/redeem_page.dart  | 117 ++++-----
 comwell_key_app/lib/redeem_debug/redeem_route.dart |   9 +
 .../widgets/invitation_code_formatter.dart         |  40 +++
 .../widgets/invitation_code_textfield.dart         |  41 ++++
 comwell_key_app/lib/routing/app_router.dart        |  14 +-
 comwell_key_app/lib/routing/app_routes.dart        |   3 +-
 comwell_key_app/lib/services/api.dart              | 109 ++++-----
 comwell_key_app/lib/services/exceptions.dart       |   5 +
 comwell_key_app/lib/services/http_client.dart      |  28 ++-
 .../interceptors/response_handle_interceptor.dart  | 203 ++++-----------
 comwell_key_app/lib/settings/settings_page.dart    |  61 +++--
 comwell_key_app/lib/utils/env_utils.dart           |  10 +
 comwell_key_app/lib/utils/locator.dart             |  84 ++++---
 comwell_key_app/lib/utils/seos_repository.dart     |  50 ++--
 comwell_key_app/scripts/run_prod.sh                |   1 +
 .../authentication_bloc_test.dart                  |  41 +---
 .../authentication_repository.dart                 |  13 +-
 .../test/key_test/key_repository_test.dart         |  66 +++--
 35 files changed, 958 insertions(+), 886 deletions(-)

Diff

diff --git a/comwell_key_app/assets/msal/msal_config_dev.json b/comwell_key_app/assets/msal/msal_config_dev.json
index c4f0f1a6..f8df1ad1 100644
--- a/comwell_key_app/assets/msal/msal_config_dev.json
+++ b/comwell_key_app/assets/msal/msal_config_dev.json
@@ -1,6 +1,6 @@
{
"client_id" : "19a8eb05-01e0-4076-9db3-34bcfefd67d8",
- "redirect_uri" : "msauth://com.comwell.phoenix.dev/PsrsGQrGkFzRWUJOtomYw29Pm1o=",
+ "redirect_uri" : "msauth://com.comwell.phoenix.dev/VzSiQcXRmi2kyjzcA+mYLEtbGVs=",
"shared_device_mode_supported": true,
"account_mode": "MULTIPLE",
"broker_redirect_uri_registered": false,
diff --git a/comwell_key_app/assets/msal/msal_config_prod.json b/comwell_key_app/assets/msal/msal_config_prod.json
index 88c8a41a..9c151074 100644
--- a/comwell_key_app/assets/msal/msal_config_prod.json
+++ b/comwell_key_app/assets/msal/msal_config_prod.json
@@ -1,6 +1,6 @@
{
"client_id" : "19a8eb05-01e0-4076-9db3-34bcfefd67d8",
- "redirect_uri" : "msauth://com.comwell.phoenix/PsrsGQrGkFzRWUJOtomYw29Pm1o=",
+ "redirect_uri" : "msauth://com.comwell.phoenix/VzSiQcXRmi2kyjzcA+mYLEtbGVs=",
"shared_device_mode_supported": true,
"account_mode": "MULTIPLE",
"broker_redirect_uri_registered": false,
diff --git a/comwell_key_app/assets/msal/msal_config_stage.json b/comwell_key_app/assets/msal/msal_config_stage.json
index 9c773fa9..7023ca11 100644
--- a/comwell_key_app/assets/msal/msal_config_stage.json
+++ b/comwell_key_app/assets/msal/msal_config_stage.json
@@ -1,6 +1,6 @@
{
"client_id" : "19a8eb05-01e0-4076-9db3-34bcfefd67d8",
- "redirect_uri" : "msauth://com.comwell.phoenix.stage/PsrsGQrGkFzRWUJOtomYw29Pm1o=",
+ "redirect_uri" : "msauth://com.comwell.phoenix.stage/VzSiQcXRmi2kyjzcA+mYLEtbGVs=",
"shared_device_mode_supported": true,
"account_mode": "MULTIPLE",
"broker_redirect_uri_registered": false,
diff --git a/comwell_key_app/lib/.generated/redeem_debug/bloc/redeem_cubit.freezed.dart b/comwell_key_app/lib/.generated/redeem_debug/bloc/redeem_cubit.freezed.dart
new file mode 100644
index 00000000..d7c44740
--- /dev/null
+++ b/comwell_key_app/lib/.generated/redeem_debug/bloc/redeem_cubit.freezed.dart
@@ -0,0 +1,271 @@
+// 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 '../../../redeem_debug/bloc/redeem_cubit.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+// dart format off
+T _$identity<T>(T value) => value;
+/// @nodoc
+mixin _$RedeemScreenState {
+
+ bool get isLoading;
+/// Create a copy of RedeemScreenState
+/// with the given fields replaced by the non-null parameter values.
+@JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+$RedeemScreenStateCopyWith<RedeemScreenState> get copyWith => _$RedeemScreenStateCopyWithImpl<RedeemScreenState>(this as RedeemScreenState, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is RedeemScreenState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,isLoading);
+
+@override
+String toString() {
+ return 'RedeemScreenState(isLoading: $isLoading)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class $RedeemScreenStateCopyWith<$Res> {
+ factory $RedeemScreenStateCopyWith(RedeemScreenState value, $Res Function(RedeemScreenState) _then) = _$RedeemScreenStateCopyWithImpl;
+@useResult
+$Res call({
+ bool isLoading
+});
+
+
+
+
+}
+/// @nodoc
+class _$RedeemScreenStateCopyWithImpl<$Res>
+ implements $RedeemScreenStateCopyWith<$Res> {
+ _$RedeemScreenStateCopyWithImpl(this._self, this._then);
+
+ final RedeemScreenState _self;
+ final $Res Function(RedeemScreenState) _then;
+
+/// Create a copy of RedeemScreenState
+/// with the given fields replaced by the non-null parameter values.
+@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,}) {
+ return _then(_self.copyWith(
+isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,
+ ));
+}
+
+}
+
+
+/// Adds pattern-matching-related methods to [RedeemScreenState].
+extension RedeemScreenStatePatterns on RedeemScreenState {
+/// 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( _RedeemScreenState value)? $default,{required TResult orElse(),}){
+final _that = this;
+switch (_that) {
+case _RedeemScreenState() 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( _RedeemScreenState value) $default,){
+final _that = this;
+switch (_that) {
+case _RedeemScreenState():
+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( _RedeemScreenState value)? $default,){
+final _that = this;
+switch (_that) {
+case _RedeemScreenState() 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)? $default,{required TResult orElse(),}) {final _that = this;
+switch (_that) {
+case _RedeemScreenState() when $default != null:
+return $default(_that.isLoading);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) $default,) {final _that = this;
+switch (_that) {
+case _RedeemScreenState():
+return $default(_that.isLoading);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)? $default,) {final _that = this;
+switch (_that) {
+case _RedeemScreenState() when $default != null:
+return $default(_that.isLoading);case _:
+ return null;
+
+}
+}
+
+}
+
+/// @nodoc
+
+
+class _RedeemScreenState implements RedeemScreenState {
+ const _RedeemScreenState({this.isLoading = false});
+
+
+@override@JsonKey() final bool isLoading;
+
+/// Create a copy of RedeemScreenState
+/// with the given fields replaced by the non-null parameter values.
+@override @JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+_$RedeemScreenStateCopyWith<_RedeemScreenState> get copyWith => __$RedeemScreenStateCopyWithImpl<_RedeemScreenState>(this, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _RedeemScreenState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,isLoading);
+
+@override
+String toString() {
+ return 'RedeemScreenState(isLoading: $isLoading)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class _$RedeemScreenStateCopyWith<$Res> implements $RedeemScreenStateCopyWith<$Res> {
+ factory _$RedeemScreenStateCopyWith(_RedeemScreenState value, $Res Function(_RedeemScreenState) _then) = __$RedeemScreenStateCopyWithImpl;
+@override @useResult
+$Res call({
+ bool isLoading
+});
+
+
+
+
+}
+/// @nodoc
+class __$RedeemScreenStateCopyWithImpl<$Res>
+ implements _$RedeemScreenStateCopyWith<$Res> {
+ __$RedeemScreenStateCopyWithImpl(this._self, this._then);
+
+ final _RedeemScreenState _self;
+ final $Res Function(_RedeemScreenState) _then;
+
+/// Create a copy of RedeemScreenState
+/// with the given fields replaced by the non-null parameter values.
+@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,}) {
+ return _then(_RedeemScreenState(
+isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,
+ ));
+}
+
+
+}
+
+// dart format on
diff --git a/comwell_key_app/lib/authentication/authentication_repository.dart b/comwell_key_app/lib/authentication/authentication_repository.dart
index 63a69092..3b6c9719 100644
--- a/comwell_key_app/lib/authentication/authentication_repository.dart
+++ b/comwell_key_app/lib/authentication/authentication_repository.dart
@@ -1,235 +1,60 @@
import 'dart:async';
-import 'package:comwell_key_app/.generated/assets/assets.gen.dart';
-import 'package:comwell_key_app/authentication/enum/authentication_status.dart';
+import 'package:comwell_key_app/data/remote/msal_service.dart';
import 'package:comwell_key_app/database/comwell_db.dart';
-import 'package:comwell_key_app/services/api.dart';
import 'package:comwell_key_app/tracking/comwell_tracking.dart';
-import 'package:comwell_key_app/utils/env_utils.dart';
import 'package:comwell_key_app/utils/secure_storage.dart';
import 'package:comwell_key_app/common/const.dart' as constants;
import 'package:firebase_analytics/firebase_analytics.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:msal_auth/msal_auth.dart';
-import '../utils/locator.dart';
import '../utils/seos_repository.dart';
class AuthenticationRepository {
- final SecureStorage secureStorage = SecureStorage();
- final _controller = StreamController<AuthenticationStatus>.broadcast();
- late final broadcast = _controller.stream.asBroadcastStream();
- AuthenticationStatus statusBuffer = AuthenticationStatus.unknown;
- final seos = locator<SeosRepository>();
- final database = locator<ComwellDatabase>();
- final api = Api();
- late final String configFilePath;
- late final String authorityUrl;
- late final MultipleAccountPca msAuth;
- final scopes = dotenv.ENTRA_API_URL.split(',');
-
- Future<void> init() async {
- final clientId = dotenv.ENTRA_ID_CLIENT_ID;
- final redirect = dotenv.ENTRA_ID_REDIRECT_URL;
-
- switch (appFlavor?.toLowerCase()) {
- case "develop":
- configFilePath = Assets.msal.msalConfigDev;
- authorityUrl = dotenv.ENTRA_ID_AUTHORITY_URL;
- case "stage":
- configFilePath = Assets.msal.msalConfigStage;
- authorityUrl = dotenv.ENTRA_ID_AUTHORITY_URL;
- case "prod":
- configFilePath = Assets.msal.msalConfigProd;
- authorityUrl = dotenv.ENTRA_ID_AUTHORITY_URL;
- default:
- throw Exception("Missing config file for flavor $appFlavor");
- }
- msAuth = await MultipleAccountPca.create(
- clientId: clientId,
- androidConfig: AndroidConfig(
- configFilePath: configFilePath,
- redirectUri: redirect,
- ),
- appleConfig: AppleConfig(
- authorityType: AuthorityType.b2c,
- broker: Broker.webView,
- authority: authorityUrl,
- ),
- );
- }
-
- AuthenticationRepository() {
- broadcast.listen((status) {
- statusBuffer = status;
- // Don't clear keychain values on every status change
- // Only clear when actually logging out
- });
-
- _controller.sink.add(AuthenticationStatus.unknown);
-
+ final SecureStorage _secureStorage;
+ final SeosRepository _seosRepository;
+ final ComwellDatabase _database;
+ final ComwellTracking _comwellTracking;
+ final MSALService _msalService;
+
+ AuthenticationRepository(
+ this._seosRepository,
+ this._database,
+ this._secureStorage,
+ this._comwellTracking,
+ this._msalService,
+ ) {
FirebaseAnalytics.instance.setUserProperty(
name: 'login_status',
value: 'false',
);
}
- Stream<AuthenticationStatus> get status => _controller.stream;
-
- Future<void> _onAuthResult(AuthenticationStatus status) async {
- try {
- if (status == AuthenticationStatus.authenticated) {
- await seos.startMobilePlugin();
- }
- } catch (e, st) {
- if (kDebugMode) print("e=$e, $st");
- }
- }
-
Future<bool> isLoggedIn() async {
final token = await secureStorage.read(constants.accessToken);
if (token == null) return false;
return token.isNotEmpty;
}
- Future<void> logIn() async {
- await FirebaseAnalytics.instance.logLogin();
+ Future<void> logOut() async {
+ _comwellTracking.trackEvent('logout');
+ await _seosRepository.terminateEndpoint();
+ await _database.deleteDatabase();
+ await secureStorage.deleteAll();
await FirebaseAnalytics.instance.setUserProperty(
name: 'login_status',
- value: 'true',
+ value: 'false',
);
- // Ensure DB is re-instantiated after login
- try {
- registerDatabase();
- } catch (e) {
- // no op
- }
-
- // Emit status and trigger side effects
- _emitStatus(AuthenticationStatus.authenticated);
- }
-
- void _emitStatus(AuthenticationStatus status) {
- _controller.sink.add(status);
- _onAuthResult(status);
- }
-
- Future<void> logOut({bool forced = false}) async {
- try {
- // Delete database BEFORE deleting cipher key from secure storage
- // The cipher key is needed to decrypt the database
- try {
- await seos.terminateEndpoint();
- await database.deleteDatabase();
- } catch (e) {
- debugPrint("Error deleting database during logout: $e");
- // Continue with logout even if database deletion fails
- }
-
- // Clear local storage (including cipher key) after database is deleted
- await secureStorage.deleteAll();
-
- // await msAuth.removeAccount();
-
- // Track the logout event
- locator<ComwellTracking>().trackEvent('logout');
- await FirebaseAnalytics.instance.setUserProperty(
- name: 'login_status',
- value: 'false',
- );
-
- // Update authentication status
- _emitStatus(
- forced ? AuthenticationStatus.forcedUnauthenticated : AuthenticationStatus.unauthenticated,
- );
- } catch (e) {
- debugPrint("Error during logout: $e");
- // Even if logout fails, still clear local data and update status
- try {
- await database.deleteDatabase();
- } catch (dbError) {
- debugPrint("Error deleting database in catch block: $dbError");
- }
- await secureStorage.deleteAll();
-
- _emitStatus(
- forced ? AuthenticationStatus.forcedUnauthenticated : AuthenticationStatus.unauthenticated,
- );
- }
}
- void dispose() => _controller.close();
-
Future<void> openAuth(Prompt prompt) async {
- try {
- final token = await msAuth.acquireToken(scopes: scopes, prompt: prompt);
- await loginWithCode(token);
- } catch (e) {
- debugPrint("qqq e=$e");
- }
- }
-
- Future<bool> doesTokenExist() async {
- try {
- // First check if we have a token in secure storage
- final storedToken = await secureStorage.read(constants.accessToken);
- if (storedToken == null || storedToken.isEmpty) {
- return false;
- }
-
- // Then try to get a fresh token silently
- await accessToken;
-
- return true;
- } catch (e) {
- return false;
- }
- }
-
- Future<void> loginWithCode(AuthenticationResult code) async {
- await secureStorage.write(constants.accessToken, code.accessToken);
- await secureStorage.write(constants.identifier, code.account.id);
- await secureStorage.write(constants.correlationId, code.correlationId ?? '');
- await logIn();
- }
-
- Future<String> get accessToken async {
- try {
- //First check if we have a token in secure storage
- final storedToken = await secureStorage.read(constants.accessToken);
- if (storedToken != null && storedToken.isNotEmpty) {
- return storedToken;
- }
-
- // If no stored token, try to get a fresh one silently
- return (await msAuth.acquireTokenSilent(scopes: scopes, authority: authorityUrl)).accessToken;
- } catch (e) {
- throw Exception('No valid access token available');
- }
+ await _msalService.openAuth(prompt);
+ await FirebaseAnalytics.instance.logLogin();
+ await FirebaseAnalytics.instance.setUserProperty(
+ name: 'login_status',
+ value: 'true',
+ );
}
- Future<AuthenticationResult> acquireTokenSilent(String identifier) async {
- if (identifier.isEmpty) {
- throw Exception('Cannot acquire token: identifier is empty');
- }
-
- try {
- debugPrint('Acquiring token silently for identifier: ${identifier.substring(0, 8)}...');
- final result = await msAuth.acquireTokenSilent(
- scopes: scopes,
- authority: authorityUrl,
- identifier: identifier,
- );
- debugPrint('Token acquired successfully');
- return result;
- } on PlatformException catch (e) {
- debugPrint('PlatformException during silent token acquisition: ${e.code} - ${e.message}');
- rethrow;
- } catch (e) {
- debugPrint('Error during silent token acquisition: $e');
- rethrow;
- }
- }
+ Future<String?> get accessToken => _secureStorage.read(constants.accessToken);
}
diff --git a/comwell_key_app/lib/data/remote/msal_service.dart b/comwell_key_app/lib/data/remote/msal_service.dart
new file mode 100644
index 00000000..debdfd19
--- /dev/null
+++ b/comwell_key_app/lib/data/remote/msal_service.dart
@@ -0,0 +1,75 @@
+import 'package:comwell_key_app/.generated/assets/assets.gen.dart';
+import 'package:comwell_key_app/services/exceptions.dart';
+import 'package:comwell_key_app/utils/env_utils.dart';
+import 'package:comwell_key_app/utils/secure_storage.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
+import 'package:msal_auth/msal_auth.dart';
+
+import '../../common/const.dart' as constants;
+
+class MSALService {
+ final SecureStorage _secureStorage;
+
+ final scopes = dotenv.ENTRA_API_URL.split(',');
+ late final PublicClientApplication msAuth;
+ late final String configFilePath;
+ final String authorityUrl = dotenv.ENTRA_ID_AUTHORITY_URL;
+
+ MSALService(this._secureStorage);
+
+ Future<void> init() async {
+ try {
+ final clientId = dotenv.ENTRA_ID_CLIENT_ID;
+ final redirect = dotenv.ENTRA_ID_REDIRECT_URL;
+ switch (appFlavor?.toLowerCase()) {
+ case "develop":
+ configFilePath = Assets.msal.msalConfigDev;
+ case "stage":
+ configFilePath = Assets.msal.msalConfigStage;
+ case "prod":
+ configFilePath = Assets.msal.msalConfigProd;
+ default:
+ throw Exception("Missing config file for flavor $appFlavor");
+ }
+ msAuth = await MultipleAccountPca.create(
+ clientId: clientId,
+ androidConfig: AndroidConfig(
+ configFilePath: configFilePath,
+ redirectUri: redirect,
+ ),
+ appleConfig: AppleConfig(
+ authorityType: AuthorityType.b2c,
+ broker: Broker.webView,
+ authority: authorityUrl,
+ ),
+ );
+ } catch (e) {
+ print("qqq msauth init=$e");
+ rethrow;
+ }
+ }
+
+ Future<void> openAuth(Prompt prompt) async {
+ final response = await msAuth.acquireToken(scopes: scopes, prompt: prompt);
+ print("qqq token=${response.accessToken}");
+ print("qqq accId=${response.account.id}");
+ print("qqq corrId=${response.correlationId}");
+ print("qqq id=${response.idToken}");
+
+ await _secureStorage.write(constants.accessToken, response.accessToken);
+ await _secureStorage.write(constants.identifier, response.account.id);
+ await _secureStorage.write(constants.correlationId, response.correlationId ?? '');
+ }
+
+ Future<void> acquireTokenSilent() async {
+ final msalIdentifier = await _secureStorage.read(constants.identifier);
+ if (msalIdentifier == null) throw UnauthorizedException();
+ final response = await msAuth.acquireTokenSilent(
+ scopes: scopes,
+ authority: authorityUrl,
+ identifier: msalIdentifier,
+ );
+ await _secureStorage.write(constants.accessToken, response.accessToken);
+ }
+}
diff --git a/comwell_key_app/lib/key/repository/key_repository.dart b/comwell_key_app/lib/key/repository/key_repository.dart
index ea1dff1a..f3e0ff24 100644
--- a/comwell_key_app/lib/key/repository/key_repository.dart
+++ b/comwell_key_app/lib/key/repository/key_repository.dart
@@ -6,14 +6,10 @@ import 'package:seos_mobile_keys_plugin/app_usage_api.dart';
import 'package:seos_mobile_keys_plugin/seos_mobile_keys_plugin.dart';
class KeyRepository {
- final SeosMobileKeysPlugin seosMobileKeysPlugin;
- final DeviceInfoPlugin deviceInfoPlugin;
+ final SeosMobileKeysPlugin _seosMobileKeysPlugin;
bool _isScanning = false;
- KeyRepository({
- required this.seosMobileKeysPlugin,
- required this.deviceInfoPlugin,
- });
+ KeyRepository(this._seosMobileKeysPlugin);
Future<void> checkDeviceInfo() async {
try {
@@ -55,7 +51,7 @@ class KeyRepository {
Future<void> stopScanning() async {
try {
- await seosMobileKeysPlugin.stopReaderScan();
+ await _seosMobileKeysPlugin.stopReaderScan();
_isScanning = false;
} catch (e) {
throw Exception('Failed to stop scanning - ${e.toString()}');
@@ -73,8 +69,11 @@ class KeyRepository {
1,
2,
];
- await seosMobileKeysPlugin.startReaderScan(
- MobileKeysScanMode.optimizePerformance, openingTypes, lockServiceCodes);
+ await _seosMobileKeysPlugin.startReaderScan(
+ MobileKeysScanMode.optimizePerformance,
+ openingTypes,
+ lockServiceCodes,
+ );
if (kDebugMode) {
debugPrint('startScanning: "scanned');
}
@@ -85,7 +84,7 @@ class KeyRepository {
Future<void> openClosestReader() async {
// try {
- await seosMobileKeysPlugin.openClosestReader();
+ await _seosMobileKeysPlugin.openClosestReader();
// } catch (e) {
// throw Exception('Failed to open closest reader - ${e.toString()}');
// }
@@ -94,7 +93,7 @@ class KeyRepository {
Future<void> setRootOpeningTrigger() async {
if (!Platform.isAndroid) return;
try {
- await seosMobileKeysPlugin.setRootOpeningTrigger();
+ await _seosMobileKeysPlugin.setRootOpeningTrigger();
} catch (e) {
// Do nothing
}
@@ -103,7 +102,7 @@ class KeyRepository {
Future<void> removeRootOpeningTrigger() async {
if (!Platform.isAndroid) return;
try {
- await seosMobileKeysPlugin.removeRootOpeningTrigger();
+ await _seosMobileKeysPlugin.removeRootOpeningTrigger();
} catch (e) {
// Do nothing
}
diff --git a/comwell_key_app/lib/login/cubit/login_cubit.dart b/comwell_key_app/lib/login/cubit/login_cubit.dart
index 5f07b93f..45429504 100644
--- a/comwell_key_app/lib/login/cubit/login_cubit.dart
+++ b/comwell_key_app/lib/login/cubit/login_cubit.dart
@@ -6,15 +6,24 @@ import 'package:msal_auth/msal_auth.dart';
part 'login_state.dart';
class LoginCubit extends Cubit<LoginState> {
- LoginCubit({required this.forced, required this.authRepository}) : super(LoginState());
+ LoginCubit(
+ this._authRepository, {
+ required this.forced,
+ }) : super(LoginState());
final bool forced;
- final AuthenticationRepository authRepository;
+ final AuthenticationRepository _authRepository;
+
+ Future<void> init() async {
+ if (forced) {
+ await _authRepository.logOut();
+ }
+ }
Future<void> login() async {
- await authRepository.openAuth(Prompt.login);
+ await _authRepository.openAuth(Prompt.login);
}
Future<void> createAccount() async {
- await authRepository.openAuth(Prompt.create);
+ await _authRepository.openAuth(Prompt.create);
}
}
diff --git a/comwell_key_app/lib/login/login_route.dart b/comwell_key_app/lib/login/login_route.dart
index 871d8522..a37b9654 100644
--- a/comwell_key_app/lib/login/login_route.dart
+++ b/comwell_key_app/lib/login/login_route.dart
@@ -13,7 +13,7 @@ RouteBase get loginRoute => GoRoute(
final forced = bool.tryParse(queryForced) ?? false;
return BlocProvider(
create: (context) => LoginCubit(
- authRepository: locator.get(),
+ locator.get(),
forced: forced,
),
child: const LoginPage(),
diff --git a/comwell_key_app/lib/main.dart b/comwell_key_app/lib/main.dart
index 652bf74e..07b96c30 100644
--- a/comwell_key_app/lib/main.dart
+++ b/comwell_key_app/lib/main.dart
@@ -1,4 +1,5 @@
import 'package:comwell_key_app/authentication/authentication_repository.dart';
+import 'package:comwell_key_app/data/remote/msal_service.dart';
import 'package:comwell_key_app/utils/env_utils.dart';
import 'package:comwell_key_app/utils/firebase.dart';
import 'package:comwell_key_app/utils/locator.dart';
@@ -70,11 +71,11 @@ class _BootstrapApp extends StatelessWidget {
});
await locator<ComwellPreferences>().init();
- await locator<AuthenticationRepository>().init();
+ await locator<MSALService>().init();
await PaymentPlugin.initialize(
config: PaymentConfig(
- dio: HttpClient().dio,
+ dio: locator.get<ComwellHttpClient>().dio,
),
);
diff --git a/comwell_key_app/lib/overview/cubit/overview_cubit.dart b/comwell_key_app/lib/overview/cubit/overview_cubit.dart
index d1dd9048..08bb02c7 100644
--- a/comwell_key_app/lib/overview/cubit/overview_cubit.dart
+++ b/comwell_key_app/lib/overview/cubit/overview_cubit.dart
@@ -15,7 +15,9 @@ class OverviewCubit extends BaseCubit<OverviewState> {
OverviewCubit(
this._overviewRepository,
this._shareBookingRepository,
- ) : super(const OverviewState());
+ ) : super(const OverviewState()) {
+ fetchBookings();
+ }
Future<void> fetchBookings() async {
try {
diff --git a/comwell_key_app/lib/overview/overview_page.dart b/comwell_key_app/lib/overview/overview_page.dart
index dae29246..7284c531 100644
--- a/comwell_key_app/lib/overview/overview_page.dart
+++ b/comwell_key_app/lib/overview/overview_page.dart
@@ -34,8 +34,6 @@ class OverviewTabViewState extends State<OverviewPage>
super.initState();
_tabController = TabController(length: 3, vsync: this);
- final OverviewCubit overviewCubit = BlocProvider.of<OverviewCubit>(context);
- overviewCubit.fetchBookings();
}
@override
diff --git a/comwell_key_app/lib/overview/repository/overview_repository.dart b/comwell_key_app/lib/overview/repository/overview_repository.dart
index 68bf2279..1188430c 100644
--- a/comwell_key_app/lib/overview/repository/overview_repository.dart
+++ b/comwell_key_app/lib/overview/repository/overview_repository.dart
@@ -1,115 +1,83 @@
-import 'package:comwell_key_app/choose_share_room/choose_share_room_repository.dart';
-import 'package:comwell_key_app/database/comwell_db.dart';
import 'package:comwell_key_app/overview/models/booking.dart';
import 'package:comwell_key_app/overview/models/bookings.dart';
-import 'package:comwell_key_app/profile/profile_repository.dart';
import 'package:comwell_key_app/services/api.dart';
import 'package:comwell_key_app/services/mappers/booking_mapper.dart';
import 'package:comwell_key_app/services/models/booking_dto.dart';
-import 'package:comwell_key_app/utils/locator.dart' show locator, registerDatabase;
class OverviewRepository {
- final api = Api();
- final profileRepository = locator<ProfileRepository>();
- final chooseShareRoomRepository = locator<ChooseShareRoomRepository>();
+ final Api _api;
- OverviewRepository();
+ OverviewRepository(this._api);
Future<Bookings> fetchAllBookingsForUser() async {
- try {
- final currentBookings = await api.fetchCurrentBookingsForUser();
- final pastBookings = await api.fetchPastBookingsForUser();
- final cancelledBookings = await api.fetchCancelledBookingsForUser();
- final database = locator<ComwellDatabase>();
- try {
- await database.bookingsDao.insert(currentBookings);
- } catch (dbError) {
- // Check if it's a database corruption error
- if (dbError.toString().contains('file is not a database') ||
- dbError.toString().contains('code 26') ||
- dbError.toString().contains('SqliteException')) {
- // Recreate the database and try again
- await database.recreateDatabase();
- // Unregister and re-register the database to get a fresh instance
- if (locator.isRegistered<ComwellDatabase>()) {
- locator.resetLazySingleton<ComwellDatabase>();
- }
- registerDatabase();
- final newDatabase = locator<ComwellDatabase>();
- await newDatabase.bookingsDao.insert(currentBookings);
- } else {
- rethrow;
- }
- }
+ final currentBookings = await _api.fetchCurrentBookingsForUser();
+ final pastBookings = await _api.fetchPastBookingsForUser();
+ final cancelledBookings = await _api.fetchCancelledBookingsForUser();
+ final bookings = Bookings(
+ current: currentBookings.map((e) => e.toBooking()).toList(),
+ past: pastBookings.map((e) => e.toBooking()).toList(),
+ cancelled: cancelledBookings.map((e) => e.toBooking()).toList(),
+ );
- final bookings = Bookings(
- current:
- currentBookings.map((e) => e.toBooking()).toList(),
- past: pastBookings.map((e) => e.toBooking()).toList(),
- cancelled: cancelledBookings
- .map((e) => e.toBooking())
- .toList());
-
- return bookings;
- } catch (e) {
- throw Exception('Failed to fetch bookings $e');
- }
+ return bookings;
}
Future<Booking?> findBooking(String bookingReference, String lastName) async {
// needs implementation
final dto = BookingDTO(
- roomNumber: "1234",
- hotelCode: "CBO",
- firstName: "Hello",
- lastName: "World",
- bookerFirstName: "Hello",
- bookerLastName: "World",
- guests: [GuestDTO(id: 1, firstName: "Hello", lastName: "World")],
- confirmationNumber: "12345",
- dayIn: "31-12-2000",
- dayOut: "31-12-2000",
- cancelTime: "31-12-2000",
- status: "newReservation",
- isCancelled: false,
- bookTime: "31-12-2000",
- roomType: "??",
- adults: 3,
- children: 5,
- balance: 12345,
- isPrimaryGuest: false,
- maskedCardNumber: "1234567890",
- addOnItems: [
- BookingAddonItem("addOnItem1", "addOnItem1", 1, 100),
- BookingAddonItem("addOnItem2", "addOnItem2", 1, 200),
- BookingAddonItem("addOnItem3", "addOnItem3", 1, 300)
- ]);
+ roomNumber: "1234",
+ hotelCode: "CBO",
+ firstName: "Hello",
+ lastName: "World",
+ bookerFirstName: "Hello",
+ bookerLastName: "World",
+ guests: [GuestDTO(id: 1, firstName: "Hello", lastName: "World")],
+ confirmationNumber: "12345",
+ dayIn: "31-12-2000",
+ dayOut: "31-12-2000",
+ cancelTime: "31-12-2000",
+ status: "newReservation",
+ isCancelled: false,
+ bookTime: "31-12-2000",
+ roomType: "??",
+ adults: 3,
+ children: 5,
+ balance: 12345,
+ isPrimaryGuest: false,
+ maskedCardNumber: "1234567890",
+ addOnItems: [
+ BookingAddonItem("addOnItem1", "addOnItem1", 1, 100),
+ BookingAddonItem("addOnItem2", "addOnItem2", 1, 200),
+ BookingAddonItem("addOnItem3", "addOnItem3", 1, 300),
+ ],
+ );
return dto.toBooking();
}
- final mockBookings = [1, 2, 3].map((i) => Booking(
- id: "id$i",
- confirmationNumber: "crmConfirmationNumber$i",
- roomNumber: "roomNumber$i",
- startDate: DateTime.now(),
- endDate: DateTime.now(),
- reservationStatus: ReservationStatus.newreservation,
- image: "assets/images/no_current_bookings_background.jpeg",
- hotelName: "hotelName$i",
- roomType: "roomType$i",
- balance: 100,
- children: 3,
- adults: 3,
- hotelCode: "hotelCode$i",
- firstName: "firstName",
- lastName: "lastName",
- bookerFirstName: "bookerFirstName",
- bookerLastName: "bookerLastName",
- bookingDate: DateTime.now(),
- digitalCard: false,
- isPrimaryGuest: false,
- maskedCardNumber: "1234567890",
- addOnItems: [
- BookingAddonItem("addOnItem$i", "addOnItem$i", 1, 100)
- ]));
+ final mockBookings = [1, 2, 3].map(
+ (i) => Booking(
+ id: "id$i",
+ confirmationNumber: "crmConfirmationNumber$i",
+ roomNumber: "roomNumber$i",
+ startDate: DateTime.now(),
+ endDate: DateTime.now(),
+ reservationStatus: ReservationStatus.newreservation,
+ image: "assets/images/no_current_bookings_background.jpeg",
+ hotelName: "hotelName$i",
+ roomType: "roomType$i",
+ balance: 100,
+ children: 3,
+ adults: 3,
+ hotelCode: "hotelCode$i",
+ firstName: "firstName",
+ lastName: "lastName",
+ bookerFirstName: "bookerFirstName",
+ bookerLastName: "bookerLastName",
+ bookingDate: DateTime.now(),
+ digitalCard: false,
+ isPrimaryGuest: false,
+ maskedCardNumber: "1234567890",
+ addOnItems: [BookingAddonItem("addOnItem$i", "addOnItem$i", 1, 100)],
+ ),
+ );
}
diff --git a/comwell_key_app/lib/profile/profile_repository.dart b/comwell_key_app/lib/profile/profile_repository.dart
index 687e8e5a..0286495f 100644
--- a/comwell_key_app/lib/profile/profile_repository.dart
+++ b/comwell_key_app/lib/profile/profile_repository.dart
@@ -1,35 +1,30 @@
import 'package:comwell_key_app/authentication/authentication_repository.dart';
import 'package:comwell_key_app/database/comwell_db.dart';
import 'package:comwell_key_app/profile_settings/model/user.dart';
-import 'package:comwell_key_app/profile_settings/repostiory/profile_settings_repository.dart';
import 'package:comwell_key_app/services/api.dart';
import 'package:comwell_key_app/services/mappers/user_mapper.dart';
import 'package:comwell_key_app/services/models/user_dto.dart';
import 'package:comwell_key_app/utils/json.dart';
import 'package:comwell_key_app/utils/locator.dart';
-import 'package:comwell_key_app/utils/secure_storage.dart';
-import 'package:comwell_key_app/utils/seos_repository.dart';
import 'package:flutter/material.dart';
-import 'package:seos_mobile_keys_plugin/seos_mobile_keys_plugin.dart';
class ProfileRepository {
- final SecureStorage secureStorage = SecureStorage();
- final AuthenticationRepository authenticationRepository =
- locator<AuthenticationRepository>();
- final SeosMobileKeysPlugin seosMobileKeysPlugin =
- SeosRepository().seosMobileKeysPlugin;
- final ProfileSettingsRepository profileSettingsRepository =
- locator<ProfileSettingsRepository>();
- final Api api = Api();
+ final AuthenticationRepository _authenticationRepository;
+ final Api _api;
late User user;
+ ProfileRepository(
+ this._authenticationRepository,
+ this._api,
+ );
+
Future<void> logOut() async {
- await authenticationRepository.logOut();
+ await _authenticationRepository.logOut();
}
Future<User> signupForComwellClub(User user) async {
try {
- await api.signupForComwellClub();
+ await _api.signupForComwellClub();
return await _updateAndPersistUser(user);
} catch (e, st) {
debugPrint('Error during Comwell Club signup: $e');
@@ -39,7 +34,7 @@ class ProfileRepository {
}
Future<User> _updateAndPersistUser(User user) async {
- final userResponse = await api.updateUser(user.toSimpleUserDto());
+ final userResponse = await _api.updateUser(user.toSimpleUserDto());
final data = userResponse.data as Json;
final userDto = UserDto.fromJson(data);
final updatedUser = userDto.toUser();
@@ -48,15 +43,13 @@ class ProfileRepository {
}
Future<User> fetchRemoteProfile() async {
- final response = await api.fetchProfileSettings();
+ final response = await _api.fetchProfileSettings();
final data = response.data as Json;
final userDto = UserDto.fromJson(data);
final user = userDto.toUser();
return user;
}
-
-
Future<User?> _checkIfProfileSettingsExists() async {
try {
final user = await locator<ComwellDatabase>().userDAO.getUser();
@@ -69,7 +62,7 @@ class ProfileRepository {
Future<User> _fetchAndSaveProfileSettingsToDatabase() async {
try {
- final response = await api.fetchProfileSettings();
+ final response = await _api.fetchProfileSettings();
final data = response.data as Json;
final userDto = UserDto.fromJson(data);
final user = userDto.toUser();
diff --git a/comwell_key_app/lib/redeem_debug/bloc/redeem_cubit.dart b/comwell_key_app/lib/redeem_debug/bloc/redeem_cubit.dart
new file mode 100644
index 00000000..1b46d18a
--- /dev/null
+++ b/comwell_key_app/lib/redeem_debug/bloc/redeem_cubit.dart
@@ -0,0 +1,35 @@
+import 'package:comwell_key_app/base/base_cubit.dart';
+import 'package:comwell_key_app/utils/secure_storage.dart';
+import 'package:comwell_key_app/utils/seos_repository.dart';
+import 'package:flutter/material.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part '../../.generated/redeem_debug/bloc/redeem_cubit.freezed.dart';
+
+class RedeemCubit extends BaseCubit<RedeemScreenState> {
+ RedeemCubit(this._seosRepository, this._secureStorage) : super(const RedeemScreenState());
+
+ final SeosRepository _seosRepository;
+ final SecureStorage _secureStorage;
+ final invitationCodeFieldController = TextEditingController();
+
+ Future<void> onRedeem() async {
+ try {
+ safeEmit(state.copyWith(isLoading: true));
+ final code = invitationCodeFieldController.value.toString();
+ await _seosRepository.setupEndpoint(code);
+ await _secureStorage.write("invitation", code);
+ } catch (e, st) {
+ logError(e, st);
+ } finally {
+ safeEmit(state.copyWith(isLoading: false));
+ }
+ }
+}
+
+@freezed
+abstract class RedeemScreenState with _$RedeemScreenState {
+ const factory RedeemScreenState({
+ @Default(false) bool isLoading,
+ }) = _RedeemScreenState;
+}
diff --git a/comwell_key_app/lib/redeem_debug/invitation_code_formatter.dart b/comwell_key_app/lib/redeem_debug/invitation_code_formatter.dart
deleted file mode 100644
index 7d10efe6..00000000
--- a/comwell_key_app/lib/redeem_debug/invitation_code_formatter.dart
+++ /dev/null
@@ -1,40 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-
-class InvitationCodeFormatter extends TextInputFormatter {
- final String sample;
- final String separator;
-
- InvitationCodeFormatter({
- required this.sample,
- required this.separator,
- });
-
- @override
- TextEditingValue formatEditUpdate(
- TextEditingValue oldValue,
- TextEditingValue newValue
- ) {
- if (newValue.text.length > oldValue.text.length) {
- if (newValue.text.characters.last == separator ||
- newValue.text.length > sample.length) {
- return oldValue;
- }
- if (newValue.text.length < sample.length &&
- sample[newValue.text.length - 1] == separator) {
- return TextEditingValue(
- text:
- '${oldValue.text}$separator${newValue.text.substring(newValue.text.length - 1)}'
- .toUpperCase(),
- selection:
- TextSelection.collapsed(
- offset: newValue.selection.end + 1)
- );
- }
- }
- return TextEditingValue(
- text: newValue.text.toUpperCase(),
- selection: newValue.selection,
- );
- }
-}
\ No newline at end of file
diff --git a/comwell_key_app/lib/redeem_debug/invitation_code_textfield.dart b/comwell_key_app/lib/redeem_debug/invitation_code_textfield.dart
deleted file mode 100644
index 6a430153..00000000
--- a/comwell_key_app/lib/redeem_debug/invitation_code_textfield.dart
+++ /dev/null
@@ -1,51 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-
-import 'invitation_code_formatter.dart';
-
-class InvitationCodeTextField extends StatelessWidget {
- InvitationCodeTextField({super.key});
-
- String? get invitationCode =>
- _formKey.currentState?.validate() == true ?
- _valueController.text :
- null;
-
- final _formKey = GlobalKey<FormState>();
- late final _valueController = TextEditingController();
-
- @override
- Widget build(BuildContext context) =>
- Form(
- key: _formKey,
- child: TextFormField(
- autofocus: true,
- controller: _valueController,
- inputFormatters: [
- FilteringTextInputFormatter.allow(RegExp(r'[0-9a-zA-Z-]')),
- InvitationCodeFormatter(
- sample: 'xxxx-xxxx-xxxx-xxxx',
- separator: '-'
- ),
- ],
- decoration: const InputDecoration(
- border: OutlineInputBorder(),
- hintText: 'XXXX-XXXX-XXXX-XXXX',
- contentPadding: EdgeInsets.symmetric(
- vertical: 10.0,
- horizontal: 10.0
- )
- ),
- validator: (value) {
- if (value == null ||
- value.isEmpty) {
- return 'Please enter an invitation code';
- } else if (value.replaceAll('-', '').length != 16) {
- return 'Please enter a valid invitation code';
- } else {
- return null;
- }
- },
- ),
- );
-}
\ No newline at end of file
diff --git a/comwell_key_app/lib/redeem_debug/redeem_page.dart b/comwell_key_app/lib/redeem_debug/redeem_page.dart
index 36508b8c..43fbb0e0 100644
--- a/comwell_key_app/lib/redeem_debug/redeem_page.dart
+++ b/comwell_key_app/lib/redeem_debug/redeem_page.dart
@@ -1,89 +1,80 @@
import 'package:comwell_key_app/common/components/comwell_app_bar.dart';
+import 'package:comwell_key_app/redeem_debug/bloc/redeem_cubit.dart';
import 'package:comwell_key_app/routing/app_routes.dart';
-import 'package:comwell_key_app/utils/secure_storage.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
-import 'invitation_code_textfield.dart';
+import 'widgets/invitation_code_textfield.dart';
import '../common/extensions/scaffold_messenger_state_extension.dart';
-import '../utils/seos_repository.dart';
-class RedeemPage extends StatefulWidget {
+class RedeemPage extends StatelessWidget {
const RedeemPage({super.key});
@override
- State<StatefulWidget> createState() => _RedeemPage();
-}
-
-class _RedeemPage extends State<RedeemPage> {
- final _seosMobileKeysPlugin = SeosRepository().seosMobileKeysPlugin;
- late final _invitationCodeField = InvitationCodeTextField();
- SecureStorage secureStorage = SecureStorage();
- var _isRedeeming = false;
-
- @override
- Widget build(BuildContext context) =>
- Scaffold(
- extendBodyBehindAppBar: true,
- appBar: const ComwellAppBar(
- shouldShowAppBar: true),
- body: SafeArea(
- child: Center(
-
- child: Padding(
- padding: const EdgeInsets.all(15.0),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Text(
- 'Invitation Code:',
- style: Theme.of(context).textTheme.titleMedium,
- ),
- const SizedBox(height: 10.0),
- _invitationCodeField,
- const SizedBox(height: 15.0),
- ElevatedButton(
- onPressed: _isRedeeming
- ? null
- : () {
- _onTapRedeem(context);
- },
- child: _isRedeeming ? null : Text(
- 'Register',
- style: Theme.of(context).textTheme.titleLarge,
+ Widget build(BuildContext context) {
+ final cubit = context.read<RedeemCubit>();
+ return BlocBuilder<RedeemCubit, RedeemScreenState>(
+ builder: (context, state) {
+ return Scaffold(
+ extendBodyBehindAppBar: true,
+ appBar: const ComwellAppBar(shouldShowAppBar: true),
+ body: SafeArea(
+ child: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(15.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Text(
+ 'Invitation Code:',
+ style: Theme.of(context).textTheme.titleMedium,
),
- ),
- ],
+ const SizedBox(height: 10.0),
+ InvitationCodeTextField(
+ valueController: cubit.invitationCodeFieldController,
+ ),
+ const SizedBox(height: 15.0),
+ ElevatedButton(
+ onPressed: state.isLoading
+ ? null
+ : () {
+ _onTapRedeem(context);
+ },
+ child: Builder(
+ builder: (context) {
+ if (state.isLoading) {
+ return const CircularProgressIndicator(color: Colors.white);
+ }
+ return Text(
+ 'Register',
+ style: Theme.of(context).textTheme.titleLarge,
+ );
+ },
+ ),
+ ),
+ ],
+ ),
),
),
),
- ),
- );
+ );
+ },
+ );
+ }
void _onTapRedeem(BuildContext context) async {
- final code = _invitationCodeField.invitationCode;
- if (code == null) return;
- setState(() {
- _isRedeeming = true;
- });
+ final cubit = context.read<RedeemCubit>();
try {
- await _seosMobileKeysPlugin.setupEndpoint(code);
- await secureStorage.write("invitation",code);
+ cubit.onRedeem();
if (!context.mounted) return;
context.goNamed(AppRoutes.bookingDetails.name);
} catch (e) {
- ScaffoldMessenger.of(context)
- .showActionSnackBar(
+ ScaffoldMessenger.of(context).showActionSnackBar(
content: Text('Unable to redeem - ${e.toString()}'),
label: 'Retry',
onPressed: () => _onTapRedeem(context),
);
- } finally {
- if (mounted) {
- setState(() {
- _isRedeeming = false;
- });
- }
}
}
}
diff --git a/comwell_key_app/lib/redeem_debug/redeem_route.dart b/comwell_key_app/lib/redeem_debug/redeem_route.dart
new file mode 100644
index 00000000..08aaa421
--- /dev/null
+++ b/comwell_key_app/lib/redeem_debug/redeem_route.dart
@@ -0,0 +1,9 @@
+import 'package:comwell_key_app/redeem_debug/redeem_page.dart';
+import 'package:go_router/go_router.dart';
+
+import '../routing/app_routes.dart';
+
+final redeemRoute = GoRoute(
+ path: AppRoutes.redeem,
+ builder: (context, state) => const RedeemPage(),
+);
diff --git a/comwell_key_app/lib/redeem_debug/widgets/invitation_code_formatter.dart b/comwell_key_app/lib/redeem_debug/widgets/invitation_code_formatter.dart
new file mode 100644
index 00000000..7d10efe6
--- /dev/null
+++ b/comwell_key_app/lib/redeem_debug/widgets/invitation_code_formatter.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+class InvitationCodeFormatter extends TextInputFormatter {
+ final String sample;
+ final String separator;
+
+ InvitationCodeFormatter({
+ required this.sample,
+ required this.separator,
+ });
+
+ @override
+ TextEditingValue formatEditUpdate(
+ TextEditingValue oldValue,
+ TextEditingValue newValue
+ ) {
+ if (newValue.text.length > oldValue.text.length) {
+ if (newValue.text.characters.last == separator ||
+ newValue.text.length > sample.length) {
+ return oldValue;
+ }
+ if (newValue.text.length < sample.length &&
+ sample[newValue.text.length - 1] == separator) {
+ return TextEditingValue(
+ text:
+ '${oldValue.text}$separator${newValue.text.substring(newValue.text.length - 1)}'
+ .toUpperCase(),
+ selection:
+ TextSelection.collapsed(
+ offset: newValue.selection.end + 1)
+ );
+ }
+ }
+ return TextEditingValue(
+ text: newValue.text.toUpperCase(),
+ selection: newValue.selection,
+ );
+ }
+}
\ No newline at end of file
diff --git a/comwell_key_app/lib/redeem_debug/widgets/invitation_code_textfield.dart b/comwell_key_app/lib/redeem_debug/widgets/invitation_code_textfield.dart
new file mode 100644
index 00000000..08ff9137
--- /dev/null
+++ b/comwell_key_app/lib/redeem_debug/widgets/invitation_code_textfield.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import 'invitation_code_formatter.dart';
+
+class InvitationCodeTextField extends StatelessWidget {
+ InvitationCodeTextField({super.key, required this.valueController});
+
+ String? get invitationCode =>
+ _formKey.currentState?.validate() == true ? valueController.text : null;
+
+ final _formKey = GlobalKey<FormState>();
+ final TextEditingController valueController;
+
+ @override
+ Widget build(BuildContext context) => Form(
+ key: _formKey,
+ child: TextFormField(
+ autofocus: true,
+ controller: valueController,
+ inputFormatters: [
+ FilteringTextInputFormatter.allow(RegExp(r'[0-9a-zA-Z-]')),
+ InvitationCodeFormatter(sample: 'xxxx-xxxx-xxxx-xxxx', separator: '-'),
+ ],
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ hintText: 'XXXX-XXXX-XXXX-XXXX',
+ contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0),
+ ),
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter an invitation code';
+ } else if (value.replaceAll('-', '').length != 16) {
+ return 'Please enter a valid invitation code';
+ } else {
+ return null;
+ }
+ },
+ ),
+ );
+}
diff --git a/comwell_key_app/lib/routing/app_router.dart b/comwell_key_app/lib/routing/app_router.dart
index cd8a3163..1211ed18 100644
--- a/comwell_key_app/lib/routing/app_router.dart
+++ b/comwell_key_app/lib/routing/app_router.dart
@@ -1,5 +1,4 @@
import 'package:comwell_key_app/authentication/authentication_repository.dart';
-import 'package:comwell_key_app/authentication/enum/authentication_status.dart';
import 'package:comwell_key_app/check_in/bloc/check_in_cubit.dart';
import 'package:comwell_key_app/check_in/check_in_page.dart';
import 'package:comwell_key_app/check_out/bloc/check_out_cubit.dart';
@@ -47,7 +46,7 @@ import 'package:comwell_key_app/received_shared_booking/cubit/received_shared_bo
import 'package:comwell_key_app/received_shared_booking/received_shared_booking_page.dart';
import 'package:comwell_key_app/received_shared_room/cubit/received_shared_room_cubit.dart';
import 'package:comwell_key_app/received_shared_room/received_shared_room_page.dart';
-import 'package:comwell_key_app/redeem_debug/redeem_page.dart';
+import 'package:comwell_key_app/redeem_debug/redeem_route.dart';
import 'package:comwell_key_app/routing/app_routes.dart';
import 'package:comwell_key_app/routing/go_router_observer.dart';
import 'package:comwell_key_app/share/cubit/share_booking_cubit.dart';
@@ -66,7 +65,6 @@ import 'package:comwell_key_app/up_sales/pages/processing/up_sales_processing_pa
import 'package:comwell_key_app/up_sales/up_sales_repository.dart';
import 'package:comwell_key_app/up_sales/up_sales_catalog.dart';
import 'package:comwell_key_app/utils/context_utils.dart';
-import 'package:comwell_key_app/utils/stream_to_listenable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';
@@ -89,10 +87,8 @@ final router = GoRouter(
navigatorKey: _rootNavigatorKey,
debugLogDiagnostics: true,
observers: [GoRouterObserver()],
- refreshListenable: StreamToListenable([locator<AuthenticationRepository>().broadcast]),
redirect: (context, state) async {
final authRepo = locator<AuthenticationRepository>();
- final status = authRepo.statusBuffer;
final isLoggedIn = await authRepo.isLoggedIn();
if (state.uri.host == 'share-room') {
@@ -110,7 +106,7 @@ final router = GoRouter(
}
if (!isLoggedIn) {
- final forced = status == AuthenticationStatus.forcedUnauthenticated;
+ const forced = false;
return "${AppRoutes.login}?forced=$forced";
}
@@ -130,6 +126,7 @@ final router = GoRouter(
notificationsPermissionRoute,
usageTrackingPermissionRoute,
overviewRoute,
+ redeemRoute,
ShellRoute(
navigatorKey: _shellNavigatorKey,
parentNavigatorKey: _rootNavigatorKey,
@@ -297,11 +294,6 @@ final router = GoRouter(
);
},
),
- GoRoute(
- path: "/redeem",
- name: AppRoutes.redeem.name,
- builder: (context, state) => const RedeemPage(),
- ),
GoRoute(
path: "/${AppRoutes.preregistration.name}",
name: AppRoutes.preregistration.name,
diff --git a/comwell_key_app/lib/routing/app_routes.dart b/comwell_key_app/lib/routing/app_routes.dart
index 07c0ab13..4fb10b25 100644
--- a/comwell_key_app/lib/routing/app_routes.dart
+++ b/comwell_key_app/lib/routing/app_routes.dart
@@ -1,7 +1,6 @@
enum AppRoutes {
welcome,
initial,
- redeem,
key,
keys,
settings,
@@ -44,7 +43,9 @@ enum AppRoutes {
static const splash = "/";
static const login = "/login";
+ static const forceLogin = "/login?forced=true";
static const overview = "/overview";
+ static const redeem = "/redeem";
static const onboardingBluetooth = "/onboarding/bluetooth";
static const onboardingNotification = "/onboarding/notification";
static const onboardingUsageTracking = "/onboarding/usage-tracking";
diff --git a/comwell_key_app/lib/services/api.dart b/comwell_key_app/lib/services/api.dart
index 7be286ee..5c00efdd 100644
--- a/comwell_key_app/lib/services/api.dart
+++ b/comwell_key_app/lib/services/api.dart
@@ -20,22 +20,16 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:package_info_plus/package_info_plus.dart';
class Api {
- Dio? _dio;
+ final ComwellHttpClient _client = locator.get();
- Dio get dio {
- if (_dio != null) {
- return _dio!;
- } else {
- _dio = HttpClient().dio;
- return _dio!;
- }
- }
+ Dio get dio => _client.dio;
+
+ Api();
// Get current locale globally
Locale get _currentLocale {
final navigatorKey = locator<GlobalKey<NavigatorState>>();
- return EasyLocalization.of(navigatorKey.currentContext!)?.locale ??
- const Locale('en', 'US');
+ return EasyLocalization.of(navigatorKey.currentContext!)?.locale ?? const Locale('en', 'US');
}
Future<Response<dynamic>> logout() async {
@@ -48,8 +42,7 @@ class Api {
"limit": 100,
};
final json = jsonEncode(body);
- final response =
- await dio.get<List<dynamic>>(ApiEndpoints.getPastBookings, data: json);
+ final response = await dio.get<List<dynamic>>(ApiEndpoints.getPastBookings, data: json);
return response.data!.map((e) => BookingDTO.fromJson(e as Json)).toList();
}
@@ -59,8 +52,7 @@ class Api {
"limit": 100,
};
final json = jsonEncode(body);
- final response = await dio
- .get<List<dynamic>>(ApiEndpoints.getCancelledBookings, data: json);
+ final response = await dio.get<List<dynamic>>(ApiEndpoints.getCancelledBookings, data: json);
return response.data!.map((e) => BookingDTO.fromJson(e as Json)).toList();
}
@@ -70,13 +62,12 @@ class Api {
"limit": 100,
};
final json = jsonEncode(body);
- final response = await dio
- .get<List<dynamic>>(ApiEndpoints.getCurrentBookings, data: json);
+ final response = await dio.get<List<dynamic>>(ApiEndpoints.getCurrentBookings, data: json);
return response.data!.map((e) => BookingDTO.fromJson(e as Json)).toList();
}
-/* Future<StoredPaymentsResponse?> getPaymentMethods() async {
+ /* Future<StoredPaymentsResponse?> getPaymentMethods() async {
final response = await dio.get<Json>(ApiEndpoints.storedPaymentMethods);
return StoredPaymentsResponse.fromJson(response.data!);
} */
@@ -163,17 +154,14 @@ class Api {
}
Future<Response<dynamic>> signupForComwellClub() async {
- final response =
- await dio.post<dynamic>(ApiEndpoints.clubSignup);
+ final response = await dio.post<dynamic>(ApiEndpoints.clubSignup);
return response;
}
Future<Response<dynamic>> preRegistration(PreregRequestDto request) async {
-
final json = jsonEncode(request);
debugPrint("json: $json");
- final response =
- await dio.post<dynamic>(ApiEndpoints.preRegistration, data: json);
+ final response = await dio.post<dynamic>(ApiEndpoints.preRegistration, data: json);
return response;
}
@@ -185,24 +173,22 @@ class Api {
return response;
}
- Future<BookingDTO> findBookingByConfirmationId(
- String confirmationId, String lastName) async {
+ Future<BookingDTO> findBookingByConfirmationId(String confirmationId, String lastName) async {
final body = {
"confirmationNumber": confirmationId,
"lastName": lastName,
};
final data = jsonEncode(body);
- final response = await dio
- .post<Json>(ApiEndpoints.findBookingByConfirmationId, data: data);
+ final response = await dio.post<Json>(ApiEndpoints.findBookingByConfirmationId, data: data);
return BookingDTO.fromJson(response.data!);
}
Future<dynamic> updateNotificationPreferences(
- Iterable<NotificationPermission> notificationPermissions) {
+ Iterable<NotificationPermission> notificationPermissions,
+ ) {
final permissions = {
- for (var permission in notificationPermissions)
- permission.code.toString(): permission.given
+ for (var permission in notificationPermissions) permission.code.toString(): permission.given,
};
final simpleUserDto = SimpleUserDto(permissions: permissions);
return updateUser(simpleUserDto);
@@ -217,12 +203,10 @@ class Api {
);
return response;
}
-
Future<dynamic> getHotelInfo(String hotelCode) async {
final cultureString = _currentLocale.toString().replaceAll('_', '-');
- final url =
- '${ApiEndpoints.getHotelInfo}?hotelCode=$hotelCode&culture=$cultureString';
+ final url = '${ApiEndpoints.getHotelInfo}?hotelCode=$hotelCode&culture=$cultureString';
final response = await dio.get<Json>(url);
return response;
}
@@ -259,8 +243,7 @@ class Api {
"refill": housekeeping.refill,
};
final data = jsonEncode(body);
- final response =
- await dio.post<Json>(ApiEndpoints.orderHousekeeping, data: data);
+ final response = await dio.post<Json>(ApiEndpoints.orderHousekeeping, data: data);
return response.data;
}
@@ -271,7 +254,7 @@ class Api {
// TODO implement
}
-/* Future<Json?> postPaymentsDetails(Json body) async {
+ /* Future<Json?> postPaymentsDetails(Json body) async {
final Json headers = {
"content-type": "application/json",
"x-API-key":
@@ -297,19 +280,19 @@ class Api {
} */
Future<String> createEndpointRegistration() async {
- final response =
- await dio.post<String>(ApiEndpoints.createEndpointRegistration);
+ final response = await dio.post<String>(ApiEndpoints.createEndpointRegistration);
final json = jsonDecode(response.data!) as Map<String, dynamic>;
return json["invitationCode"]! as String;
}
Future<void> provisionKey(String confirmationNumber, String hotelCode) async {
- await dio
- .post<void>(ApiEndpoints.provisionKey, data: {'confirmationNumber': confirmationNumber, 'hotelCode': hotelCode});
+ await dio.post<void>(
+ ApiEndpoints.provisionKey,
+ data: {'confirmationNumber': confirmationNumber, 'hotelCode': hotelCode},
+ );
}
- Future<BookingDTO?> getBookingDetails(
- String confirmationNumber, String hotelCode) async {
+ Future<BookingDTO?> getBookingDetails(String confirmationNumber, String hotelCode) async {
final uri = Uri.parse(ApiEndpoints.getBookingDetails).replace(
queryParameters: {
'confirmationNumber': confirmationNumber,
@@ -327,13 +310,11 @@ class Api {
"userId": userId,
};
final data = jsonEncode(body);
- final response =
- await dio.post<Json>('ApiEndpoints.roomSelection', data: data);
+ final response = await dio.post<Json>('ApiEndpoints.roomSelection', data: data);
return response.data!;
}
- Future<UpSalesDTO> fetchUpSales(
- String confirmationNumber, String hotelCode) async {
+ Future<UpSalesDTO> fetchUpSales(String confirmationNumber, String hotelCode) async {
final body = {
"confirmationNumber": confirmationNumber,
"property": hotelCode,
@@ -344,8 +325,12 @@ class Api {
return UpSalesDTO.fromJson(response.data!);
}
- Future<void> addUpSalesToBooking(String confirmationNumber, String hotelCode,
- String roomType, List<AddOnList> selectedUpSales) async {
+ Future<void> addUpSalesToBooking(
+ String confirmationNumber,
+ String hotelCode,
+ String roomType,
+ List<AddOnList> selectedUpSales,
+ ) async {
final body = {
"confirmationNumber": confirmationNumber,
"property": hotelCode,
@@ -358,7 +343,10 @@ class Api {
}
Future<void> removeGuestsFromBooking(
- String confirmationNumber, String hotelCode, int guestId) async {
+ String confirmationNumber,
+ String hotelCode,
+ int guestId,
+ ) async {
final body = {
"hotelCode": hotelCode,
"confirmationNumber": confirmationNumber,
@@ -370,21 +358,22 @@ class Api {
}
Future<String> createRoomSharingLink(
- String confirmationNumber, String hotelCode, int sharingType) async {
+ String confirmationNumber,
+ String hotelCode,
+ int sharingType,
+ ) async {
final body = {
"confirmationNumber": confirmationNumber,
"hotelCode": hotelCode,
"sharingType": sharingType,
};
final data = jsonEncode(body);
- final response =
- await dio.post<String>(ApiEndpoints.createRoomSharingLink, data: data);
+ final response = await dio.post<String>(ApiEndpoints.createRoomSharingLink, data: data);
debugPrint("Response: $response");
return response.data!;
}
- Future<BookingDTO> consumeRoomSharingLink(
- String sharingId, String hotelCode) async {
+ Future<BookingDTO> consumeRoomSharingLink(String sharingId, String hotelCode) async {
final uri = Uri.parse(ApiEndpoints.consumeRoomSharingLink).replace(
queryParameters: {
'sharingId': sharingId,
@@ -434,12 +423,16 @@ class Api {
],
};
final data = jsonEncode(body);
- await dio.put<void>('$symplifyUrl/$customerId/apps/$packageName/devices',
- data: data,
- options: Options(headers: {
+ await dio.put<void>(
+ '$symplifyUrl/$customerId/apps/$packageName/devices',
+ data: data,
+ options: Options(
+ headers: {
'X-Carma-Authentication-Token': apiKey,
'Content-Type': 'application/json',
- }));
+ },
+ ),
+ );
return;
}
}
diff --git a/comwell_key_app/lib/services/exceptions.dart b/comwell_key_app/lib/services/exceptions.dart
new file mode 100644
index 00000000..03332f49
--- /dev/null
+++ b/comwell_key_app/lib/services/exceptions.dart
@@ -0,0 +1,5 @@
+import 'dart:io';
+
+class UnauthorizedException extends IOException {
+ UnauthorizedException() : super();
+}
diff --git a/comwell_key_app/lib/services/http_client.dart b/comwell_key_app/lib/services/http_client.dart
index 6b5f9b57..51065b12 100644
--- a/comwell_key_app/lib/services/http_client.dart
+++ b/comwell_key_app/lib/services/http_client.dart
@@ -1,31 +1,33 @@
import 'package:comwell_key_app/services/interceptors/response_handle_interceptor.dart';
+import 'package:comwell_key_app/utils/env_utils.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
+import '../utils/locator.dart';
-class HttpClient {
- final dio = _createDio();
+class ComwellHttpClient {
+ final Dio dio;
- HttpClient._privateConstructor();
-
- static final _singleton = HttpClient._privateConstructor();
-
- factory HttpClient() => _singleton;
+ ComwellHttpClient() : dio = _createDio();
static Dio _createDio() {
- var dio = Dio(
+ final dio = Dio(
BaseOptions(
- baseUrl: dotenv.env['SERVICE_URL'] ?? '',
+ baseUrl: dotenv.SERVICE_URL,
receiveTimeout: const Duration(milliseconds: 15000),
connectTimeout: const Duration(milliseconds: 15000),
sendTimeout: const Duration(milliseconds: 15000),
),
);
- dio.interceptors.add(ResponseHandleInterceptor(dio));
- if(kDebugMode) dio.interceptors.add(PrettyDioLogger(requestHeader: true));
-
+ dio.interceptors.add(
+ ResponseHandleInterceptor(
+ locator.get(),
+ locator.get(),
+ ),
+ );
+ if (kDebugMode) dio.interceptors.add(PrettyDioLogger(requestHeader: true));
return dio;
}
-}
\ No newline at end of file
+}
diff --git a/comwell_key_app/lib/services/interceptors/response_handle_interceptor.dart b/comwell_key_app/lib/services/interceptors/response_handle_interceptor.dart
index 91ecd215..51b4a13c 100644
--- a/comwell_key_app/lib/services/interceptors/response_handle_interceptor.dart
+++ b/comwell_key_app/lib/services/interceptors/response_handle_interceptor.dart
@@ -1,26 +1,35 @@
-import 'package:comwell_key_app/authentication/authentication_repository.dart';
+import 'package:comwell_key_app/data/remote/msal_service.dart';
+import 'package:comwell_key_app/routing/app_routes.dart';
+import 'package:comwell_key_app/services/exceptions.dart';
import 'package:comwell_key_app/utils/env_utils.dart';
import 'package:comwell_key_app/utils/locator.dart';
+import 'package:comwell_key_app/utils/secure_storage.dart';
import 'package:dio/dio.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
-import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:comwell_key_app/common/const.dart' as constants;
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';
+import 'package:msal_auth/msal_auth.dart';
class ResponseHandleInterceptor extends Interceptor {
- final Dio _dio;
- final FlutterSecureStorage _secureStorageService = const FlutterSecureStorage();
+ final SecureStorage _secureStorage;
+ final MSALService _msalService;
- final AuthenticationRepository _authenticationRepository = locator<AuthenticationRepository>();
- int retryCount = 0;
+ ResponseHandleInterceptor(this._secureStorage, this._msalService);
- ResponseHandleInterceptor(this._dio);
+ BuildContext? get context {
+ final navigatorKey = locator<GlobalKey<NavigatorState>>();
+ return navigatorKey.currentContext!;
+ }
Future<void> checkAuth(RequestOptions requestOptions) async {
- final accessToken = await _secureStorageService.read(key: constants.accessToken);
- if (accessToken != null) {
- requestOptions.headers['Authorization'] = accessToken;
+ try {
+ await _msalService.acquireTokenSilent();
+ final accessToken = await _secureStorage.read(constants.accessToken);
+ requestOptions.headers['Authorization'] = accessToken!;
+ } catch (e, _) {
+ debugPrint("checkAuthError=$e");
+ throw UnauthorizedException();
}
}
@@ -29,9 +38,25 @@ class ResponseHandleInterceptor extends Interceptor {
RequestOptions options,
RequestInterceptorHandler handler,
) async {
- await checkAuth(options);
- options.headers['Ocp-Apim-Subscription-Key'] = dotenv.OCP_APIM_SUBSCRIPTION_KEY;
- return handler.next(options);
+ try {
+ await checkAuth(options);
+ options.headers['Ocp-Apim-Subscription-Key'] = dotenv.OCP_APIM_SUBSCRIPTION_KEY;
+ return handler.next(options);
+ } on UnauthorizedException catch (e, st) {
+ final exc = DioException(requestOptions: options, error: e, stackTrace: st);
+ handler.reject(exc);
+ logOut();
+ } on MsalArgumentException catch (e, st) {
+ final exc = DioException(requestOptions: options, error: e, stackTrace: st);
+ handler.reject(exc);
+ logOut();
+ } catch (e, st) {
+ handler.reject(DioException(requestOptions: options, error: e, stackTrace: st));
+ }
+ }
+
+ void logOut() {
+ if (context != null) GoRouter.of(context!).go(AppRoutes.forceLogin);
}
@override
@@ -42,144 +67,17 @@ class ResponseHandleInterceptor extends Interceptor {
final response = err.response;
if (response == null) {
- debugPrint('Error: No response received - ${err.message}');
- return handler.next(
- DioException(
- message: "No response received: ${err.message}",
- requestOptions: err.requestOptions,
- type: err.type,
- error: err.error,
- ),
- );
- }
-
- final statusCode = response.statusCode;
- debugPrint('HTTP Error: $statusCode on ${response.requestOptions.path}');
-
- try {
- switch (statusCode) {
- case 404:
- final error = DioException(
- requestOptions: response.requestOptions,
- response: response,
- error: 'Not found: ${response.requestOptions.path}',
- );
- handler.next(error);
- break;
-
- case 401:
- await _handleUnauthorized(err, handler, response);
- break;
-
- case 500:
- final errorMessage = _extractErrorMessage(response.data, 'Internal server error');
- final error = DioException(
- requestOptions: response.requestOptions,
- response: response,
- error: errorMessage,
- );
- handler.next(error);
- break;
-
- case 426:
- // Navigate to force update page
- final navigatorKey = locator<GlobalKey<NavigatorState>>();
- final context = navigatorKey.currentContext;
- if (context != null) {
- GoRouter.of(context).go('/forceUpdate');
- }
- final error = DioException(
- requestOptions: response.requestOptions,
- response: response,
- error: 'Update required',
- );
- handler.next(error);
- break;
-
- default:
- final errorMessage = _extractErrorMessage(response.data, 'Unknown error');
- final error = DioException(
- requestOptions: response.requestOptions,
- response: response,
- error: 'HTTP $statusCode: $errorMessage',
- );
- handler.next(error);
- break;
- }
- } catch (e, stackTrace) {
- debugPrint('Error handling HTTP response: $e');
- debugPrintStack(stackTrace: stackTrace);
- final error = DioException(
- requestOptions: response.requestOptions,
- response: response,
- error: 'Failed to handle response (HTTP $statusCode): $e',
- );
- handler.next(error);
- }
- }
-
- Future<void> _handleUnauthorized(
- DioException originalError,
- ErrorInterceptorHandler handler,
- Response<dynamic> response,
- ) async {
- retryCount++;
- debugPrint('401 Unauthorized - Attempt $retryCount/3 to refresh token');
-
- if (retryCount >= 3) {
- debugPrint('Max retry attempts reached. Forcing logout.');
- retryCount = 0;
- await _authenticationRepository.logOut(forced: true);
- return handler.reject(originalError);
+ debugPrint('Error: No response received - ${_extractErrorMessage}');
+ return handler.next(err);
}
+ debugPrint('Error: ${_extractErrorMessage(response.data, "No data")}');
- try {
- final identifier = await _secureStorageService.read(key: constants.identifier);
-
- if (identifier == null || identifier.isEmpty) {
- debugPrint('No identifier found. Cannot refresh token. Forcing logout.');
- retryCount = 0;
- await _authenticationRepository.logOut(forced: true);
- return handler.reject(originalError);
- }
-
- debugPrint('Attempting silent token acquisition...');
- final authResult = await _authenticationRepository.acquireTokenSilent(identifier);
-
- if (authResult.accessToken.isEmpty) {
- debugPrint('Received empty token. Forcing logout.');
- retryCount = 0;
- await _authenticationRepository.logOut(forced: true);
- return handler.reject(originalError);
- }
-
- await _secureStorageService.write(key: constants.accessToken, value: authResult.accessToken);
- debugPrint('Token refreshed successfully. Retrying request...');
-
- // Retry the original request with the new token
- final opts = Options(
- method: response.requestOptions.method,
- headers: {
- ...response.requestOptions.headers,
- 'Authorization': authResult.accessToken,
- },
- );
-
- final retryRequest = await _dio.request<dynamic>(
- response.requestOptions.path,
- options: opts,
- data: response.requestOptions.data,
- queryParameters: response.requestOptions.queryParameters,
- );
-
- return handler.resolve(retryRequest);
- } catch (e, stackTrace) {
- debugPrint('Error acquiring token silently: $e');
- debugPrintStack(stackTrace: stackTrace);
- retryCount = 0;
- await _authenticationRepository.logOut(forced: true);
- return handler.reject(originalError);
+ switch (response.statusCode) {
+ case 401:
+ case 426:
+ logOut();
}
+ return handler.next(err);
}
String _extractErrorMessage(dynamic data, String fallback) {
@@ -193,13 +91,4 @@ class ResponseHandleInterceptor extends Interceptor {
}
return fallback;
}
-
- @override
- Future<dynamic> onResponse(
- Response<dynamic> response,
- ResponseInterceptorHandler handler,
- ) async {
- retryCount = 0;
- return handler.next(response);
- }
}
diff --git a/comwell_key_app/lib/settings/settings_page.dart b/comwell_key_app/lib/settings/settings_page.dart
index 759aa549..d8b9d197 100644
--- a/comwell_key_app/lib/settings/settings_page.dart
+++ b/comwell_key_app/lib/settings/settings_page.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
+import 'package:seos_mobile_keys_plugin/seos_mobile_keys_plugin.dart';
-import '../utils/seos_repository.dart';
+import '../utils/locator.dart';
import '../common/extensions/scaffold_messenger_state_extension.dart';
import '../common/components/shimmer_loader/settings_shimmer_loader.dart';
@@ -8,41 +9,38 @@ class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
- State<StatefulWidget> createState() =>
- _SettingPage();
+ State<StatefulWidget> createState() => _SettingPage();
}
class _SettingPage extends State<SettingsPage> {
- final _seosMobileKeysPlugin = SeosRepository().seosMobileKeysPlugin;
+ final _seosMobileKeysPlugin = locator.get<SeosMobileKeysPlugin>();
bool _isUnregistering = false;
@override
- Widget build(BuildContext context) =>
- Scaffold(
- appBar: AppBar(
- title: const Text(
- 'Settings',
- ),
- ),
- body: ListView(
- children: _menuList(context),
- ),
- );
+ Widget build(BuildContext context) => Scaffold(
+ appBar: AppBar(
+ title: const Text(
+ 'Settings',
+ ),
+ ),
+ body: ListView(
+ children: _menuList(context),
+ ),
+ );
- List<Widget> _menuList(BuildContext context) =>
- [
- ListTile(
- title: Text(
- 'Unregister device',
- style: Theme.of(context).textTheme.titleMedium,
- ),
- leading: const Icon(
- Icons.logout_sharp,
- ),
- trailing: _isUnregistering ? const SettingsShimmerLoader() : null,
- onTap: _isUnregistering ? null : _unregister,
- ),
- ];
+ List<Widget> _menuList(BuildContext context) => [
+ ListTile(
+ title: Text(
+ 'Unregister device',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ leading: const Icon(
+ Icons.logout_sharp,
+ ),
+ trailing: _isUnregistering ? const SettingsShimmerLoader() : null,
+ onTap: _isUnregistering ? null : _unregister,
+ ),
+ ];
void _unregister() async {
if (!mounted) return;
@@ -54,12 +52,11 @@ class _SettingPage extends State<SettingsPage> {
if (!mounted) return;
Navigator.pop(context);
} catch (e) {
- ScaffoldMessenger.of(context)
- .showActionSnackBar(
+ ScaffoldMessenger.of(context).showActionSnackBar(
content: Text('Unable to unregister: ${e.toString()}'),
label: 'Retry',
onPressed: _unregister,
);
}
}
-}
\ No newline at end of file
+}
diff --git a/comwell_key_app/lib/utils/env_utils.dart b/comwell_key_app/lib/utils/env_utils.dart
index e844a7da..5a71025f 100644
--- a/comwell_key_app/lib/utils/env_utils.dart
+++ b/comwell_key_app/lib/utils/env_utils.dart
@@ -12,4 +12,14 @@ extension EnvUtils on DotEnv {
String get ENTRA_API_URL => env["ENTRA_API_URL"]!;
String get SENTRY_DSN => env["SENTRY_DSN"]!;
+
+ String get MOBILE_KEYS_OPTION_APPLICATION_ID => env["MOBILEKEYSOPTIONAPPLICATIONID"]!;
+
+ String get MOBILE_KEYS_OPTION_APP_DESCRIPTION => env["MOBILEKEYSOPTIONAPPDESCRIPTION"]!;
+
+ String get MOBILE_KEYS_OPTION_VERSION => env["MOBILEKEYSOPTIONVERSION"]!;
+
+ String get MOBILE_KEYS_OPTION_LOGS_MAIL => env["MOBILEKEYSOPTIONLOGSMAIL"]!;
+
+ String get SERVICE_URL => env["SERVICE_URL"]!;
}
diff --git a/comwell_key_app/lib/utils/locator.dart b/comwell_key_app/lib/utils/locator.dart
index 83c74afb..851c65e2 100644
--- a/comwell_key_app/lib/utils/locator.dart
+++ b/comwell_key_app/lib/utils/locator.dart
@@ -3,6 +3,7 @@ import 'package:comwell_key_app/authentication/authentication_repository.dart';
import 'package:comwell_key_app/check_out/check_out_repository.dart';
import 'package:comwell_key_app/choose_share_room/choose_share_room_repository.dart';
import 'package:comwell_key_app/contact/repository/contact_repository.dart';
+import 'package:comwell_key_app/data/remote/msal_service.dart';
import 'package:comwell_key_app/database/comwell_db.dart';
import 'package:comwell_key_app/domain/repositories/bluetooth_repository.dart';
import 'package:comwell_key_app/domain/repositories/internet_status_repository.dart';
@@ -16,6 +17,7 @@ import 'package:comwell_key_app/overview/repository/overview_repository.dart';
import 'package:comwell_key_app/pregistration/pregistration_repository.dart';
import 'package:comwell_key_app/profile/profile_repository.dart';
import 'package:comwell_key_app/profile_settings/repostiory/profile_settings_repository.dart';
+import 'package:comwell_key_app/services/api.dart';
import 'package:comwell_key_app/services/http_client.dart';
import 'package:comwell_key_app/share/share_booking_repository.dart';
import 'package:comwell_key_app/push_notifications/push_notification_repository.dart';
@@ -24,7 +26,6 @@ import 'package:comwell_key_app/up_sales/up_sales_repository.dart';
import 'package:comwell_key_app/utils/secure_storage.dart';
import 'package:comwell_key_app/utils/seos_repository.dart';
import 'package:comwell_key_app/routing/app_router.dart';
-import 'package:flutter/material.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
@@ -36,54 +37,59 @@ import '../booking_details/booking_details_repository.dart';
final locator = GetIt.I;
void registerDatabase() {
- if (!locator.isRegistered<ComwellDatabase>()) {
- locator.registerLazySingleton<ComwellDatabase>(() {
- final db = ComwellDatabase();
- // Ensure database is properly initialized
- return db;
- });
- }
+ if (locator.isRegistered<ComwellDatabase>()) return;
+ locator.registerLazySingleton<ComwellDatabase>(() {
+ return ComwellDatabase();
+ });
}
void setupLocator() {
locator.registerFactory<DeviceInfoPlugin>(() => DeviceInfoPlugin());
if (!kIsWeb) {
- locator.registerSingleton<ComwellTracking>(ComwellTracking());
+ locator.registerSingleton(ComwellPreferences());
+ locator.registerSingleton(ComwellTracking());
registerDatabase();
+ locator.registerSingleton(SecureStorage());
+ locator.registerSingleton(MSALService(locator.get()));
locator.registerSingleton(SeosMobileKeysPlugin());
- locator.registerFactory<KeyRepository>(
- () => KeyRepository(
- deviceInfoPlugin: locator<DeviceInfoPlugin>(),
- seosMobileKeysPlugin: locator<SeosMobileKeysPlugin>(),
+ locator.registerSingleton(ComwellHttpClient());
+ locator.registerSingleton(Api());
+ locator.registerFactory(() => KeyRepository(locator.get()));
+ locator.registerSingleton(rootNavigatorKey);
+ locator.registerSingleton(
+ SeosRepository(
+ locator.get(),
+ locator.get(),
+ locator.get(),
),
);
- locator.registerSingleton(SeosRepository());
- locator.registerFactory<OverviewRepository>(() => OverviewRepository());
- locator.registerFactory<ProfileSettingsRepository>(() => ProfileSettingsRepository());
- locator.registerSingleton<AuthenticationRepository>(AuthenticationRepository());
- locator.registerFactory<BookingDetailsRepository>(() => BookingDetailsRepository());
- locator.registerFactory<ProfileRepository>(() => ProfileRepository());
- locator.registerFactory<PreregistrationRepository>(() => PreregistrationRepository());
- locator.registerFactory<HotelInformationRepository>(() => HotelInformationRepository());
- locator.registerFactory<NotificationsRepository>(() => NotificationsRepository());
- locator.registerFactory<ContactRepository>(() => ContactRepository());
- locator.registerFactory<UpSalesRepository>(() => UpSalesRepository());
- locator.registerFactory<ChooseShareRoomRepository>(() => ChooseShareRoomRepository());
- locator.registerFactory<FindBookingRepository>(() => FindBookingRepository());
- locator.registerSingleton<GlobalKey<NavigatorState>>(rootNavigatorKey);
- locator.registerFactory<CheckOutRepository>(() => CheckOutRepository());
- locator.registerFactory<MyBookingRepository>(() => MyBookingRepository());
- locator.registerFactory<ShareBookingRepository>(() => ShareBookingRepository());
- locator.registerSingleton<SecureStorage>(SecureStorage());
- locator.registerFactory<HouseKeepingRepository>(() => HouseKeepingRepository());
- locator.registerFactory<PushNotificationRepository>(() => PushNotificationRepository());
- locator.registerFactory(() => BluetoothRepository());
- locator.registerFactory(() => InternetStatusRepository());
- locator.registerFactory<AdyenRepository>(
- () => AdyenRepository(
- dio: HttpClient().dio,
+ locator.registerFactory(() => OverviewRepository(locator.get()));
+ locator.registerFactory(() => ProfileSettingsRepository());
+ locator.registerSingleton(
+ AuthenticationRepository(
+ locator.get(),
+ locator.get(),
+ locator.get(),
+ locator.get(),
+ locator.get(),
),
);
- locator.registerSingleton(ComwellPreferences());
+ locator.registerFactory(() => BookingDetailsRepository());
+ locator.registerFactory(() => ProfileRepository(locator.get(), locator.get()));
+ locator.registerFactory(() => PreregistrationRepository());
+ locator.registerFactory(() => HotelInformationRepository());
+ locator.registerFactory(() => NotificationsRepository());
+ locator.registerFactory(() => ContactRepository());
+ locator.registerFactory(() => UpSalesRepository());
+ locator.registerFactory(() => ChooseShareRoomRepository());
+ locator.registerFactory(() => FindBookingRepository());
+ locator.registerFactory(() => CheckOutRepository());
+ locator.registerFactory(() => MyBookingRepository());
+ locator.registerFactory(() => ShareBookingRepository());
+ locator.registerFactory(() => HouseKeepingRepository());
+ locator.registerFactory(() => PushNotificationRepository());
+ locator.registerFactory(() => BluetoothRepository());
+ locator.registerFactory(() => InternetStatusRepository());
+ locator.registerFactory(() => AdyenRepository(dio: locator.get<ComwellHttpClient>().dio));
}
}
diff --git a/comwell_key_app/lib/utils/seos_repository.dart b/comwell_key_app/lib/utils/seos_repository.dart
index 5c065190..2007281f 100644
--- a/comwell_key_app/lib/utils/seos_repository.dart
+++ b/comwell_key_app/lib/utils/seos_repository.dart
@@ -1,5 +1,5 @@
import 'package:comwell_key_app/services/api.dart';
-import 'package:comwell_key_app/utils/locator.dart';
+import 'package:comwell_key_app/utils/env_utils.dart';
import 'package:comwell_key_app/utils/secure_storage.dart';
import 'package:flutter/material.dart';
@@ -10,25 +10,25 @@ import 'package:seos_mobile_keys_plugin/seos_mobile_keys_plugin.dart';
import 'package:comwell_key_app/common/const.dart' as constants;
class SeosRepository {
- final secureStorage = SecureStorage();
- final api = Api();
- final seosMobileKeysPlugin = locator<SeosMobileKeysPlugin>();
+ final SecureStorage _secureStorage;
+ final Api _api;
+ final SeosMobileKeysPlugin _seosMobileKeysPlugin;
bool _pluginStarted = false;
+ SeosRepository(this._secureStorage, this._api, this._seosMobileKeysPlugin);
+
Future<void> startMobilePlugin() async {
final mobileKeysOptions = {
- "MobileKeysOptionApplicationId":
- dotenv.env['MOBILEKEYSOPTIONAPPLICATIONID'],
- "MobileKeysOptionAppDescription":
- dotenv.env['MOBILEKEYSOPTIONAPPDESCRIPTION'],
- "MobileKeysOptionVersion": dotenv.env['MOBILEKEYSOPTIONVERSION'],
+ "MobileKeysOptionApplicationId": dotenv.MOBILE_KEYS_OPTION_APPLICATION_ID,
+ "MobileKeysOptionAppDescription": dotenv.MOBILE_KEYS_OPTION_APP_DESCRIPTION,
+ "MobileKeysOptionVersion": dotenv.MOBILE_KEYS_OPTION_VERSION,
"MobileKeysOptionLockServiceCodes": [1],
- "MobileKeysOptionLogsMail": dotenv.env['MOBILEKEYSOPTIONLOGSMAIL'],
+ "MobileKeysOptionLogsMail": dotenv.MOBILE_KEYS_OPTION_LOGS_MAIL,
};
try {
if (!_pluginStarted) {
- await seosMobileKeysPlugin.startUp(mobileKeysOptions);
+ await _seosMobileKeysPlugin.startUp(mobileKeysOptions);
}
} on PlatformException catch (_) {
// seosMobileKeysPlugin.startUp will throw an error if it's already been started,
@@ -40,15 +40,15 @@ class SeosRepository {
_pluginStarted = true;
}
try {
- final isEndpointSetup = await seosMobileKeysPlugin.isEndpointSetup();
+ final isEndpointSetup = await _seosMobileKeysPlugin.isEndpointSetup();
if (isEndpointSetup) {
await _updateEndpont();
} else {
- final invitationCode = await api.createEndpointRegistration();
- await seosMobileKeysPlugin.setupEndpoint(invitationCode);
+ final invitationCode = await _api.createEndpointRegistration();
+ await _seosMobileKeysPlugin.setupEndpoint(invitationCode);
await Future<void>.delayed(const Duration(seconds: 2));
- await seosMobileKeysPlugin.updateEndpoint();
+ await _seosMobileKeysPlugin.updateEndpoint();
}
} on PlatformException catch (e) {
throw Exception('Failed to init MobileKeysManager - ${e.toString()}');
@@ -58,8 +58,8 @@ class SeosRepository {
}
Future<void> _updateEndpont() async {
- await seosMobileKeysPlugin.updateEndpoint();
- final isEndpointSetupRecheck = await seosMobileKeysPlugin.isEndpointSetup();
+ await _seosMobileKeysPlugin.updateEndpoint();
+ final isEndpointSetupRecheck = await _seosMobileKeysPlugin.isEndpointSetup();
// Required for AAH certification 3.3
if (!isEndpointSetupRecheck) throw Exception("Endpoint is not set up");
}
@@ -67,7 +67,7 @@ class SeosRepository {
Future<bool> isEndpointSetup({bool firstLaunch = true}) async {
if (!_pluginStarted) return false;
try {
- return seosMobileKeysPlugin.isEndpointSetup();
+ return _seosMobileKeysPlugin.isEndpointSetup();
} catch (e) {
throw Exception('Failed to check if endpoint setup - ${e.toString()}');
}
@@ -77,8 +77,8 @@ class SeosRepository {
if (!_pluginStarted) return;
try {
- await api.provisionKey(bookingId, hotelCode);
- await seosMobileKeysPlugin.updateEndpoint();
+ await _api.provisionKey(bookingId, hotelCode);
+ await _seosMobileKeysPlugin.updateEndpoint();
} catch (e) {
throw Exception('Failed to provision a key - ${e.toString()}');
}
@@ -87,8 +87,8 @@ class SeosRepository {
Future<List<MobileKeysKey>> refreshKeys() async {
if (!_pluginStarted) return [];
try {
- final List<MobileKeysKey> listOfKeys = await seosMobileKeysPlugin.listMobileKeys();
- await secureStorage.write(constants.hasKey, DateTime.now().toString());
+ final List<MobileKeysKey> listOfKeys = await _seosMobileKeysPlugin.listMobileKeys();
+ await _secureStorage.write(constants.hasKey, DateTime.now().toString());
return listOfKeys;
} catch (e) {
throw Exception('Failed to list keys - ${e.toString()}');
@@ -98,9 +98,13 @@ class SeosRepository {
Future<void> terminateEndpoint() async {
if (!_pluginStarted) return;
try {
- await seosMobileKeysPlugin.terminateEndpoint();
+ await _seosMobileKeysPlugin.terminateEndpoint();
} catch (e) {
throw Exception('Failed to terminate endpoint - ${e.toString()}');
}
}
+
+ Future<void> setupEndpoint(String code) async {
+ await _seosMobileKeysPlugin.setupEndpoint(code);
+ }
}
diff --git a/comwell_key_app/scripts/run_prod.sh b/comwell_key_app/scripts/run_prod.sh
new file mode 100644
index 00000000..c41849ba
--- /dev/null
+++ b/comwell_key_app/scripts/run_prod.sh
@@ -0,0 +1 @@
+fvm flutter run --flavor prod
diff --git a/comwell_key_app/test/authentication_test/authentication_bloc_test.dart b/comwell_key_app/test/authentication_test/authentication_bloc_test.dart
index f3486273..79b3aed1 100644
--- a/comwell_key_app/test/authentication_test/authentication_bloc_test.dart
+++ b/comwell_key_app/test/authentication_test/authentication_bloc_test.dart
@@ -1,12 +1,10 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:comwell_key_app/authentication/authentication_repository.dart';
import 'package:comwell_key_app/authentication/bloc/authentication_bloc.dart';
-import 'package:comwell_key_app/authentication/enum/authentication_status.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
-class MockAuthenticationRepository extends Mock
- implements AuthenticationRepository {}
+class MockAuthenticationRepository extends Mock implements AuthenticationRepository {}
void main() {
late AuthenticationRepository authenticationRepository;
@@ -21,15 +19,14 @@ void main() {
);
}
-
group('authenticationBloc', () {
group('AuthenticationSubscriptionRequested', () {
final error = Exception('oops');
blocTest<AuthenticationBloc, AuthenticationState>(
'emits [unauthenticated] when status is unauthenticated',
setUp: () {
- when(() => authenticationRepository.status).thenAnswer(
- (_) => Stream.value(AuthenticationStatus.unauthenticated),
+ when(() => authenticationRepository.isLoggedIn()).thenAnswer(
+ (_) => Future.value(false),
);
},
build: buildBloc,
@@ -40,8 +37,8 @@ void main() {
blocTest<AuthenticationBloc, AuthenticationState>(
'emits [authenticated] when status is authenticated',
setUp: () {
- when(() => authenticationRepository.status).thenAnswer(
- (_) => Stream.value(AuthenticationStatus.authenticated),
+ when(() => authenticationRepository.isLoggedIn()).thenAnswer(
+ (_) => Future.value(false),
);
},
build: buildBloc,
@@ -53,8 +50,8 @@ void main() {
'adds error when status stream emits an error',
setUp: () {
when(
- () => authenticationRepository.status,
- ).thenAnswer((_) => Stream.error(error));
+ () => authenticationRepository.isLoggedIn(),
+ ).thenAnswer((_) => Future.error(Exception()));
},
build: buildBloc,
act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()),
@@ -68,28 +65,4 @@ void main() {
);
expect(authenticationBloc.state, const AuthenticationState.unknown());
});
-
- test('emits [authenticated] when logIn is called', () {
- when(() => authenticationRepository.logIn()).thenAnswer((_) async {});
- when(() => authenticationRepository.status).thenAnswer((_) async* {
- yield AuthenticationStatus.authenticated;
- });
-
- authenticationRepository.logIn();
-
- expect(authenticationRepository.status,
- emitsInOrder([AuthenticationStatus.authenticated]));
- });
-
- test('emits [unauthenticated] when logOut is called', () {
- when(() => authenticationRepository.logOut()).thenAnswer((_) async {});
- when(() => authenticationRepository.status).thenAnswer((_) async* {
- yield AuthenticationStatus.unauthenticated;
- });
-
- authenticationRepository.logOut();
-
- expect(authenticationRepository.status,
- emitsInOrder([AuthenticationStatus.unauthenticated]));
- });
}
diff --git a/comwell_key_app/test/authentication_test/authentication_repository.dart b/comwell_key_app/test/authentication_test/authentication_repository.dart
index 078affb5..9fd6c563 100644
--- a/comwell_key_app/test/authentication_test/authentication_repository.dart
+++ b/comwell_key_app/test/authentication_test/authentication_repository.dart
@@ -11,36 +11,31 @@ class MockAuthenticationRepository extends Mock implements AuthenticationReposit
void main() {
// Declare the mocks
late MockSeosRepository mockSeosRepository;
- late MockAuthenticationRepository authenticationRepository;
// Setup function runs before every test
setUp(() {
// Initialize the mocks
TestWidgetsFlutterBinding.ensureInitialized();
mockSeosRepository = MockSeosRepository();
- authenticationRepository = MockAuthenticationRepository();
-
- // Stub the seos getter to return the mock SeosRepository
- when(() => authenticationRepository.seos).thenReturn(mockSeosRepository);
});
group('AuthenticationRepository - SeosRepository integration', () {
test('isEndpointSetup returns true when endpoint is setup', () async {
when(() => mockSeosRepository.isEndpointSetup()).thenAnswer((_) async => true);
- expect(await authenticationRepository.seos.isEndpointSetup(), isTrue);
+ expect(await mockSeosRepository.isEndpointSetup(), isTrue);
});
test('isEndpointSetup returns false when endpoint is not setup', () async {
when(() => mockSeosRepository.isEndpointSetup()).thenAnswer((_) async => false);
- expect(await authenticationRepository.seos.isEndpointSetup(), isFalse);
+ expect(await mockSeosRepository.isEndpointSetup(), isFalse);
});
test('refreshKeys returns a list of MobileKeysKey on success', () async {
when(() => mockSeosRepository.refreshKeys()).thenAnswer((_) async => [MobileKeysKey(active: true, credentialType: 1)]);
- final keys = await authenticationRepository.seos.refreshKeys();
+ final keys = await mockSeosRepository.refreshKeys();
expect(keys, isNotNull);
expect(keys, isA<List<MobileKeysKey>>());
expect(keys.length, equals(1));
@@ -49,7 +44,7 @@ void main() {
test('refreshKeys throws an exception on failure', () async {
when(() => mockSeosRepository.refreshKeys()).thenThrow(Exception('Failed to list keys'));
- expect(authenticationRepository.seos.refreshKeys(), throwsA(isA<Exception>()));
+ expect(mockSeosRepository.refreshKeys(), throwsA(isA<Exception>()));
});
});
}
\ No newline at end of file
diff --git a/comwell_key_app/test/key_test/key_repository_test.dart b/comwell_key_app/test/key_test/key_repository_test.dart
index 931f942b..6293c0cb 100644
--- a/comwell_key_app/test/key_test/key_repository_test.dart
+++ b/comwell_key_app/test/key_test/key_repository_test.dart
@@ -9,6 +9,7 @@ import 'package:seos_mobile_keys_plugin/seos_mobile_keys_plugin.dart';
import '../booking_details_test/booking_details_repository_test.dart';
class MockAndroidDeviceInfo extends Mock implements AndroidDeviceInfo {}
+
class MockDeviceInfoPlugin extends Mock implements DeviceInfoPlugin {}
void main() {
@@ -21,33 +22,59 @@ void main() {
mockSeosMobileKeysPlugin = MockSeosMobileKeysPlugin();
mockAndroidDeviceInfo = MockAndroidDeviceInfo();
mockDeviceInfoPlugin = MockDeviceInfoPlugin();
- keyRepository = KeyRepository(seosMobileKeysPlugin: mockSeosMobileKeysPlugin, deviceInfoPlugin: mockDeviceInfoPlugin);
+ keyRepository = KeyRepository(mockSeosMobileKeysPlugin);
});
group('KeyRepository', () {
test('checkDeviceInfo calls _checkAndStartScan on Android SDK >= 33', () async {
when(() => mockDeviceInfoPlugin.androidInfo).thenAnswer((_) async => mockAndroidDeviceInfo);
when(() => mockAndroidDeviceInfo.version.sdkInt).thenReturn(33);
- when(() => Permission.bluetoothScan.request()).thenAnswer((_) async => PermissionStatus.granted);
- when(() => Permission.bluetoothConnect.request()).thenAnswer((_) async => PermissionStatus.granted);
- when(() => Permission.bluetoothAdvertise.request()).thenAnswer((_) async => PermissionStatus.granted);
- when(() => Permission.notification.request()).thenAnswer((_) async => PermissionStatus.granted);
+ when(
+ () => Permission.bluetoothScan.request(),
+ ).thenAnswer((_) async => PermissionStatus.granted);
+ when(
+ () => Permission.bluetoothConnect.request(),
+ ).thenAnswer((_) async => PermissionStatus.granted);
+ when(
+ () => Permission.bluetoothAdvertise.request(),
+ ).thenAnswer((_) async => PermissionStatus.granted);
+ when(
+ () => Permission.notification.request(),
+ ).thenAnswer((_) async => PermissionStatus.granted);
await keyRepository.checkDeviceInfo();
- verify(() => mockSeosMobileKeysPlugin.startReaderScan(MobileKeysScanMode.optimizePerformance, [any()], [any()])).called(1);
+ verify(
+ () => mockSeosMobileKeysPlugin.startReaderScan(
+ MobileKeysScanMode.optimizePerformance,
+ [any()],
+ [any()],
+ ),
+ ).called(1);
});
test('checkDeviceInfo calls _checkAndStartScan on Android SDK >= 31', () async {
when(() => mockDeviceInfoPlugin.androidInfo).thenAnswer((_) async => mockAndroidDeviceInfo);
when(() => mockAndroidDeviceInfo.version.sdkInt).thenReturn(31);
- when(() => Permission.bluetoothScan.request()).thenAnswer((_) async => PermissionStatus.granted);
- when(() => Permission.bluetoothConnect.request()).thenAnswer((_) async => PermissionStatus.granted);
- when(() => Permission.bluetoothAdvertise.request()).thenAnswer((_) async => PermissionStatus.granted);
+ when(
+ () => Permission.bluetoothScan.request(),
+ ).thenAnswer((_) async => PermissionStatus.granted);
+ when(
+ () => Permission.bluetoothConnect.request(),
+ ).thenAnswer((_) async => PermissionStatus.granted);
+ when(
+ () => Permission.bluetoothAdvertise.request(),
+ ).thenAnswer((_) async => PermissionStatus.granted);
await keyRepository.checkDeviceInfo();
- verify(() => mockSeosMobileKeysPlugin.startReaderScan(MobileKeysScanMode.optimizePerformance, [any()], [any()])).called(1);
+ verify(
+ () => mockSeosMobileKeysPlugin.startReaderScan(
+ MobileKeysScanMode.optimizePerformance,
+ [any()],
+ [any()],
+ ),
+ ).called(1);
});
test('checkDeviceInfo calls _checkAndStartScan on Android SDK < 31', () async {
@@ -57,7 +84,13 @@ void main() {
await keyRepository.checkDeviceInfo();
- verify(() => mockSeosMobileKeysPlugin.startReaderScan(MobileKeysScanMode.optimizePerformance, [any()], [any()])).called(1);
+ verify(
+ () => mockSeosMobileKeysPlugin.startReaderScan(
+ MobileKeysScanMode.optimizePerformance,
+ [any()],
+ [any()],
+ ),
+ ).called(1);
});
test('stopScanning calls stopReaderScan', () async {
@@ -73,10 +106,15 @@ void main() {
});
test('_startScanning calls startReaderScan', () async {
-
keyRepository.startScanning();
- verify(() => mockSeosMobileKeysPlugin.startReaderScan(MobileKeysScanMode.optimizePerformance, [any()], [any()])).called(1);
+ verify(
+ () => mockSeosMobileKeysPlugin.startReaderScan(
+ MobileKeysScanMode.optimizePerformance,
+ [any()],
+ [any()],
+ ),
+ ).called(1);
});
});
-}
\ No newline at end of file
+}