6177214e-ce7c-49e3-99de-ff9721b26f63 — Commit 3b0497c6
Changed files
.../check_in/bloc/check_in_state.freezed.dart | 70 ++++++++--- .../lib/check_in/bloc/check_in_cubit.dart | 137 +++++++++++++++++++++ .../lib/check_in/bloc/check_in_state.dart | 35 ++++++ comwell_key_app/lib/check_in/check_in_flow.dart | 62 ++++++++++ comwell_key_app/lib/check_in/check_in_route.dart | 18 +++ .../check_in/components/check_in_bottom_sheet.dart | 63 ++++++++++ .../lib/check_in/pages/check_in_page_enum.dart | 20 +++ .../lib/check_in/pages/check_in_payment_page.dart | 38 ++++++ .../check_in/pages/check_in_processing_page.dart | 102 +++++++++++++++ .../components/check_in_button.dart | 5 +- .../components/check_in_button_timer.dart | 18 +-- comwell_key_app/lib/routing/app_router.dart | 1 + comwell_key_app/lib/routing/app_routes.dart | 1 + 13 files changed, 540 insertions(+), 30 deletions(-)
Diff
diff --git a/comwell_key_app/lib/.generated/check_in/bloc/check_in_state.freezed.dart b/comwell_key_app/lib/.generated/check_in/bloc/check_in_state.freezed.dart
index 208bb28e..844d4295 100644
--- a/comwell_key_app/lib/.generated/check_in/bloc/check_in_state.freezed.dart
+++ b/comwell_key_app/lib/.generated/check_in/bloc/check_in_state.freezed.dart
@@ -14,7 +14,8 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CheckInState {
- bool get isLoading; CheckInStatus get cardState; String get roomNumber;
+ bool get isLoading; CheckInStatus get cardState; String get roomNumber;// Payment-related fields
+ List<BookingAddonItem> get items; bool get isTermsAccepted; bool get applyClubPoints; int get clubPoints; bool get showTermsError; bool get isPaymentProcessingNeeded; CheckInPaymentStatus get paymentStatus;
/// Create a copy of CheckInState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +26,16 @@ $CheckInStateCopyWith<CheckInState> get copyWith => _$CheckInStateCopyWithImpl<C
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is CheckInState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.cardState, cardState) || other.cardState == cardState)&&(identical(other.roomNumber, roomNumber) || other.roomNumber == roomNumber));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is CheckInState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.cardState, cardState) || other.cardState == cardState)&&(identical(other.roomNumber, roomNumber) || other.roomNumber == roomNumber)&&const DeepCollectionEquality().equals(other.items, items)&&(identical(other.isTermsAccepted, isTermsAccepted) || other.isTermsAccepted == isTermsAccepted)&&(identical(other.applyClubPoints, applyClubPoints) || other.applyClubPoints == applyClubPoints)&&(identical(other.clubPoints, clubPoints) || other.clubPoints == clubPoints)&&(identical(other.showTermsError, showTermsError) || other.showTermsError == showTermsError)&&(identical(other.isPaymentProcessingNeeded, isPaymentProcessingNeeded) || other.isPaymentProcessingNeeded == isPaymentProcessingNeeded)&&(identical(other.paymentStatus, paymentStatus) || other.paymentStatus == paymentStatus));
}
@override
-int get hashCode => Object.hash(runtimeType,isLoading,cardState,roomNumber);
+int get hashCode => Object.hash(runtimeType,isLoading,cardState,roomNumber,const DeepCollectionEquality().hash(items),isTermsAccepted,applyClubPoints,clubPoints,showTermsError,isPaymentProcessingNeeded,paymentStatus);
@override
String toString() {
- return 'CheckInState(isLoading: $isLoading, cardState: $cardState, roomNumber: $roomNumber)';
+ return 'CheckInState(isLoading: $isLoading, cardState: $cardState, roomNumber: $roomNumber, items: $items, isTermsAccepted: $isTermsAccepted, applyClubPoints: $applyClubPoints, clubPoints: $clubPoints, showTermsError: $showTermsError, isPaymentProcessingNeeded: $isPaymentProcessingNeeded, paymentStatus: $paymentStatus)';
}
@@ -45,7 +46,7 @@ abstract mixin class $CheckInStateCopyWith<$Res> {
factory $CheckInStateCopyWith(CheckInState value, $Res Function(CheckInState) _then) = _$CheckInStateCopyWithImpl;
@useResult
$Res call({
- bool isLoading, CheckInStatus cardState, String roomNumber
+ bool isLoading, CheckInStatus cardState, String roomNumber, List<BookingAddonItem> items, bool isTermsAccepted, bool applyClubPoints, int clubPoints, bool showTermsError, bool isPaymentProcessingNeeded, CheckInPaymentStatus paymentStatus
});
@@ -62,12 +63,19 @@ class _$CheckInStateCopyWithImpl<$Res>
/// Create a copy of CheckInState
/// with the given fields replaced by the non-null parameter values.
-@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? cardState = null,Object? roomNumber = null,}) {
+@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? cardState = null,Object? roomNumber = null,Object? items = null,Object? isTermsAccepted = null,Object? applyClubPoints = null,Object? clubPoints = null,Object? showTermsError = null,Object? isPaymentProcessingNeeded = null,Object? paymentStatus = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,cardState: null == cardState ? _self.cardState : cardState // ignore: cast_nullable_to_non_nullable
as CheckInStatus,roomNumber: null == roomNumber ? _self.roomNumber : roomNumber // ignore: cast_nullable_to_non_nullable
-as String,
+as String,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
+as List<BookingAddonItem>,isTermsAccepted: null == isTermsAccepted ? _self.isTermsAccepted : isTermsAccepted // ignore: cast_nullable_to_non_nullable
+as bool,applyClubPoints: null == applyClubPoints ? _self.applyClubPoints : applyClubPoints // ignore: cast_nullable_to_non_nullable
+as bool,clubPoints: null == clubPoints ? _self.clubPoints : clubPoints // ignore: cast_nullable_to_non_nullable
+as int,showTermsError: null == showTermsError ? _self.showTermsError : showTermsError // ignore: cast_nullable_to_non_nullable
+as bool,isPaymentProcessingNeeded: null == isPaymentProcessingNeeded ? _self.isPaymentProcessingNeeded : isPaymentProcessingNeeded // ignore: cast_nullable_to_non_nullable
+as bool,paymentStatus: null == paymentStatus ? _self.paymentStatus : paymentStatus // ignore: cast_nullable_to_non_nullable
+as CheckInPaymentStatus,
));
}
@@ -152,10 +160,10 @@ return $default(_that);case _:
/// }
/// ```
-@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, CheckInStatus cardState, String roomNumber)? $default,{required TResult orElse(),}) {final _that = this;
+@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, CheckInStatus cardState, String roomNumber, List<BookingAddonItem> items, bool isTermsAccepted, bool applyClubPoints, int clubPoints, bool showTermsError, bool isPaymentProcessingNeeded, CheckInPaymentStatus paymentStatus)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _CheckInState() when $default != null:
-return $default(_that.isLoading,_that.cardState,_that.roomNumber);case _:
+return $default(_that.isLoading,_that.cardState,_that.roomNumber,_that.items,_that.isTermsAccepted,_that.applyClubPoints,_that.clubPoints,_that.showTermsError,_that.isPaymentProcessingNeeded,_that.paymentStatus);case _:
return orElse();
}
@@ -173,10 +181,10 @@ return $default(_that.isLoading,_that.cardState,_that.roomNumber);case _:
/// }
/// ```
-@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, CheckInStatus cardState, String roomNumber) $default,) {final _that = this;
+@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, CheckInStatus cardState, String roomNumber, List<BookingAddonItem> items, bool isTermsAccepted, bool applyClubPoints, int clubPoints, bool showTermsError, bool isPaymentProcessingNeeded, CheckInPaymentStatus paymentStatus) $default,) {final _that = this;
switch (_that) {
case _CheckInState():
-return $default(_that.isLoading,_that.cardState,_that.roomNumber);case _:
+return $default(_that.isLoading,_that.cardState,_that.roomNumber,_that.items,_that.isTermsAccepted,_that.applyClubPoints,_that.clubPoints,_that.showTermsError,_that.isPaymentProcessingNeeded,_that.paymentStatus);case _:
throw StateError('Unexpected subclass');
}
@@ -193,10 +201,10 @@ return $default(_that.isLoading,_that.cardState,_that.roomNumber);case _:
/// }
/// ```
-@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, CheckInStatus cardState, String roomNumber)? $default,) {final _that = this;
+@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, CheckInStatus cardState, String roomNumber, List<BookingAddonItem> items, bool isTermsAccepted, bool applyClubPoints, int clubPoints, bool showTermsError, bool isPaymentProcessingNeeded, CheckInPaymentStatus paymentStatus)? $default,) {final _that = this;
switch (_that) {
case _CheckInState() when $default != null:
-return $default(_that.isLoading,_that.cardState,_that.roomNumber);case _:
+return $default(_that.isLoading,_that.cardState,_that.roomNumber,_that.items,_that.isTermsAccepted,_that.applyClubPoints,_that.clubPoints,_that.showTermsError,_that.isPaymentProcessingNeeded,_that.paymentStatus);case _:
return null;
}
@@ -208,12 +216,27 @@ return $default(_that.isLoading,_that.cardState,_that.roomNumber);case _:
class _CheckInState extends CheckInState {
- const _CheckInState({this.isLoading = false, this.cardState = CheckInStatus.loading, this.roomNumber = ""}): super._();
+ const _CheckInState({this.isLoading = false, this.cardState = CheckInStatus.loading, this.roomNumber = "", final List<BookingAddonItem> items = const [], this.isTermsAccepted = false, this.applyClubPoints = false, this.clubPoints = 0, this.showTermsError = false, this.isPaymentProcessingNeeded = true, this.paymentStatus = CheckInPaymentStatus.idle}): _items = items,super._();
@override@JsonKey() final bool isLoading;
@override@JsonKey() final CheckInStatus cardState;
@override@JsonKey() final String roomNumber;
+// Payment-related fields
+ final List<BookingAddonItem> _items;
+// Payment-related fields
+@override@JsonKey() List<BookingAddonItem> get items {
+ if (_items is EqualUnmodifiableListView) return _items;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_items);
+}
+
+@override@JsonKey() final bool isTermsAccepted;
+@override@JsonKey() final bool applyClubPoints;
+@override@JsonKey() final int clubPoints;
+@override@JsonKey() final bool showTermsError;
+@override@JsonKey() final bool isPaymentProcessingNeeded;
+@override@JsonKey() final CheckInPaymentStatus paymentStatus;
/// Create a copy of CheckInState
/// with the given fields replaced by the non-null parameter values.
@@ -225,16 +248,16 @@ _$CheckInStateCopyWith<_CheckInState> get copyWith => __$CheckInStateCopyWithImp
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is _CheckInState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.cardState, cardState) || other.cardState == cardState)&&(identical(other.roomNumber, roomNumber) || other.roomNumber == roomNumber));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _CheckInState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.cardState, cardState) || other.cardState == cardState)&&(identical(other.roomNumber, roomNumber) || other.roomNumber == roomNumber)&&const DeepCollectionEquality().equals(other._items, _items)&&(identical(other.isTermsAccepted, isTermsAccepted) || other.isTermsAccepted == isTermsAccepted)&&(identical(other.applyClubPoints, applyClubPoints) || other.applyClubPoints == applyClubPoints)&&(identical(other.clubPoints, clubPoints) || other.clubPoints == clubPoints)&&(identical(other.showTermsError, showTermsError) || other.showTermsError == showTermsError)&&(identical(other.isPaymentProcessingNeeded, isPaymentProcessingNeeded) || other.isPaymentProcessingNeeded == isPaymentProcessingNeeded)&&(identical(other.paymentStatus, paymentStatus) || other.paymentStatus == paymentStatus));
}
@override
-int get hashCode => Object.hash(runtimeType,isLoading,cardState,roomNumber);
+int get hashCode => Object.hash(runtimeType,isLoading,cardState,roomNumber,const DeepCollectionEquality().hash(_items),isTermsAccepted,applyClubPoints,clubPoints,showTermsError,isPaymentProcessingNeeded,paymentStatus);
@override
String toString() {
- return 'CheckInState(isLoading: $isLoading, cardState: $cardState, roomNumber: $roomNumber)';
+ return 'CheckInState(isLoading: $isLoading, cardState: $cardState, roomNumber: $roomNumber, items: $items, isTermsAccepted: $isTermsAccepted, applyClubPoints: $applyClubPoints, clubPoints: $clubPoints, showTermsError: $showTermsError, isPaymentProcessingNeeded: $isPaymentProcessingNeeded, paymentStatus: $paymentStatus)';
}
@@ -245,7 +268,7 @@ abstract mixin class _$CheckInStateCopyWith<$Res> implements $CheckInStateCopyWi
factory _$CheckInStateCopyWith(_CheckInState value, $Res Function(_CheckInState) _then) = __$CheckInStateCopyWithImpl;
@override @useResult
$Res call({
- bool isLoading, CheckInStatus cardState, String roomNumber
+ bool isLoading, CheckInStatus cardState, String roomNumber, List<BookingAddonItem> items, bool isTermsAccepted, bool applyClubPoints, int clubPoints, bool showTermsError, bool isPaymentProcessingNeeded, CheckInPaymentStatus paymentStatus
});
@@ -262,12 +285,19 @@ class __$CheckInStateCopyWithImpl<$Res>
/// Create a copy of CheckInState
/// with the given fields replaced by the non-null parameter values.
-@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? cardState = null,Object? roomNumber = null,}) {
+@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? cardState = null,Object? roomNumber = null,Object? items = null,Object? isTermsAccepted = null,Object? applyClubPoints = null,Object? clubPoints = null,Object? showTermsError = null,Object? isPaymentProcessingNeeded = null,Object? paymentStatus = null,}) {
return _then(_CheckInState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,cardState: null == cardState ? _self.cardState : cardState // ignore: cast_nullable_to_non_nullable
as CheckInStatus,roomNumber: null == roomNumber ? _self.roomNumber : roomNumber // ignore: cast_nullable_to_non_nullable
-as String,
+as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
+as List<BookingAddonItem>,isTermsAccepted: null == isTermsAccepted ? _self.isTermsAccepted : isTermsAccepted // ignore: cast_nullable_to_non_nullable
+as bool,applyClubPoints: null == applyClubPoints ? _self.applyClubPoints : applyClubPoints // ignore: cast_nullable_to_non_nullable
+as bool,clubPoints: null == clubPoints ? _self.clubPoints : clubPoints // ignore: cast_nullable_to_non_nullable
+as int,showTermsError: null == showTermsError ? _self.showTermsError : showTermsError // ignore: cast_nullable_to_non_nullable
+as bool,isPaymentProcessingNeeded: null == isPaymentProcessingNeeded ? _self.isPaymentProcessingNeeded : isPaymentProcessingNeeded // ignore: cast_nullable_to_non_nullable
+as bool,paymentStatus: null == paymentStatus ? _self.paymentStatus : paymentStatus // ignore: cast_nullable_to_non_nullable
+as CheckInPaymentStatus,
));
}
diff --git a/comwell_key_app/lib/check_in/bloc/check_in_cubit.dart b/comwell_key_app/lib/check_in/bloc/check_in_cubit.dart
index 45dab4e0..fa261938 100644
--- a/comwell_key_app/lib/check_in/bloc/check_in_cubit.dart
+++ b/comwell_key_app/lib/check_in/bloc/check_in_cubit.dart
@@ -1,21 +1,152 @@
import 'package:comwell_key_app/base/base_cubit.dart';
import 'package:comwell_key_app/check_in/bloc/check_in_state.dart';
import 'package:comwell_key_app/check_in/check_in_repository.dart';
+import 'package:comwell_key_app/domain/repositories/booking_details_repository.dart';
+import 'package:comwell_key_app/domain/repositories/profile_repository.dart';
import 'package:comwell_key_app/overview/models/booking.dart';
+import 'package:comwell_key_app/routing/app_routes.dart';
+import 'package:comwell_key_app/services/models/booking_dto.dart';
+import 'package:comwell_key_app/tracking/comwell_tracking.dart';
+import 'package:comwell_key_app/tracking/models/analytics_event_item.dart';
+import 'package:comwell_key_app/utils/locator.dart';
+import 'package:comwell_key_app/utils/urls.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:payment_plugin/presentation/app/bloc/payment_cubit.dart';
+import 'package:url_launcher/url_launcher.dart';
class CheckInCubit extends BaseCubit<CheckInState> {
final _checkInRepository = CheckInRepository();
+ final ProfileRepository profileRepository = locator<ProfileRepository>();
+ final BookingDetailsRepository bookingDetailsRepository = locator<BookingDetailsRepository>();
+ final _tracking = locator<ComwellTracking>();
+ final pageController = PageController();
+ bool _isAnimating = false;
late final Booking booking;
+ PaymentCubit? paymentServicesCubit;
CheckInCubit(this.booking) : super(const CheckInState()) {
init();
}
+ CheckInCubit.withPayment(this.booking, this.paymentServicesCubit)
+ : super(const CheckInState()) {
+ initPaymentFlow();
+ }
+
CheckInCubit.initialOnlyKeys(this.booking) : super(const CheckInState()) {
initOnlyKeys();
}
+ /// Initialize the payment flow: fetch club points, booking details, and add-on items
+ Future<void> initPaymentFlow() async {
+ try {
+ safeEmit(state.copyWith(isLoading: true));
+ final user = await profileRepository.fetchProfileSettings();
+ safeEmit(state.copyWith(clubPoints: user.points));
+
+ booking = await bookingDetailsRepository.getRemoteBookingDetails(
+ booking.confirmationNumber,
+ booking.hotelCode,
+ );
+
+ setItems(booking.addOnItems ?? []);
+ safeEmit(state.copyWith(isLoading: false));
+ } catch (e) {
+ logError(e, StackTrace.current);
+ safeEmit(state.copyWith(isLoading: false));
+ }
+ }
+
+ void setItems(Iterable<BookingAddonItem> items) {
+ final newItems = <BookingAddonItem>[];
+ for (var item in items) {
+ newItems.add(item.copyWith(price: item.price * item.quantity));
+ }
+ safeEmit(state.copyWith(items: newItems));
+ }
+
+ void onApplyClubPointsClicked(bool value) {
+ safeEmit(state.copyWith(applyClubPoints: value));
+ }
+
+ void onAcceptTermsChanged(bool value) {
+ if (value) {
+ safeEmit(state.copyWith(isTermsAccepted: true, showTermsError: false));
+ } else {
+ safeEmit(state.copyWith(isTermsAccepted: false));
+ }
+ }
+
+ void showTermsAndConditions() {
+ launchUrl(Uri.parse(ComwellUrls.termsAndConditions));
+ }
+
+ void onContinueClicked(BuildContext context) {
+ if (!state.isTermsAccepted && (booking.balance ?? 0.0) > 0.0) {
+ safeEmit(state.copyWith(showTermsError: true));
+ } else {
+ if (booking.balance == 0 || booking.balance == null) {
+ // No payment needed, go straight to check-in processing
+ safeEmit(state.copyWith(isPaymentProcessingNeeded: false));
+ _startCheckIn(context);
+ } else {
+ context.push(AppRoutes.paymentProcessing);
+ processPayment();
+ }
+ }
+ }
+
+ Future<void> processPayment() async {
+ final analyticsEventItem = AnalyticsEventItem(
+ hotelName: booking.hotelName,
+ currency: "DKK",
+ value: booking.balance?.toInt() ?? 0,
+ placement: "placement",
+ items: booking.addOnItems?.map((e) => e.description).toList() ?? [],
+ itemId: "itemId",
+ itemName: "itemName",
+ price: booking.balance?.toInt() ?? 0,
+ quantity: 1,
+ );
+ _tracking.trackBeginCheckout(analyticsEventItem);
+ try {
+ await paymentServicesCubit?.createSession(
+ booking.balance?.toInt() ?? 0,
+ booking.confirmationNumber,
+ state.applyClubPoints,
+ booking.hotelCode,
+ );
+ await Future<void>.delayed(const Duration(milliseconds: 4000));
+ } catch (e) {
+ safeEmit(state.copyWith(paymentStatus: CheckInPaymentStatus.error));
+ }
+ }
+
+ void _startCheckIn(BuildContext context) {
+ context.push(AppRoutes.checkIn, extra: [booking, false]);
+ }
+
+ bool onBackPressed() {
+ if (pageController.page == 0.0) return false;
+ if (_isAnimating) return true;
+ _isAnimating = true;
+ pageController
+ .previousPage(duration: const Duration(milliseconds: 500), curve: Curves.fastOutSlowIn)
+ .then((_) {
+ _isAnimating = false;
+ });
+ return true;
+ }
+
+ /// Removes the decimal point from the balance if it is .0
+ int get trimmedBalance {
+ final balance = booking.balance;
+ if (balance == null) return 0;
+ return balance.toInt();
+ }
+
Future<void> initOnlyKeys() async {
try {
final bookingDetails = await _checkInRepository.getBookingDetails(
@@ -71,5 +202,11 @@ class CheckInCubit extends BaseCubit<CheckInState> {
}
}
+ @override
+ Future<void> close() {
+ pageController.dispose();
+ return super.close();
+ }
+
static const _getKeysRetryAttempts = 3;
}
diff --git a/comwell_key_app/lib/check_in/bloc/check_in_state.dart b/comwell_key_app/lib/check_in/bloc/check_in_state.dart
index 7eb27561..e0a077f9 100644
--- a/comwell_key_app/lib/check_in/bloc/check_in_state.dart
+++ b/comwell_key_app/lib/check_in/bloc/check_in_state.dart
@@ -1,3 +1,4 @@
+import 'package:comwell_key_app/services/models/booking_dto.dart';
import 'package:comwell_key_app/utils/l10n_utils.dart';
import 'package:flutter/cupertino.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -11,16 +12,50 @@ enum CheckInStatus {
error,
}
+enum CheckInPaymentStatus {
+ idle,
+ processing,
+ success,
+ error,
+}
+
@freezed
abstract class CheckInState with _$CheckInState {
const factory CheckInState({
@Default(false) bool isLoading,
@Default(CheckInStatus.loading) CheckInStatus cardState,
@Default("") String roomNumber,
+ // Payment-related fields
+ @Default([]) List<BookingAddonItem> items,
+ @Default(false) bool isTermsAccepted,
+ @Default(false) bool applyClubPoints,
+ @Default(0) int clubPoints,
+ @Default(false) bool showTermsError,
+ @Default(true) bool isPaymentProcessingNeeded,
+ @Default(CheckInPaymentStatus.idle) CheckInPaymentStatus paymentStatus,
}) = _CheckInState;
const CheckInState._();
+ int get totalPriceBeforeDiscount => _sumOfList(items);
+
+ int get totalPriceAfterDiscount => _sumOfList(itemsWithDiscount);
+
+ Iterable<BookingAddonItem> get itemsWithDiscount {
+ if (applyClubPoints) {
+ return [
+ ...items,
+ BookingAddonItem("discount", "discount", clubPoints * -1, clubPoints * -1),
+ ];
+ }
+ return items;
+ }
+
+ int _sumOfList(Iterable<BookingAddonItem> list) {
+ if (list.isEmpty) return 0;
+ return list.map((item) => item.price).reduce((total, price) => total + price);
+ }
+
String titleStringId(BuildContext context) {
switch (cardState) {
case CheckInStatus.loading:
diff --git a/comwell_key_app/lib/check_in/check_in_flow.dart b/comwell_key_app/lib/check_in/check_in_flow.dart
new file mode 100644
index 00000000..7f027f35
--- /dev/null
+++ b/comwell_key_app/lib/check_in/check_in_flow.dart
@@ -0,0 +1,62 @@
+import 'package:comwell_key_app/check_in/bloc/check_in_cubit.dart';
+import 'package:comwell_key_app/check_in/bloc/check_in_state.dart';
+import 'package:comwell_key_app/check_in/components/check_in_bottom_sheet.dart';
+import 'package:comwell_key_app/check_in/pages/check_in_page_enum.dart';
+import 'package:comwell_key_app/check_in/pages/check_in_processing_page.dart';
+import 'package:comwell_key_app/common/components/comwell_app_bar.dart';
+import 'package:comwell_key_app/routing/app_routes.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
+import 'package:payment_plugin/presentation/app/bloc/payment_cubit.dart';
+import 'package:payment_plugin/presentation/app/bloc/payment_processing_state.dart';
+
+class CheckInFlow extends StatelessWidget {
+ const CheckInFlow({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final cubit = context.read<CheckInCubit>();
+ if (!cubit.state.isPaymentProcessingNeeded) {
+ return const CheckInProcessingPage();
+ } else {
+ return BlocListener<PaymentCubit, PaymentProcessingState>(
+ listenWhen: (previous, current) =>
+ previous is! PaymentProcessingStateConfirmed &&
+ current is PaymentProcessingStateConfirmed,
+ listener: (context, state) async {
+ if (state is PaymentProcessingStateConfirmed) {
+ //This is here to add time so that the payment is represented in the BookingDetails
+ await Future<void>.delayed(const Duration(seconds: 1));
+ // After payment, navigate to the actual check-in processing
+ if (context.mounted) {
+ context.push(AppRoutes.checkIn, extra: [cubit.booking, false]);
+ }
+ }
+ },
+ child: BlocBuilder<CheckInCubit, CheckInState>(
+ builder: (context, state) {
+ return Scaffold(
+ appBar: ComwellAppBar(
+ shouldShowProfileButton: false,
+ onBackPressed: () {
+ final didScroll = cubit.onBackPressed();
+ if (!didScroll) context.pop();
+ },
+ ),
+ bottomSheet: CheckInBottomSheet(state: state),
+ backgroundColor: Colors.white,
+ body: PageView(
+ controller: cubit.pageController,
+ key: const PageStorageKey("check_in_flow"),
+ physics: const NeverScrollableScrollPhysics(),
+ children:
+ CheckInFlowPage.getPages(ValueKey(state)).toList(),
+ ),
+ );
+ },
+ ),
+ );
+ }
+ }
+}
diff --git a/comwell_key_app/lib/check_in/check_in_route.dart b/comwell_key_app/lib/check_in/check_in_route.dart
index f5e898b0..0e8133e9 100644
--- a/comwell_key_app/lib/check_in/check_in_route.dart
+++ b/comwell_key_app/lib/check_in/check_in_route.dart
@@ -1,7 +1,10 @@
import 'package:comwell_key_app/check_in/bloc/check_in_cubit.dart';
+import 'package:comwell_key_app/check_in/bloc/check_in_state.dart';
+import 'package:comwell_key_app/check_in/check_in_flow.dart';
import 'package:comwell_key_app/check_in/check_in_page.dart';
import 'package:comwell_key_app/overview/models/booking.dart';
import 'package:comwell_key_app/routing/app_routes.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -22,3 +25,18 @@ final checkInRoute = GoRoute(
);
},
);
+
+final checkInPaymentRoute = GoRoute(
+ path: AppRoutes.checkInPayment,
+ builder: (context, state) {
+ final booking = state.extra as Booking;
+ return BlocProvider(
+ create: (context) => CheckInCubit.withPayment(booking, context.read()),
+ child: BlocBuilder<CheckInCubit, CheckInState>(
+ builder: (context, checkInState) {
+ return CheckInFlow(key: ValueKey(checkInState));
+ },
+ ),
+ );
+ },
+);
diff --git a/comwell_key_app/lib/check_in/components/check_in_bottom_sheet.dart b/comwell_key_app/lib/check_in/components/check_in_bottom_sheet.dart
new file mode 100644
index 00000000..0e61f1b6
--- /dev/null
+++ b/comwell_key_app/lib/check_in/components/check_in_bottom_sheet.dart
@@ -0,0 +1,63 @@
+import 'package:comwell_key_app/check_in/bloc/check_in_cubit.dart';
+import 'package:comwell_key_app/check_in/bloc/check_in_state.dart';
+import 'package:comwell_key_app/themes/light_theme.dart';
+import 'package:comwell_key_app/utils/l10n_utils.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class CheckInBottomSheet extends StatelessWidget {
+ final CheckInState state;
+ const CheckInBottomSheet({super.key, required this.state});
+
+ @override
+ Widget build(BuildContext context) {
+ final cubit = context.read<CheckInCubit>();
+ final theme = Theme.of(context);
+ final isEnabled = state.isTermsAccepted;
+
+ final buttonStyle = ButtonStyle(
+ backgroundColor: WidgetStateProperty.resolveWith((states) {
+ if (states.contains(WidgetState.disabled)) {
+ return sandColor.withValues(alpha: 0.5);
+ }
+ return sandColor;
+ }),
+ foregroundColor: const WidgetStatePropertyAll(colorBackground),
+ );
+
+ return Container(
+ decoration: const BoxDecoration(
+ shape: BoxShape.rectangle, color: colorBackground),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Divider(color: colorDivider),
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 18.0, right: 18.0, bottom: 32.0, top: 18.0),
+ child: Row(
+ children: [
+ Expanded(
+ child: ElevatedButton(
+ onPressed: isEnabled
+ ? () => cubit.onContinueClicked(context)
+ : null,
+ style: buttonStyle,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 17.0),
+ child: Text(
+ context.strings.checkout_page_payment_payment_title,
+ style: theme.textTheme.bodyMedium
+ ?.copyWith(color: colorBackground),
+ ),
+ )),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 8)
+ ],
+ ),
+ );
+ }
+}
diff --git a/comwell_key_app/lib/check_in/pages/check_in_page_enum.dart b/comwell_key_app/lib/check_in/pages/check_in_page_enum.dart
new file mode 100644
index 00000000..0d51cbc2
--- /dev/null
+++ b/comwell_key_app/lib/check_in/pages/check_in_page_enum.dart
@@ -0,0 +1,20 @@
+import 'package:flutter/cupertino.dart';
+
+import 'check_in_payment_page.dart';
+
+enum CheckInFlowPage {
+ payment;
+
+ static CheckInFlowPage fromIndex(int index) {
+ return CheckInFlowPage.values[index];
+ }
+
+ static Iterable<Widget> getPages(Key key) {
+ return CheckInFlowPage.values.map((page) {
+ switch (page) {
+ case CheckInFlowPage.payment:
+ return CheckInPaymentPage(key: key);
+ }
+ });
+ }
+}
diff --git a/comwell_key_app/lib/check_in/pages/check_in_payment_page.dart b/comwell_key_app/lib/check_in/pages/check_in_payment_page.dart
new file mode 100644
index 00000000..c34e2470
--- /dev/null
+++ b/comwell_key_app/lib/check_in/pages/check_in_payment_page.dart
@@ -0,0 +1,38 @@
+import 'package:comwell_key_app/check_in/bloc/check_in_cubit.dart';
+import 'package:comwell_key_app/common/template_pages/payment_page_template.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class CheckInPaymentPage extends StatelessWidget {
+ const CheckInPaymentPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final cubit = context.read<CheckInCubit>();
+ final addOnItems = cubit.state.items;
+ final applyClubPoints = cubit.state.applyClubPoints;
+ final totalPriceBeforeDiscount = cubit.state.totalPriceBeforeDiscount;
+ final totalPriceAfterDiscount = cubit.state.totalPriceAfterDiscount;
+ final trimmedBalance = cubit.trimmedBalance;
+ final clubPoints = cubit.state.clubPoints;
+ final isTermsAccepted = cubit.state.isTermsAccepted;
+ final onAcceptTermsChanged = cubit.onAcceptTermsChanged;
+ final showError = cubit.state.showTermsError;
+ final onShowTermsAndConditions = cubit.showTermsAndConditions;
+ final onApplyClubPointsChanged = cubit.onApplyClubPointsClicked;
+
+ return PaymentPageTemplate(
+ addOnItems: addOnItems,
+ applyClubPoints: applyClubPoints,
+ totalPriceBeforeDiscount: totalPriceBeforeDiscount,
+ totalPriceAfterDiscount: totalPriceAfterDiscount,
+ trimmedBalance: trimmedBalance,
+ clubPoints: clubPoints,
+ isTermsAccepted: isTermsAccepted,
+ userEmail: cubit.booking.bookerLastName,
+ onAcceptTermsChanged: onAcceptTermsChanged,
+ showError: showError,
+ onShowTermsAndConditions: onShowTermsAndConditions,
+ onApplyClubPointsChanged: onApplyClubPointsChanged);
+ }
+}
diff --git a/comwell_key_app/lib/check_in/pages/check_in_processing_page.dart b/comwell_key_app/lib/check_in/pages/check_in_processing_page.dart
new file mode 100644
index 00000000..12395450
--- /dev/null
+++ b/comwell_key_app/lib/check_in/pages/check_in_processing_page.dart
@@ -0,0 +1,102 @@
+import 'package:comwell_key_app/check_in/bloc/check_in_cubit.dart';
+import 'package:comwell_key_app/check_in/bloc/check_in_state.dart';
+import 'package:comwell_key_app/themes/dark_theme.dart';
+import 'package:comwell_key_app/utils/lottie_utils.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:lottie/lottie.dart';
+
+class CheckInProcessingPage extends StatefulWidget {
+ const CheckInProcessingPage({super.key});
+
+ @override
+ State<CheckInProcessingPage> createState() => _CheckInProcessingPageState();
+}
+
+class _CheckInProcessingPageState extends State<CheckInProcessingPage>
+ with SingleTickerProviderStateMixin {
+ LottieComposition? loadingComposition;
+ late final AnimationController animationController;
+
+ @override
+ void initState() {
+ animationController = AnimationController(vsync: this);
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ animationController.dispose();
+ super.dispose();
+ }
+
+ void onAnimationEnd() {
+ playLoading();
+ }
+
+ void playLoading() {
+ loadingComposition?.playBetween(
+ animationController,
+ "spinner",
+ markerEnd: "success",
+ repeat: true,
+ );
+ }
+
+ void playError() async {
+ loadingComposition?.playBetween(
+ animationController,
+ "error",
+ repeat: true,
+ );
+ await Future<void>.delayed(const Duration(seconds: 1));
+ if (mounted) Navigator.of(context).pop();
+ }
+
+ void playSuccess() async {
+ await loadingComposition?.playBetween(
+ animationController,
+ "success",
+ markerEnd: "error",
+ );
+ await Future<void>.delayed(const Duration(seconds: 1));
+ if (mounted) Navigator.of(context).pop();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocListener<CheckInCubit, CheckInState>(
+ listenWhen: (previous, current) =>
+ previous.paymentStatus != current.paymentStatus,
+ listener: (context, state) {
+ if (state.paymentStatus == CheckInPaymentStatus.success) {
+ playSuccess();
+ } else if (state.paymentStatus == CheckInPaymentStatus.error) {
+ playError();
+ }
+ },
+ child: Scaffold(
+ body: Container(
+ alignment: Alignment.center,
+ color: sandColor[80],
+ child: Builder(builder: (context) {
+ return Lottie.asset(
+ 'assets/animations/load_animation.json',
+ controller: animationController,
+ onLoaded: (composition) {
+ if (loadingComposition == null) {
+ loadingComposition = composition;
+ animationController.duration = composition.duration;
+ playLoading();
+ }
+ },
+ fit: BoxFit.cover,
+ width: 64,
+ height: 64,
+ );
+ }),
+ ),
+ ),
+ );
+ }
+}
diff --git a/comwell_key_app/lib/presentation/screens/booking_details/components/check_in_button.dart b/comwell_key_app/lib/presentation/screens/booking_details/components/check_in_button.dart
index 2c1eeec4..7798ca99 100644
--- a/comwell_key_app/lib/presentation/screens/booking_details/components/check_in_button.dart
+++ b/comwell_key_app/lib/presentation/screens/booking_details/components/check_in_button.dart
@@ -19,11 +19,14 @@ class CheckInButton extends StatelessWidget {
}
Widget getDigitalCardWidget(BuildContext context, BookingDetailsCubit cubit) {
+ final hasBalance = (cubit.booking.balance ?? 0) > 0;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 10),
child: ElevatedButton(
onPressed: () async {
- final (result) = await context.push(AppRoutes.checkIn, extra: [cubit.booking, false]);
+ final result = hasBalance
+ ? await context.push(AppRoutes.checkInPayment, extra: cubit.booking)
+ : await context.push(AppRoutes.checkIn, extra: [cubit.booking, false]);
if (result == true && !cubit.isClosed) {
cubit.checkInEvent();
}
diff --git a/comwell_key_app/lib/presentation/screens/booking_details/components/check_in_button_timer.dart b/comwell_key_app/lib/presentation/screens/booking_details/components/check_in_button_timer.dart
index 64ce86f3..7a9a421e 100644
--- a/comwell_key_app/lib/presentation/screens/booking_details/components/check_in_button_timer.dart
+++ b/comwell_key_app/lib/presentation/screens/booking_details/components/check_in_button_timer.dart
@@ -166,15 +166,15 @@ class _CheckInButtonTimerState extends State<CheckInButtonTimer> {
),
);
- // if (_isDevOrStage) {
- // return GestureDetector(
- // onTap: () {
- // // Bypass timer in dev/stage - show check-in button
- // cubit.bypassTimer();
- // },
- // child: timerContent,
- // );
- // }
+ if (_isDevOrStage) {
+ return GestureDetector(
+ onTap: () {
+ // Bypass timer in dev/stage - show check-in button
+ cubit.bypassTimer();
+ },
+ child: timerContent,
+ );
+ }
return timerContent;
},
diff --git a/comwell_key_app/lib/routing/app_router.dart b/comwell_key_app/lib/routing/app_router.dart
index 355e6c37..2fd4aaa0 100644
--- a/comwell_key_app/lib/routing/app_router.dart
+++ b/comwell_key_app/lib/routing/app_router.dart
@@ -98,6 +98,7 @@ final router = GoRouter(
bookingDetailsRoute,
keyRoute,
checkInRoute,
+ checkInPaymentRoute,
chooseShareRoomRoute,
contactRoute,
findBookingRoute,
diff --git a/comwell_key_app/lib/routing/app_routes.dart b/comwell_key_app/lib/routing/app_routes.dart
index bc9214c5..1b848b50 100644
--- a/comwell_key_app/lib/routing/app_routes.dart
+++ b/comwell_key_app/lib/routing/app_routes.dart
@@ -25,6 +25,7 @@ abstract class AppRoutes {
static const loadingPage = "/loading-page";
static const changePassword = "/change-password";
static const checkIn = "/check-in";
+ static const checkInPayment = "/check-in-payment";
static const contact = "/contact";
static const preregistration = "/preregistration";
static const hotelInformation = "/hotel-information";