6177214e-ce7c-49e3-99de-ff9721b26f63 — Commit 8f43057d

AuthorEdmir Suljic<esu@dwarf.dk>
Date2025-04-08 15:35:12 +0200
Resolved PR comments.

Changed files

comwell_key_app/assets/translations/da-DK.json     |   2 +-
 .../lib/booking_details/components/guest_list.dart |  57 +++----
 .../booking_details/components/share_button.dart   | 190 +++++++++++----------
 comwell_key_app/lib/overview/models/booking.dart   |  28 ++-
 .../lib/share/cubit/share_booking_cubit.dart       |  30 ++++
 .../lib/share/cubit/share_booking_state.dart       |  22 +++
 comwell_key_app/lib/share/share_booking_page.dart  |  12 +-
 comwell_key_app/lib/utils/secure_storage.dart      |   7 +-
 comwell_key_app/lib/utils/share_button_utils.dart  |   9 +
 9 files changed, 211 insertions(+), 146 deletions(-)

Diff

diff --git a/comwell_key_app/assets/translations/da-DK.json b/comwell_key_app/assets/translations/da-DK.json
index 297f1dcf..f091e497 100644
--- a/comwell_key_app/assets/translations/da-DK.json
+++ b/comwell_key_app/assets/translations/da-DK.json
@@ -173,7 +173,7 @@
"checkout_page_payment_dialog_confirm": "Ja, check ud nu",
"checkout_page_payment_dialog_cancel": "Nej",
"checkout_page_processing_success_title": "Check-out bekræftet",
- "checkout_page_processing_success_subtitle": "Det check-out er nu bekræftet og du har nu 30 minutter til at forlade dit værelse. Herefter vil du ikke længere kunne bruge dit nøglekort.",
+ "checkout_page_processing_success_subtitle": "Dit check-out er nu bekræftet og du har nu 30 minutter til at forlade dit værelse. Herefter vil du ikke længere kunne bruge dit nøglekort.",
"payment_cards_title": "Betalingskort",
"payment_cards_my_cards": "Mine kort",
"payment_cards_edit_card_title": "Redigér kort",
diff --git a/comwell_key_app/lib/booking_details/components/guest_list.dart b/comwell_key_app/lib/booking_details/components/guest_list.dart
index 2caa57cc..565b0582 100644
--- a/comwell_key_app/lib/booking_details/components/guest_list.dart
+++ b/comwell_key_app/lib/booking_details/components/guest_list.dart
@@ -1,30 +1,20 @@
import 'package:flutter/material.dart';
-class GuestList extends StatefulWidget {
+class GuestList extends StatelessWidget {
final List<String> guests;
+ final List<String> selectedGuests;
final Function(List<String>) onGuestSelected;
- final String? selectedGuest;
const GuestList({
super.key,
required this.guests,
+ required this.selectedGuests,
required this.onGuestSelected,
- this.selectedGuest,
});
- @override
- State<GuestList> createState() => _GuestListState();
-}
-
-class _GuestListState extends State<GuestList> {
- List<String> selectedGuests = [];
-
- @override
- void initState() {
- super.initState();
- if (widget.selectedGuest != null) {
- selectedGuests = [widget.selectedGuest!];
- }
+ void handleCancelSharing(BuildContext context) {
+ onGuestSelected([]);
+ Navigator.pop(context);
}
@override
@@ -32,9 +22,9 @@ class _GuestListState extends State<GuestList> {
return SingleChildScrollView(
child: ListView.builder(
shrinkWrap: true,
- itemCount: widget.guests.length,
+ itemCount: guests.length,
itemBuilder: (context, index) {
- final guest = widget.guests[index];
+ final guest = guests[index];
final initials = guest.split(' ').map((name) => name[0]).join('');
return Padding(
@@ -42,14 +32,13 @@ class _GuestListState extends State<GuestList> {
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: GestureDetector(
onTap: () {
- setState(() {
- if (selectedGuests.contains(guest)) {
- selectedGuests.remove(guest);
- } else {
- selectedGuests.add(guest);
- }
- });
- widget.onGuestSelected(selectedGuests);
+ final newSelectedGuests = List<String>.from(selectedGuests);
+ if (selectedGuests.contains(guest)) {
+ newSelectedGuests.remove(guest);
+ } else {
+ newSelectedGuests.add(guest);
+ }
+ onGuestSelected(newSelectedGuests);
},
child: Container(
decoration: BoxDecoration(
@@ -91,14 +80,14 @@ class _GuestListState extends State<GuestList> {
value: selectedGuests.contains(guest),
onChanged: (bool? value) {
if (value != null) {
- setState(() {
- if (value) {
- selectedGuests.add(guest);
- } else {
- selectedGuests.remove(guest);
- }
- });
- widget.onGuestSelected(selectedGuests);
+ final newSelectedGuests =
+ List<String>.from(selectedGuests);
+ if (value) {
+ newSelectedGuests.add(guest);
+ } else {
+ newSelectedGuests.remove(guest);
+ }
+ onGuestSelected(newSelectedGuests);
}
},
),
diff --git a/comwell_key_app/lib/booking_details/components/share_button.dart b/comwell_key_app/lib/booking_details/components/share_button.dart
index 6bb3308b..1be4fe65 100644
--- a/comwell_key_app/lib/booking_details/components/share_button.dart
+++ b/comwell_key_app/lib/booking_details/components/share_button.dart
@@ -4,6 +4,8 @@ import 'package:comwell_key_app/overview/models/guest.dart';
import 'package:comwell_key_app/routing/app_routes.dart';
import 'package:comwell_key_app/booking_details/components/guest_list.dart';
import 'package:comwell_key_app/themes/light_theme.dart';
+import 'package:comwell_key_app/utils/share_button_utils.dart';
+import 'package:comwell_key_app/share/cubit/share_booking_cubit.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -15,21 +17,13 @@ class ShareButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final booking = context.read<BookingDetailsBloc>().booking;
final bloc = context.read<BookingDetailsBloc>();
+ final booking = bloc.booking;
const userButtonWidth = 50.0;
- const userButtonOverlap =
- 15.0; // How much each button overlaps the previous one
- final numberOfUsers = 1 + (guests.length); // Add 1 for the booker
- final bookerInitials =
- booking.booker.split(' ').map((name) => name[0]).join('');
- final guestInitials = guests
- .map((guest) => guest.name.split(' ').map((name) => name[0]).join(''))
- .toList();
-
- // Combine initials with booker first
- final allInitials = [bookerInitials, ...guestInitials];
+ const userButtonOverlap = 15.0;
+ final numberOfUsers = guests.length;
+ final allInitials = generateInitials(guests);
return Stack(
key: Key(guests.length.toString()),
@@ -75,8 +69,8 @@ class ShareButton extends StatelessWidget {
height: userButtonWidth,
child: ElevatedButton(
onPressed: () async {
- final results =
- await _showGuestList(context, index, guests);
+ final results = await _showGuestList(
+ context, index, guests, booking.booker);
if (results is List<String>) {
final updatedBooking =
@@ -113,98 +107,106 @@ class ShareButton extends StatelessWidget {
);
}
- Future<dynamic> _showGuestList(
- BuildContext context, int index, List<Guest> guests) async {
+ Future<dynamic> _showGuestList(BuildContext context, int index,
+ List<Guest> guests, String booker) async {
final theme = Theme.of(context);
- List<String> selectedGuests = [];
return showModalBottomSheet<dynamic>(
context: context,
backgroundColor: Colors.white,
builder: (BuildContext bottomSheetContext) {
- return StatefulBuilder(
- builder: (BuildContext statefulContext, StateSetter setState) {
- return SizedBox(
- height: 450,
- width: MediaQuery.of(context).size.width,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.start,
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: <Widget>[
- Padding(
- padding: const EdgeInsets.all(16.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text('handle_guests_title'.tr(),
- style: theme.textTheme.titleLarge?.copyWith(
- color: Colors.black,
- fontWeight: FontWeight.w600,
- )),
- ElevatedButton(
- style: ElevatedButton.styleFrom(
- backgroundColor: Colors.grey[200],
- shape: const CircleBorder(),
- elevation: 0),
- child: const Icon(Icons.close, color: Colors.black),
- onPressed: () => Navigator.pop(bottomSheetContext),
- ),
- ],
- ),
- ),
- const SizedBox(height: 16),
- Expanded(
- child: GuestList(
- guests: guests.map((guest) => guest.name).toList(),
- onGuestSelected: (guests) {
- setState(() {
- selectedGuests = guests;
- });
- },
- selectedGuest: null,
+ return BlocProvider(
+ create: (context) => ShareBookingCubit(),
+ child: BlocConsumer<ShareBookingCubit, ShareBookingState>(
+ listener: (context, state) {},
+ builder: (context, state) {
+ final cubit = context.read<ShareBookingCubit>();
+ return SizedBox(
+ height: 450,
+ width: MediaQuery.of(context).size.width,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: <Widget>[
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('handle_guests_title'.tr(),
+ style: theme.textTheme.titleLarge?.copyWith(
+ color: Colors.black,
+ fontWeight: FontWeight.w600,
+ )),
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.grey[200],
+ shape: const CircleBorder(),
+ elevation: 0),
+ child: const Icon(Icons.close, color: Colors.black),
+ onPressed: () {
+ cubit.clearSelection();
+ Navigator.pop(bottomSheetContext);
+ },
+ ),
+ ],
+ ),
),
- ),
- Column(
- children: [
- Divider(
- height: 1,
- color: Colors.grey[300],
+ const SizedBox(height: 16),
+ Expanded(
+ child: GuestList(
+ guests: guests
+ .where((guest) => guest.name != booker)
+ .map((guest) => guest.name)
+ .toList(),
+ selectedGuests: state.selectedGuests,
+ onGuestSelected: (List<String> newSelection) {
+ cubit.updateSelectedGuests(newSelection);
+ },
),
- Padding(
- padding: const EdgeInsets.all(16.0),
- child: SizedBox(
- width: double.infinity,
- child: ElevatedButton(
- onPressed: selectedGuests.isNotEmpty
- ? () {
- context.pop(selectedGuests);
- }
- : null,
- style: ElevatedButton.styleFrom(
- backgroundColor: selectedGuests.isNotEmpty
- ? Colors.black
- : Colors.grey[200],
- minimumSize: const Size.fromHeight(50),
- elevation: 0,
- ),
- child: Text(
- 'cancel_sharing'.tr(),
- style: TextStyle(
- color: selectedGuests.isNotEmpty
- ? Colors.white
- : Colors.grey[500],
+ ),
+ Column(
+ children: [
+ Divider(
+ height: 1,
+ color: Colors.grey.shade300,
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: state.selectedGuests.isNotEmpty
+ ? () {
+ context.pop(state.selectedGuests);
+ }
+ : null,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: state.selectedGuests.isNotEmpty
+ ? Colors.black
+ : const Color(0xffE0E0E0),
+ minimumSize: const Size.fromHeight(50),
+ elevation: 0,
+ ),
+ child: Text(
+ 'cancel_sharing'.tr(),
+ style: TextStyle(
+ color: state.selectedGuests.isNotEmpty
+ ? Colors.white
+ : Colors.grey[500],
+ ),
),
),
),
),
- ),
- ],
- ),
- ],
- ),
- );
- },
+ ],
+ ),
+ ],
+ ),
+ );
+ },
+ ),
);
},
);
diff --git a/comwell_key_app/lib/overview/models/booking.dart b/comwell_key_app/lib/overview/models/booking.dart
index c3cde66b..16d29397 100644
--- a/comwell_key_app/lib/overview/models/booking.dart
+++ b/comwell_key_app/lib/overview/models/booking.dart
@@ -24,10 +24,9 @@ class Booking extends Equatable {
final DateTime bookingDate;
final PaymentDetails paymentDetails;
final String confirmationId;
- // TODO: Should this be here or is it a separate endpoint?
- final List<Guest>? guests;
+ final List<Guest> guests;
- const Booking({
+ Booking({
required this.id,
required this.confirmationId,
required this.userId,
@@ -44,8 +43,20 @@ class Booking extends Equatable {
required this.booker,
required this.bookingDate,
required this.paymentDetails,
- required this.guests,
- });
+ List<Guest>? guests,
+ }) : guests = _ensureBookerInGuestList(booker, userId, guests ?? []);
+
+ static List<Guest> _ensureBookerInGuestList(
+ String booker, String userId, List<Guest> guests) {
+ final bookerExists = guests.any((guest) => guest.name == booker);
+ if (!bookerExists) {
+ return [
+ Guest(name: booker, id: userId),
+ ...guests,
+ ];
+ }
+ return guests;
+ }
factory Booking.fromJson(Json json) => _$BookingFromJson(json);
@@ -112,10 +123,11 @@ class Booking extends Equatable {
}
Booking updateGuests(List<String> guestNames) {
- if (guests == null) return this;
+ final updatedGuests = guests
+ .where(
+ (guest) => guest.name == booker || !guestNames.contains(guest.name))
+ .toList();
- final updatedGuests =
- guests!.where((guest) => !guestNames.contains(guest.name)).toList();
return copyWith(guests: updatedGuests);
}
}
diff --git a/comwell_key_app/lib/share/cubit/share_booking_cubit.dart b/comwell_key_app/lib/share/cubit/share_booking_cubit.dart
new file mode 100644
index 00000000..8346648f
--- /dev/null
+++ b/comwell_key_app/lib/share/cubit/share_booking_cubit.dart
@@ -0,0 +1,30 @@
+import 'package:bloc/bloc.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:equatable/equatable.dart';
+import 'package:comwell_key_app/overview/models/booking.dart';
+import 'package:share_plus/share_plus.dart';
+
+part 'share_booking_state.dart';
+
+class ShareBookingCubit extends Cubit<ShareBookingState> {
+ ShareBookingCubit() : super(const ShareBookingState.initial());
+
+ void updateSelectedGuests(List<String> guests) {
+ emit(state.updateSelectedGuests(guests));
+ }
+
+ Future<void> shareBooking(Booking booking) async {
+ // TODO: Implement actual sharing logic here
+ Share.share(
+ 'Check out my booking at ${booking.hotelName}!\n\n'
+ 'Dates: ${DateFormat('d. MMM').format(booking.startDate)} - ${DateFormat('d. MMM').format(booking.endDate)}\n'
+ 'Guests: ${booking.adults} ${booking.adults > 1 ? 'adults'.tr() : 'adult'.tr()}${booking.children > 0 ? ' | ${booking.children} ${booking.children > 1 ? 'children'.tr() : 'child'.tr()}' : ''}\n\n'
+ 'View booking: https://comwell.app/booking/${booking.id}',
+ subject: 'Comwell Booking',
+ );
+ }
+
+ void clearSelection() {
+ emit(const ShareBookingState.initial());
+ }
+}
diff --git a/comwell_key_app/lib/share/cubit/share_booking_state.dart b/comwell_key_app/lib/share/cubit/share_booking_state.dart
new file mode 100644
index 00000000..86052ef3
--- /dev/null
+++ b/comwell_key_app/lib/share/cubit/share_booking_state.dart
@@ -0,0 +1,22 @@
+part of 'share_booking_cubit.dart';
+
+class ShareBookingState extends Equatable {
+ final List<String> selectedGuests;
+
+ const ShareBookingState._({
+ required this.selectedGuests,
+ });
+
+ const ShareBookingState.initial() : this._(selectedGuests: const []);
+
+ ShareBookingState updateSelectedGuests(List<String> guests) =>
+ _copyWith(selectedGuests: guests);
+
+ ShareBookingState _copyWith({List<String>? selectedGuests}) {
+ return ShareBookingState._(
+ selectedGuests: selectedGuests ?? this.selectedGuests);
+ }
+
+ @override
+ List<Object?> get props => [selectedGuests];
+}
diff --git a/comwell_key_app/lib/share/share_booking_page.dart b/comwell_key_app/lib/share/share_booking_page.dart
index cc2d78be..3c748838 100644
--- a/comwell_key_app/lib/share/share_booking_page.dart
+++ b/comwell_key_app/lib/share/share_booking_page.dart
@@ -2,7 +2,8 @@ import 'package:comwell_key_app/overview/models/booking.dart';
import 'package:comwell_key_app/themes/light_theme.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
-import 'package:share_plus/share_plus.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:comwell_key_app/share/cubit/share_booking_cubit.dart';
class ShareBookingPage extends StatelessWidget {
final Booking booking;
@@ -11,6 +12,7 @@ class ShareBookingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
+ final cubit = context.read<ShareBookingCubit>();
return Scaffold(
backgroundColor: sandColor,
body: Center(
@@ -107,13 +109,7 @@ class ShareBookingPage extends StatelessWidget {
width: double.infinity,
child: ElevatedButton(
onPressed: () {
- Share.share(
- 'Check out my booking at ${booking.hotelName}!\n\n'
- 'Dates: ${DateFormat('d. MMM').format(booking.startDate)} - ${DateFormat('d. MMM').format(booking.endDate)}\n'
- 'Guests: ${booking.adults} ${booking.adults > 1 ? 'adults'.tr() : 'adult'.tr()}${booking.children > 0 ? ' | ${booking.children} ${booking.children > 1 ? 'children'.tr() : 'child'.tr()}' : ''}\n\n'
- 'View booking: https://comwell.app/booking/${booking.id}',
- subject: 'Comwell Booking',
- );
+ cubit.shareBooking(booking);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
diff --git a/comwell_key_app/lib/utils/secure_storage.dart b/comwell_key_app/lib/utils/secure_storage.dart
index 2091d759..08e678e6 100644
--- a/comwell_key_app/lib/utils/secure_storage.dart
+++ b/comwell_key_app/lib/utils/secure_storage.dart
@@ -1,7 +1,12 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorage {
- final FlutterSecureStorage _storage = const FlutterSecureStorage();
+ final FlutterSecureStorage _storage = const FlutterSecureStorage(
+ iOptions: IOSOptions(
+ accessibility: KeychainAccessibility.first_unlock,
+ synchronizable: true,
+ ),
+ );
Future<String?> read(String key) async {
return await _storage.read(key: key);
diff --git a/comwell_key_app/lib/utils/share_button_utils.dart b/comwell_key_app/lib/utils/share_button_utils.dart
new file mode 100644
index 00000000..8cf35682
--- /dev/null
+++ b/comwell_key_app/lib/utils/share_button_utils.dart
@@ -0,0 +1,9 @@
+import 'package:comwell_key_app/overview/models/guest.dart';
+
+List<String> generateInitials(List<Guest> guests) {
+ final guestInitials = guests
+ .map((guest) => guest.name.split(' ').map((name) => name[0]).join(''))
+ .toList();
+
+ return [...guestInitials];
+}