6177214e-ce7c-49e3-99de-ff9721b26f63 — Commit 9856e782

AuthorMikkel Thygesen<mikkelet@gmail.com>
Date2026-02-25 00:03:13 +0100
2777: added product details screen

Changed files

.../components/concierge_button.dart               | 132 +++++-----
 .../screens/concierge/concierge_route.dart         |  10 +-
 comwell_key_app/lib/routing/app_router.dart        |   6 +-
 comwell_key_app/pubspec.yaml                       |   1 +
 .../presentation/app/cart_cubit.freezed.dart       | 283 +++++++++++++++++++++
 .../hotel_overview_page_route.g.dart               |  17 +-
 .../bloc/product_details_state.freezed.dart        | 277 ++++++++++++++++++++
 .../product_details/product_details_route.g.dart   |  39 +++
 concierge/lib/concierge_route.dart                 |  72 ++++++
 concierge/lib/presentation/app/cart_cubit.dart     |  38 +++
 .../lib/presentation/navigation/app_routes.dart    |   1 +
 concierge/lib/presentation/navigation/router.dart  |   2 +
 .../bloc/hotel_overview_page_cubit.dart            |   1 +
 .../hotel_overview_page_route.dart                 |  13 +-
 .../widgets/product_list_tile.dart                 |  93 ++++---
 .../bloc/product_details_cubit.dart                |  30 +++
 .../bloc/product_details_state.dart                |  18 ++
 .../product_details/product_details_route.dart     |  27 ++
 .../product_details/product_details_screen.dart    |  67 +++++
 concierge/pubspec.yaml                             |   2 +-
 20 files changed, 1012 insertions(+), 117 deletions(-)

Diff

diff --git a/comwell_key_app/lib/presentation/screens/booking_details/components/concierge_button.dart b/comwell_key_app/lib/presentation/screens/booking_details/components/concierge_button.dart
index 22e45568..8cd81dc3 100644
--- a/comwell_key_app/lib/presentation/screens/booking_details/components/concierge_button.dart
+++ b/comwell_key_app/lib/presentation/screens/booking_details/components/concierge_button.dart
@@ -1,82 +1,94 @@
-
-
+import 'package:comwell_key_app/data/remote/msal_service.dart';
import 'package:comwell_key_app/presentation/screens/concierge/concierge_route.dart';
import 'package:comwell_key_app/themes/light_theme.dart';
import 'package:comwell_key_app/utils/l10n_utils.dart';
+import 'package:comwell_key_app/utils/locator.dart';
+import 'package:concierge/presentation/screens/hotel_overview_page/hotel_overview_page_route.dart';
import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
class ConciergeButton extends StatelessWidget {
final String hotelCode;
+
const ConciergeButton({super.key, required this.hotelCode});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
-
- return GestureDetector(
- onTap: () async {
- await ConciergeRoute(hotelCode: hotelCode).push<void>(context);
- },
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- child: Container(
- width: double.infinity,
- height: 211,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: colorDivider, width: 1),
- ),
- child: Stack(
- children: [
- ClipRRect(
- borderRadius: BorderRadius.circular(12),
- child: Image.asset(
- 'assets/images/catalog_image.png',
- width: double.infinity,
- height: double.infinity,
- fit: BoxFit.cover,
- ),
+
+ return GestureDetector(
+ onTap: () async {
+ final msalService = locator<MSALService>();
+ final token = await msalService.acquireTokenSilent();
+ if (context.mounted) {
+ HotelOverviewPageRoute(
+ hotelCode: hotelCode,
+ authToken: token,
+ flavor: "stage",
+ ).push(context);
+ }
+ //await ConciergeRoute(hotelCode: hotelCode).push<void>(context);
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Container(
+ width: double.infinity,
+ height: 211,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: colorDivider, width: 1),
+ ),
+ child: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: Image.asset(
+ 'assets/images/catalog_image.png',
+ width: double.infinity,
+ height: double.infinity,
+ fit: BoxFit.cover,
),
- Container(
- decoration: const BoxDecoration(
- borderRadius: BorderRadius.all(Radius.circular(12)),
- gradient: LinearGradient(
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- colors: [
- Colors.black26,
- Colors.black54,
- ],
- ),
+ ),
+ Container(
+ decoration: const BoxDecoration(
+ borderRadius: BorderRadius.all(Radius.circular(12)),
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.black26,
+ Colors.black54,
+ ],
),
),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.end,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- context.strings.up_sales_catalog_button_title,
- style: theme.textTheme.headlineMedium?.copyWith(
- color: Colors.white,
- ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.end,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ context.strings.up_sales_catalog_button_title,
+ style: theme.textTheme.headlineMedium?.copyWith(
+ color: Colors.white,
),
- Text(
- context.strings.up_sales_catalog_button_subtitle,
- style: theme.textTheme.bodySmall?.copyWith(
- color: Colors.white,
- ),
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
+ ),
+ Text(
+ context.strings.up_sales_catalog_button_subtitle,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: Colors.white,
),
- ],
- ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
),
- ],
- ),
+ ),
+ ],
),
),
- );
+ ),
+ );
}
}
diff --git a/comwell_key_app/lib/presentation/screens/concierge/concierge_route.dart b/comwell_key_app/lib/presentation/screens/concierge/concierge_route.dart
index e11edd70..2c343880 100644
--- a/comwell_key_app/lib/presentation/screens/concierge/concierge_route.dart
+++ b/comwell_key_app/lib/presentation/screens/concierge/concierge_route.dart
@@ -10,9 +10,7 @@ import 'package:comwell_key_app/presentation/navigation/transitions/slide_in_tra
part '../../../.generated/presentation/screens/concierge/concierge_route.g.dart';
-@TypedGoRoute<ConciergeRoute>(
- path: AppRoutes.concierge,
-)
+@TypedGoRoute<ConciergeRoute>(path: AppRoutes.concierge)
class ConciergeRoute extends GoRouteData with $ConciergeRoute {
final String hotelCode;
@@ -37,11 +35,7 @@ class ConciergeRoute extends GoRouteData with $ConciergeRoute {
return Text("${asyncSnapshot.error}");
}
- return ConciergeApp(
- authToken: asyncSnapshot.requireData,
- flavor: appFlavor!,
- hotelCode: hotelCode,
- );
+ return Text("data");
},
),
);
diff --git a/comwell_key_app/lib/routing/app_router.dart b/comwell_key_app/lib/routing/app_router.dart
index e44022e3..292e232d 100644
--- a/comwell_key_app/lib/routing/app_router.dart
+++ b/comwell_key_app/lib/routing/app_router.dart
@@ -27,10 +27,10 @@ import 'package:comwell_key_app/routing/go_router_observer.dart';
import 'package:comwell_key_app/share/share_booking_route.dart';
import 'package:comwell_key_app/up_sales/up_sales_route.dart';
import 'package:comwell_key_app/utils/context_utils.dart';
-import 'package:concierge/presentation/screens/hotel_overview_page/hotel_overview_page_route.dart';
+import 'package:concierge/concierge_route.dart';
+import 'package:concierge/flavors.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/cupertino.dart';
import '../presentation/screens/login/login_route.dart';
import '../presentation/screens/onboarding/onboarding_routes.dart';
import '../presentation/screens/received_shared_booking/received_shared_booking_route.dart';
@@ -90,7 +90,6 @@ final router = GoRouter(
$paymentCardsRoute,
$paymentProcessingRoute,
$receivedSharedBookingRoute,
- $conciergeRoute,
overviewRoute,
changePasswordRoute,
webviewRoute,
@@ -113,5 +112,6 @@ final router = GoRouter(
...checkOutRoutes,
upSalesRoute,
roomInfoRoute,
+ conciergeShellRoute
],
);
diff --git a/comwell_key_app/pubspec.yaml b/comwell_key_app/pubspec.yaml
index 6ffbf006..51850926 100644
--- a/comwell_key_app/pubspec.yaml
+++ b/comwell_key_app/pubspec.yaml
@@ -73,6 +73,7 @@ dependencies:
app_tracking_transparency: ^2.0.6+1
webview_flutter: ^4.13.1
app_links: ^6.4.1
+ fpdart: ^1.2.0
dependency_overrides:
#Remove override when slider button updates
diff --git a/concierge/lib/_generated/presentation/app/cart_cubit.freezed.dart b/concierge/lib/_generated/presentation/app/cart_cubit.freezed.dart
new file mode 100644
index 00000000..7f71bf4e
--- /dev/null
+++ b/concierge/lib/_generated/presentation/app/cart_cubit.freezed.dart
@@ -0,0 +1,283 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// coverage:ignore-file
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of '../../../presentation/app/cart_cubit.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+// dart format off
+T _$identity<T>(T value) => value;
+/// @nodoc
+mixin _$CartState {
+
+ bool get isLoading; AppError get error; List<Product> get productsInCart;
+/// Create a copy of CartState
+/// with the given fields replaced by the non-null parameter values.
+@JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+$CartStateCopyWith<CartState> get copyWith => _$CartStateCopyWithImpl<CartState>(this as CartState, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is CartState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&const DeepCollectionEquality().equals(other.productsInCart, productsInCart));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,isLoading,error,const DeepCollectionEquality().hash(productsInCart));
+
+@override
+String toString() {
+ return 'CartState(isLoading: $isLoading, error: $error, productsInCart: $productsInCart)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class $CartStateCopyWith<$Res> {
+ factory $CartStateCopyWith(CartState value, $Res Function(CartState) _then) = _$CartStateCopyWithImpl;
+@useResult
+$Res call({
+ bool isLoading, AppError error, List<Product> productsInCart
+});
+
+
+
+
+}
+/// @nodoc
+class _$CartStateCopyWithImpl<$Res>
+ implements $CartStateCopyWith<$Res> {
+ _$CartStateCopyWithImpl(this._self, this._then);
+
+ final CartState _self;
+ final $Res Function(CartState) _then;
+
+/// Create a copy of CartState
+/// with the given fields replaced by the non-null parameter values.
+@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? error = null,Object? productsInCart = null,}) {
+ return _then(_self.copyWith(
+isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
+as AppError,productsInCart: null == productsInCart ? _self.productsInCart : productsInCart // ignore: cast_nullable_to_non_nullable
+as List<Product>,
+ ));
+}
+
+}
+
+
+/// Adds pattern-matching-related methods to [CartState].
+extension CartStatePatterns on CartState {
+/// 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( _CartState value)? $default,{required TResult orElse(),}){
+final _that = this;
+switch (_that) {
+case _CartState() 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( _CartState value) $default,){
+final _that = this;
+switch (_that) {
+case _CartState():
+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( _CartState value)? $default,){
+final _that = this;
+switch (_that) {
+case _CartState() 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, AppError error, List<Product> productsInCart)? $default,{required TResult orElse(),}) {final _that = this;
+switch (_that) {
+case _CartState() when $default != null:
+return $default(_that.isLoading,_that.error,_that.productsInCart);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, AppError error, List<Product> productsInCart) $default,) {final _that = this;
+switch (_that) {
+case _CartState():
+return $default(_that.isLoading,_that.error,_that.productsInCart);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, AppError error, List<Product> productsInCart)? $default,) {final _that = this;
+switch (_that) {
+case _CartState() when $default != null:
+return $default(_that.isLoading,_that.error,_that.productsInCart);case _:
+ return null;
+
+}
+}
+
+}
+
+/// @nodoc
+
+
+class _CartState extends CartState {
+ const _CartState({this.isLoading = false, this.error = AppError.none, final List<Product> productsInCart = const []}): _productsInCart = productsInCart,super._();
+
+
+@override@JsonKey() final bool isLoading;
+@override@JsonKey() final AppError error;
+ final List<Product> _productsInCart;
+@override@JsonKey() List<Product> get productsInCart {
+ if (_productsInCart is EqualUnmodifiableListView) return _productsInCart;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_productsInCart);
+}
+
+
+/// Create a copy of CartState
+/// with the given fields replaced by the non-null parameter values.
+@override @JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+_$CartStateCopyWith<_CartState> get copyWith => __$CartStateCopyWithImpl<_CartState>(this, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _CartState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&const DeepCollectionEquality().equals(other._productsInCart, _productsInCart));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,isLoading,error,const DeepCollectionEquality().hash(_productsInCart));
+
+@override
+String toString() {
+ return 'CartState(isLoading: $isLoading, error: $error, productsInCart: $productsInCart)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class _$CartStateCopyWith<$Res> implements $CartStateCopyWith<$Res> {
+ factory _$CartStateCopyWith(_CartState value, $Res Function(_CartState) _then) = __$CartStateCopyWithImpl;
+@override @useResult
+$Res call({
+ bool isLoading, AppError error, List<Product> productsInCart
+});
+
+
+
+
+}
+/// @nodoc
+class __$CartStateCopyWithImpl<$Res>
+ implements _$CartStateCopyWith<$Res> {
+ __$CartStateCopyWithImpl(this._self, this._then);
+
+ final _CartState _self;
+ final $Res Function(_CartState) _then;
+
+/// Create a copy of CartState
+/// with the given fields replaced by the non-null parameter values.
+@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? error = null,Object? productsInCart = null,}) {
+ return _then(_CartState(
+isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
+as AppError,productsInCart: null == productsInCart ? _self._productsInCart : productsInCart // ignore: cast_nullable_to_non_nullable
+as List<Product>,
+ ));
+}
+
+
+}
+
+// dart format on
diff --git a/concierge/lib/_generated/presentation/screens/hotel_overview_page/hotel_overview_page_route.g.dart b/concierge/lib/_generated/presentation/screens/hotel_overview_page/hotel_overview_page_route.g.dart
index 131325e9..d6f6b950 100644
--- a/concierge/lib/_generated/presentation/screens/hotel_overview_page/hotel_overview_page_route.g.dart
+++ b/concierge/lib/_generated/presentation/screens/hotel_overview_page/hotel_overview_page_route.g.dart
@@ -15,10 +15,23 @@ RouteBase get $hotelOverviewPageRoute => GoRouteData.$route(
mixin $HotelOverviewPageRoute on GoRouteData {
static HotelOverviewPageRoute _fromState(GoRouterState state) =>
- HotelOverviewPageRoute();
+ HotelOverviewPageRoute(
+ hotelCode: state.uri.queryParameters['hotel-code']!,
+ authToken: state.uri.queryParameters['auth-token']!,
+ flavor: state.uri.queryParameters['flavor']!,
+ );
+
+ HotelOverviewPageRoute get _self => this as HotelOverviewPageRoute;
@override
- String get location => GoRouteData.$location('/hotel-overview-page');
+ String get location => GoRouteData.$location(
+ '/hotel-overview-page',
+ queryParams: {
+ 'hotel-code': _self.hotelCode,
+ 'auth-token': _self.authToken,
+ 'flavor': _self.flavor,
+ },
+ );
@override
void go(BuildContext context) => context.go(location);
diff --git a/concierge/lib/_generated/presentation/screens/product_details/bloc/product_details_state.freezed.dart b/concierge/lib/_generated/presentation/screens/product_details/bloc/product_details_state.freezed.dart
new file mode 100644
index 00000000..4358b96e
--- /dev/null
+++ b/concierge/lib/_generated/presentation/screens/product_details/bloc/product_details_state.freezed.dart
@@ -0,0 +1,277 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// coverage:ignore-file
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of '../../../../../presentation/screens/product_details/bloc/product_details_state.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+// dart format off
+T _$identity<T>(T value) => value;
+/// @nodoc
+mixin _$ProductDetailsState {
+
+ bool get isLoading; AppError get error; Product? get product;
+/// Create a copy of ProductDetailsState
+/// with the given fields replaced by the non-null parameter values.
+@JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+$ProductDetailsStateCopyWith<ProductDetailsState> get copyWith => _$ProductDetailsStateCopyWithImpl<ProductDetailsState>(this as ProductDetailsState, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is ProductDetailsState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&(identical(other.product, product) || other.product == product));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,isLoading,error,product);
+
+@override
+String toString() {
+ return 'ProductDetailsState(isLoading: $isLoading, error: $error, product: $product)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class $ProductDetailsStateCopyWith<$Res> {
+ factory $ProductDetailsStateCopyWith(ProductDetailsState value, $Res Function(ProductDetailsState) _then) = _$ProductDetailsStateCopyWithImpl;
+@useResult
+$Res call({
+ bool isLoading, AppError error, Product? product
+});
+
+
+
+
+}
+/// @nodoc
+class _$ProductDetailsStateCopyWithImpl<$Res>
+ implements $ProductDetailsStateCopyWith<$Res> {
+ _$ProductDetailsStateCopyWithImpl(this._self, this._then);
+
+ final ProductDetailsState _self;
+ final $Res Function(ProductDetailsState) _then;
+
+/// Create a copy of ProductDetailsState
+/// with the given fields replaced by the non-null parameter values.
+@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? error = null,Object? product = freezed,}) {
+ return _then(_self.copyWith(
+isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
+as AppError,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable
+as Product?,
+ ));
+}
+
+}
+
+
+/// Adds pattern-matching-related methods to [ProductDetailsState].
+extension ProductDetailsStatePatterns on ProductDetailsState {
+/// 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( _ProductDetailsState value)? $default,{required TResult orElse(),}){
+final _that = this;
+switch (_that) {
+case _ProductDetailsState() 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( _ProductDetailsState value) $default,){
+final _that = this;
+switch (_that) {
+case _ProductDetailsState():
+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( _ProductDetailsState value)? $default,){
+final _that = this;
+switch (_that) {
+case _ProductDetailsState() 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, AppError error, Product? product)? $default,{required TResult orElse(),}) {final _that = this;
+switch (_that) {
+case _ProductDetailsState() when $default != null:
+return $default(_that.isLoading,_that.error,_that.product);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, AppError error, Product? product) $default,) {final _that = this;
+switch (_that) {
+case _ProductDetailsState():
+return $default(_that.isLoading,_that.error,_that.product);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, AppError error, Product? product)? $default,) {final _that = this;
+switch (_that) {
+case _ProductDetailsState() when $default != null:
+return $default(_that.isLoading,_that.error,_that.product);case _:
+ return null;
+
+}
+}
+
+}
+
+/// @nodoc
+
+
+class _ProductDetailsState extends ProductDetailsState {
+ const _ProductDetailsState({this.isLoading = false, this.error = AppError.none, this.product}): super._();
+
+
+@override@JsonKey() final bool isLoading;
+@override@JsonKey() final AppError error;
+@override final Product? product;
+
+/// Create a copy of ProductDetailsState
+/// with the given fields replaced by the non-null parameter values.
+@override @JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+_$ProductDetailsStateCopyWith<_ProductDetailsState> get copyWith => __$ProductDetailsStateCopyWithImpl<_ProductDetailsState>(this, _$identity);
+
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProductDetailsState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&(identical(other.product, product) || other.product == product));
+}
+
+
+@override
+int get hashCode => Object.hash(runtimeType,isLoading,error,product);
+
+@override
+String toString() {
+ return 'ProductDetailsState(isLoading: $isLoading, error: $error, product: $product)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class _$ProductDetailsStateCopyWith<$Res> implements $ProductDetailsStateCopyWith<$Res> {
+ factory _$ProductDetailsStateCopyWith(_ProductDetailsState value, $Res Function(_ProductDetailsState) _then) = __$ProductDetailsStateCopyWithImpl;
+@override @useResult
+$Res call({
+ bool isLoading, AppError error, Product? product
+});
+
+
+
+
+}
+/// @nodoc
+class __$ProductDetailsStateCopyWithImpl<$Res>
+ implements _$ProductDetailsStateCopyWith<$Res> {
+ __$ProductDetailsStateCopyWithImpl(this._self, this._then);
+
+ final _ProductDetailsState _self;
+ final $Res Function(_ProductDetailsState) _then;
+
+/// Create a copy of ProductDetailsState
+/// with the given fields replaced by the non-null parameter values.
+@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? error = null,Object? product = freezed,}) {
+ return _then(_ProductDetailsState(
+isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
+as bool,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
+as AppError,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable
+as Product?,
+ ));
+}
+
+
+}
+
+// dart format on
diff --git a/concierge/lib/_generated/presentation/screens/product_details/product_details_route.g.dart b/concierge/lib/_generated/presentation/screens/product_details/product_details_route.g.dart
new file mode 100644
index 00000000..59cc9475
--- /dev/null
+++ b/concierge/lib/_generated/presentation/screens/product_details/product_details_route.g.dart
@@ -0,0 +1,39 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of '../../../../presentation/screens/product_details/product_details_route.dart';
+
+// **************************************************************************
+// GoRouterGenerator
+// **************************************************************************
+
+List<RouteBase> get $appRoutes => [$productDetailsRoute];
+
+RouteBase get $productDetailsRoute => GoRouteData.$route(
+ path: '/products/:id',
+ factory: $ProductDetailsRoute._fromState,
+);
+
+mixin $ProductDetailsRoute on GoRouteData {
+ static ProductDetailsRoute _fromState(GoRouterState state) =>
+ ProductDetailsRoute(int.parse(state.pathParameters['id']!));
+
+ ProductDetailsRoute get _self => this as ProductDetailsRoute;
+
+ @override
+ String get location => GoRouteData.$location(
+ '/products/${Uri.encodeComponent(_self.id.toString())}',
+ );
+
+ @override
+ void go(BuildContext context) => context.go(location);
+
+ @override
+ Future<T?> push<T>(BuildContext context) => context.push<T>(location);
+
+ @override
+ void pushReplacement(BuildContext context) =>
+ context.pushReplacement(location);
+
+ @override
+ void replace(BuildContext context) => context.replace(location);
+}
diff --git a/concierge/lib/concierge_route.dart b/concierge/lib/concierge_route.dart
new file mode 100644
index 00000000..b32e7bc4
--- /dev/null
+++ b/concierge/lib/concierge_route.dart
@@ -0,0 +1,72 @@
+
+import 'package:concierge/presentation/app/cart_cubit.dart';
+import 'package:concierge/presentation/screens/hotel_overview_page/hotel_overview_page_route.dart';
+import 'package:concierge/presentation/screens/product_details/product_details_route.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:go_router/go_router.dart';
+import 'package:pretty_dio_logger/pretty_dio_logger.dart';
+
+import 'data/local/secure_storage/concierge_secure_storage.dart';
+import 'data/remote/api/concierge_interceptor.dart';
+import 'data/remote/api/concierge_service.dart';
+import 'domain/repositories/property_repository.dart';
+import 'flavors.dart';
+
+final conciergeShellRoute = StatefulShellRoute.indexedStack(
+ branches: [
+ StatefulShellBranch(
+ routes: [
+ $hotelOverviewPageRoute,
+ $productDetailsRoute,
+ ],
+ ),
+ ],
+ builder: (context, state, child) {
+ Future<(ConciergeService, ConciergeSecureStorage)> getDependencies() async {
+ final flavorName = state.uri.queryParameters["flavor"].toString();
+ final authToken = state.uri.queryParameters["auth-token"].toString();
+ final storage = ConciergeSecureStorage(FlutterSecureStorage());
+ if (state.uri.queryParameters.isNotEmpty) {
+ print("qqq init ${state.uri.queryParameters}");
+ await storage.setUserToken(authToken);
+ F.appFlavor = Flavor.values.firstWhere((flavor) => flavor.name == flavorName);
+ }
+ final dio = Dio(BaseOptions(baseUrl: F.baseUrl));
+ dio.interceptors.addAll([
+ ConciergeInterceptor(storage),
+ if (kDebugMode) PrettyDioLogger(requestBody: true, requestHeader: true),
+ ]);
+ await storage.setUserToken("authToken");
+ return (ConciergeService(dio), storage);
+ }
+
+ return FutureBuilder(
+ future: getDependencies(),
+ builder: (context, asyncSnapshot) {
+ if (!asyncSnapshot.hasData) {
+ return Center(child: CircularProgressIndicator());
+ }
+
+ if (asyncSnapshot.hasError) {
+ return Center(child: Text(asyncSnapshot.error.toString()));
+ }
+
+ final (service, storage) = asyncSnapshot.data!;
+ return MultiRepositoryProvider(
+ providers: [
+ RepositoryProvider(create: (context) => PropertyRepository(service)),
+ RepositoryProvider(create: (context) => storage),
+ ],
+ child: MultiBlocProvider(
+ providers: [BlocProvider(create: (context) => CartCubit())],
+ child: child,
+ ),
+ );
+ },
+ );
+ },
+);
\ No newline at end of file
diff --git a/concierge/lib/presentation/app/cart_cubit.dart b/concierge/lib/presentation/app/cart_cubit.dart
new file mode 100644
index 00000000..42e7e290
--- /dev/null
+++ b/concierge/lib/presentation/app/cart_cubit.dart
@@ -0,0 +1,38 @@
+import 'package:concierge/presentation/base/base_cubit.dart';
+import 'package:concierge/data/remote/models/product.dart';
+import 'package:concierge/domain/models/app_error.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part '../../_generated/presentation/app/cart_cubit.freezed.dart';
+
+class CartCubit extends BaseCubit<CartState> {
+ CartCubit() : super(const CartState());
+
+ void addToCart(Product product) {
+ final copy = List.of(state.productsInCart);
+ copy.add(product);
+ safeEmit(state.copyWith(productsInCart: copy));
+ }
+
+
+ void removeProduct(Product product) {
+ final copy = List.of(state.productsInCart);
+ copy.remove(product);
+ safeEmit(state.copyWith(productsInCart: copy));
+ }
+
+ int getQuantityForProduct(int productId) {
+ return state.productsInCart.map((p) => p.id == productId).length;
+ }
+}
+
+@freezed
+abstract class CartState with _$CartState {
+ const factory CartState({
+ @Default(false) bool isLoading,
+ @Default(AppError.none) AppError error,
+ @Default([]) List<Product> productsInCart,
+ }) = _CartState;
+
+ const CartState._();
+}
diff --git a/concierge/lib/presentation/navigation/app_routes.dart b/concierge/lib/presentation/navigation/app_routes.dart
index 113dfe69..01d7a5af 100644
--- a/concierge/lib/presentation/navigation/app_routes.dart
+++ b/concierge/lib/presentation/navigation/app_routes.dart
@@ -1,3 +1,4 @@
abstract class AppRoutes {
static const hotelOverviewPage = "/hotel-overview-page";
+ static const productDetails = "/products/:id";
}
diff --git a/concierge/lib/presentation/navigation/router.dart b/concierge/lib/presentation/navigation/router.dart
index bcd26ffa..44ca0cb4 100644
--- a/concierge/lib/presentation/navigation/router.dart
+++ b/concierge/lib/presentation/navigation/router.dart
@@ -1,4 +1,5 @@
import 'package:concierge/presentation/navigation/app_routes.dart';
+import 'package:concierge/presentation/screens/product_details/product_details_route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@@ -22,5 +23,6 @@ GoRouter router = GoRouter(
},
routes: [
$hotelOverviewPageRoute,
+ $productDetailsRoute,
],
);
diff --git a/concierge/lib/presentation/screens/hotel_overview_page/bloc/hotel_overview_page_cubit.dart b/concierge/lib/presentation/screens/hotel_overview_page/bloc/hotel_overview_page_cubit.dart
index 8a75f5b4..af7acc54 100644
--- a/concierge/lib/presentation/screens/hotel_overview_page/bloc/hotel_overview_page_cubit.dart
+++ b/concierge/lib/presentation/screens/hotel_overview_page/bloc/hotel_overview_page_cubit.dart
@@ -15,6 +15,7 @@ class HotelOverviewPageCubit extends BaseCubit<HotelOverviewPageState> {
}
Future<void> init() async {
+ print("qqq init $runtimeType");
try {
safeEmit(state.copyWith(isLoading: true, errorMessage: ""));
final property = await _propertyRepository.getHotelOverview(hotelCode: hotelCode);
diff --git a/concierge/lib/presentation/screens/hotel_overview_page/hotel_overview_page_route.dart b/concierge/lib/presentation/screens/hotel_overview_page/hotel_overview_page_route.dart
index e48c65a6..1c04a124 100644
--- a/concierge/lib/presentation/screens/hotel_overview_page/hotel_overview_page_route.dart
+++ b/concierge/lib/presentation/screens/hotel_overview_page/hotel_overview_page_route.dart
@@ -1,5 +1,7 @@
+import 'package:concierge/presentation/screens/hotel_overview_page/bloc/hotel_overview_page_cubit.dart';
import 'package:flutter/material.dart';
import 'package:concierge/presentation/navigation/app_routes.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:concierge/presentation/navigation/transitions/slide_up_transition.dart';
import 'package:concierge/presentation/screens/hotel_overview_page/hotel_overview_page_screen.dart';
@@ -8,13 +10,18 @@ part '../../../_generated/presentation/screens/hotel_overview_page/hotel_overvie
@TypedGoRoute<HotelOverviewPageRoute>(path: AppRoutes.hotelOverviewPage)
class HotelOverviewPageRoute extends GoRouteData with $HotelOverviewPageRoute {
+ final String hotelCode;
+ final String authToken;
+ final String flavor;
+
+ HotelOverviewPageRoute({required this.hotelCode, required this.authToken, required this.flavor});
+
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return slideUpTransition(
state: state,
- child: PopScope(
- canPop: context.canPop(),
- onPopInvokedWithResult: (didPop, result) {},
+ child: BlocProvider(
+ create: (context) => HotelOverviewPageCubit(context.read(), hotelCode: hotelCode),
child: HotelOverviewPageScreen(),
),
);
diff --git a/concierge/lib/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart b/concierge/lib/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart
index ace8720c..4aecca20 100644
--- a/concierge/lib/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart
+++ b/concierge/lib/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart
@@ -1,4 +1,5 @@
import 'package:concierge/data/remote/models/product.dart';
+import 'package:concierge/presentation/screens/product_details/product_details_route.dart';
import 'package:concierge/presentation/widgets/padded_column.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
@@ -10,47 +11,59 @@ class ProductListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Container(
- width: MediaQuery.of(context).size.width * 0.8,
- decoration: BoxDecoration(
- border: Border.all(color: Colors.grey, width: 1),
- borderRadius: BorderRadius.all(Radius.circular(16)),
- ),
- clipBehavior: Clip.antiAlias,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (product.images.isNotEmpty)
- Image.network(
- product.images.first.url,
- fit: BoxFit.fitWidth,
- height: 180,
- width: double.infinity,
- )
- else
- SizedBox(height: 180),
- PaddedColumn(
- padding: EdgeInsets.all(16),
- children: [
- Row(
- children: [
- Icon(Icons.watch_later_outlined),
- Text(product.estimatedDeliveryTime),
- ],
- ),
- Gap(16),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(product.title),
- Text(product.price.toString()),
- ],
- ),
- Gap(8),
- Text(product.subTitle),
- ],
+ return Material(
+ child: Ink(
+ width: MediaQuery.of(context).size.width * 0.8,
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey, width: 1),
+ borderRadius: BorderRadius.all(Radius.circular(16)),
+ ),
+ child: InkWell(
+ borderRadius: BorderRadius.all(Radius.circular(16)),
+
+ onTap: () {
+ ProductDetailsRoute(product.id).push(context);
+ },
+ child: ClipRect(
+ clipBehavior: Clip.antiAlias,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (product.images.isNotEmpty)
+ Image.network(
+
+ product.images.first.url,
+ fit: BoxFit.fitWidth,
+ height: 180,
+ width: double.infinity,
+ )
+ else
+ SizedBox(height: 180),
+ PaddedColumn(
+ padding: EdgeInsets.all(16),
+ children: [
+ Row(
+ children: [
+ Icon(Icons.watch_later_outlined),
+ Text(product.estimatedDeliveryTime),
+ ],
+ ),
+ Gap(16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(product.title),
+ Text(product.price.toString()),
+ ],
+ ),
+ Gap(8),
+ Text(product.subTitle),
+ ],
+ ),
+ ],
+ ),
),
- ],
+ ),
),
);
}
diff --git a/concierge/lib/presentation/screens/product_details/bloc/product_details_cubit.dart b/concierge/lib/presentation/screens/product_details/bloc/product_details_cubit.dart
new file mode 100644
index 00000000..17662c66
--- /dev/null
+++ b/concierge/lib/presentation/screens/product_details/bloc/product_details_cubit.dart
@@ -0,0 +1,30 @@
+import 'package:concierge/domain/models/app_error.dart';
+import 'package:concierge/domain/repositories/property_repository.dart';
+import 'package:concierge/presentation/base/base_cubit.dart';
+import 'package:concierge/presentation/screens/product_details/bloc/product_details_state.dart';
+
+class ProductDetailsCubit extends BaseCubit<ProductDetailsState> {
+ final PropertyRepository _propertyRepository;
+
+ ProductDetailsCubit(
+ this._propertyRepository, {
+ required this.productId,
+ }) : super(const ProductDetailsState()) {
+ init();
+ }
+
+ final int productId;
+
+ Future<void> init() async {
+ try {
+ safeEmit(state.copyWith(isLoading: true));
+ final product = await _propertyRepository.getProduct(productId);
+ safeEmit(state.copyWith(product: product));
+ } catch (e, st) {
+ handleError(e, st);
+ safeEmit(state.copyWith(error: AppError.unknown(e.toString())));
+ } finally {
+ safeEmit(state.copyWith(isLoading: false));
+ }
+ }
+}
diff --git a/concierge/lib/presentation/screens/product_details/bloc/product_details_state.dart b/concierge/lib/presentation/screens/product_details/bloc/product_details_state.dart
new file mode 100644
index 00000000..a604f70e
--- /dev/null
+++ b/concierge/lib/presentation/screens/product_details/bloc/product_details_state.dart
@@ -0,0 +1,18 @@
+import 'package:concierge/data/remote/models/product.dart';
+import 'package:concierge/domain/models/app_error.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part '../../../../_generated/presentation/screens/product_details/bloc/product_details_state.freezed.dart';
+
+@freezed
+abstract class ProductDetailsState with _$ProductDetailsState {
+ const factory ProductDetailsState({
+ @Default(false) bool isLoading,
+ @Default(AppError.none) AppError error,
+ Product? product,
+ }) = _ProductDetailsState;
+
+ const ProductDetailsState._();
+
+ Product get requireProduct => product!;
+}
\ No newline at end of file
diff --git a/concierge/lib/presentation/screens/product_details/product_details_route.dart b/concierge/lib/presentation/screens/product_details/product_details_route.dart
new file mode 100644
index 00000000..121552a0
--- /dev/null
+++ b/concierge/lib/presentation/screens/product_details/product_details_route.dart
@@ -0,0 +1,27 @@
+import 'package:concierge/presentation/navigation/transitions/slide_in_transition.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:concierge/presentation/navigation/app_routes.dart';
+import 'package:go_router/go_router.dart';
+import 'package:concierge/presentation/screens/product_details/bloc/product_details_cubit.dart';
+import 'package:concierge/presentation/screens/product_details/product_details_screen.dart';
+
+part '../../../_generated/presentation/screens/product_details/product_details_route.g.dart';
+
+@TypedGoRoute<ProductDetailsRoute>(path: AppRoutes.productDetails)
+class ProductDetailsRoute extends GoRouteData with $ProductDetailsRoute {
+ final int id;
+
+ const ProductDetailsRoute(this.id);
+
+ @override
+ Page<void> buildPage(BuildContext context, GoRouterState state) {
+ return slideInTransition(
+ state: state,
+ child: BlocProvider(
+ create: (context) => ProductDetailsCubit(context.read(), productId: id),
+ child: ProductDetailsScreen(),
+ ),
+ );
+ }
+}
diff --git a/concierge/lib/presentation/screens/product_details/product_details_screen.dart b/concierge/lib/presentation/screens/product_details/product_details_screen.dart
new file mode 100644
index 00000000..90f95db4
--- /dev/null
+++ b/concierge/lib/presentation/screens/product_details/product_details_screen.dart
@@ -0,0 +1,67 @@
+import 'package:concierge/presentation/app/cart_cubit.dart';
+import 'package:concierge/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:concierge/presentation/screens/product_details/bloc/product_details_cubit.dart';
+import 'package:concierge/presentation/screens/product_details/bloc/product_details_state.dart';
+
+class ProductDetailsScreen extends StatelessWidget {
+ const ProductDetailsScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final cartCubit = context.watch<CartCubit>();
+ final cubit = context.read<ProductDetailsCubit>();
+ return BlocBuilder<ProductDetailsCubit, ProductDetailsState>(
+ builder: (context, state) {
+ return MultiBlocListener(
+ listeners: [
+ BlocListener<ProductDetailsCubit, ProductDetailsState>(
+ listenWhen: (prev, curr) => prev.isLoading && curr.error.isError,
+ listener: (context, state) {},
+ ),
+ ],
+ child: Scaffold(
+ appBar: AppBar(
+ backgroundColor: Colors.white,
+ ),
+ backgroundColor: Colors.white,
+ body: Builder(
+ builder: (context) {
+ if (state.isLoading) {
+ return Center(child: CircularProgressIndicator());
+ }
+ if (state.error.isError) {
+ return Center(child: Text(state.error.toString()));
+ }
+ return Column(
+ children: [
+ ProductListTile(product: state.requireProduct),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ IconButton(
+ onPressed: () {
+ cartCubit.removeProduct(state.requireProduct);
+ },
+ icon: Icon(Icons.remove),
+ ),
+ Text(cartCubit.getQuantityForProduct(cubit.productId).toString()),
+ IconButton(
+ onPressed: () {
+ cartCubit.addToCart(state.requireProduct);
+ },
+ icon: Icon(Icons.add),
+ ),
+ ],
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/concierge/pubspec.yaml b/concierge/pubspec.yaml
index 57bd382b..cddfd12d 100644
--- a/concierge/pubspec.yaml
+++ b/concierge/pubspec.yaml
@@ -28,7 +28,7 @@ dependencies:
go_router_builder: ^4.1.0
flutter_svg: ^2.2.1
flutter_secure_storage: ^9.2.4
- fpdart: ^1.1.1
+ fpdart: ^1.2.0
dev_dependencies:
flutter_test: