From 177f891a31f46c7b52786dfd613c3ed2c5b9e2a3 Mon Sep 17 00:00:00 2001 From: "dinesh.patil" Date: Tue, 17 Mar 2026 17:07:14 +0530 Subject: [PATCH] added check in and Qr scaner and bookng with api and more fixes and changes --- lib/add_details/add_details_view.dart | 141 ++--- lib/buy_a_pass/view/buy_pass_view.dart | 2 +- .../bloc/pass_purchase_details_bloc.dart | 1 + .../bloc/pass_purchase_details_event.dart | 2 + .../pass_purchase_details_repository.dart | 58 +- lib/common_packages/custom_textfield.dart | 209 ++++--- lib/core/app_router.dart | 2 + lib/core/inside_bottom_navigator.dart | 39 +- .../bloc/create_account_bloc.dart | 1 + .../bloc/create_account_event.dart | 3 + .../repository/create_account_repository.dart | 2 + .../view/create_account_view.dart | 332 ++++++----- lib/esim_offer/esim_offer_view.dart | 6 +- lib/home/views/first_time_user_home_page.dart | 6 +- lib/home/widgets/get_your_pass_card.dart | 2 +- lib/home/widgets/pass_card_list.dart | 4 +- lib/hotel_offer/hotel_offer_view.dart | 2 +- .../views/itinerary_creation_start_view.dart | 2 +- .../itinerary_completion_view.dart | 16 +- lib/my_pass/blocs/checkIn/check_in_bloc.dart | 40 ++ lib/my_pass/blocs/checkIn/check_in_event.dart | 27 + lib/my_pass/blocs/checkIn/check_in_state.dart | 38 ++ lib/my_pass/blocs/make_booking_bloc.dart | 71 ++- lib/my_pass/blocs/make_booking_events.dart | 27 +- lib/my_pass/blocs/make_booking_state.dart | 36 +- .../pass_attraction_details_bloc.dart | 45 ++ .../pass_attraction_details_event.dart | 15 + .../pass_attraction_details_state.dart | 19 + .../models/pass_attraction_details_model.dart | 282 +++++++++ .../repository/check_in_repository.dart | 15 + .../repository/make_booking_repository.dart | 27 + .../pass_attraction_details_repository.dart | 18 + lib/my_pass/views/booking_page_view.dart | 530 +++++++++++------ .../views/booking_successful_page_view.dart | 7 +- .../views/pass_attraction_details_view.dart | 345 ++++++++--- .../views/pass_attractions_page_view.dart | 4 +- lib/my_pass/views/pass_details_page_view.dart | 7 +- .../widgets/check_in_bottom_sheet.dart | 215 +++++++ .../widgets/how_to_redeem_bottomsheet.dart | 101 ++++ lib/my_pass/widgets/pass_attraction_card.dart | 8 +- lib/networkApiServices/api_urls.dart | 3 + .../network_api_services.dart | 4 +- .../blocs/postcard_creation_bloc.dart | 1 + .../blocs/postcard_creation_events.dart | 2 + .../blocs/postcard_creation_state.dart | 4 + .../views/postcard_creation_page_view.dart | 5 +- .../postcard_purchase_form_page_view.dart | 559 ++++++++++-------- .../widgets/edit_post_card/your_details.dart | 375 ++++++------ .../purchase_details_bottom_sheet.dart | 1 + .../bloc/contactUs/contact_us_bloc.dart | 1 + .../bloc/contactUs/contact_us_event.dart | 3 + lib/profile/bloc/profile/profile_event.dart | 4 + lib/profile/models/profile_model.dart | 10 +- .../repository/contact_us_repository.dart | 2 + .../repository/profile_repository.dart | 3 + .../view/contact_us/contact_us_view.dart | 94 ++- .../view/edit_profile/edit_profile_view.dart | 282 ++++----- .../models/your_itinerary_details_model.dart | 61 +- .../view/your_itinerary_view.dart | 6 +- .../widgets/summary_card_view.dart | 8 +- pubspec.lock | 72 +++ pubspec.yaml | 4 + 62 files changed, 2876 insertions(+), 1335 deletions(-) create mode 100644 lib/my_pass/blocs/checkIn/check_in_bloc.dart create mode 100644 lib/my_pass/blocs/checkIn/check_in_event.dart create mode 100644 lib/my_pass/blocs/checkIn/check_in_state.dart create mode 100644 lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_bloc.dart create mode 100644 lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_event.dart create mode 100644 lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_state.dart create mode 100644 lib/my_pass/models/pass_attraction_details_model.dart create mode 100644 lib/my_pass/repository/check_in_repository.dart create mode 100644 lib/my_pass/repository/make_booking_repository.dart create mode 100644 lib/my_pass/repository/pass_attraction_details_repository.dart create mode 100644 lib/my_pass/widgets/check_in_bottom_sheet.dart create mode 100644 lib/my_pass/widgets/how_to_redeem_bottomsheet.dart diff --git a/lib/add_details/add_details_view.dart b/lib/add_details/add_details_view.dart index 3979080..0555c5f 100644 --- a/lib/add_details/add_details_view.dart +++ b/lib/add_details/add_details_view.dart @@ -2,9 +2,11 @@ import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_textfield.dart'; +import 'package:country_code_picker/country_code_picker.dart'; // ✅ NEW IMPORT import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; // ✅ NEW IMPORT import '../checkout/bloc/pass_purchase_details_bloc.dart'; import '../checkout/bloc/pass_purchase_details_event.dart'; @@ -25,13 +27,16 @@ class _AddDetailsViewState extends State { final TextEditingController emailController = TextEditingController(); final TextEditingController phoneController = TextEditingController(); final TextEditingController cityController = TextEditingController(); - String? selectedCountry; + final TextEditingController countryController = TextEditingController(); + + String _selectedIsdCode = '+61'; // ✅ NEW: tracks selected country dial code @override void dispose() { firstNameController.dispose(); lastNameController.dispose(); emailController.dispose(); + countryController.dispose(); phoneController.dispose(); cityController.dispose(); super.dispose(); @@ -42,22 +47,26 @@ class _AddDetailsViewState extends State { return emailRegex.hasMatch(email); } + // ✅ UPDATED: now validates phone using phone_numbers_parser against the selected ISD code bool _isValidPhone(String phone) { - final phoneRegex = RegExp(r'^[0-9]{10}$'); - return phoneRegex.hasMatch(phone); + try { + final fullNumber = '$_selectedIsdCode$phone'; + final parsed = PhoneNumber.parse(fullNumber); + return parsed.isValid(); + } catch (_) { + return false; + } } void _handleSubmit(BuildContext context, bool isSubmitting) { - // If already submitting, do nothing if (isSubmitting) return; - // Validate inputs if (firstNameController.text.isEmpty || lastNameController.text.isEmpty || emailController.text.isEmpty || phoneController.text.isEmpty || cityController.text.isEmpty || - selectedCountry == null) { + countryController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please fill all fields'), @@ -67,7 +76,6 @@ class _AddDetailsViewState extends State { return; } - // Validate email if (!_isValidEmail(emailController.text)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -78,28 +86,28 @@ class _AddDetailsViewState extends State { return; } - // Validate phone number + // ✅ UPDATED: error message now shows the selected ISD code if (!_isValidPhone(phoneController.text)) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please enter a valid 10-digit phone number'), + SnackBar( + content: Text('Enter a valid phone number for $_selectedIsdCode'), backgroundColor: Colors.red, ), ); return; } - // Submit gift details context.read().add( SubmitUserDetailsEvent( bookingId: widget.bookingId, isForSelf: false, recipientFirstName: firstNameController.text, recipientLastName: lastNameController.text, + isdCode: _selectedIsdCode, recipientEmail: emailController.text, recipientPhone: phoneController.text, city: cityController.text, - country: selectedCountry!, + country: countryController.text, ), ); } @@ -110,21 +118,10 @@ class _AddDetailsViewState extends State { create: (_) => PurchaseDetailsBloc(), child: BlocConsumer( listener: (context, state) { - // Handle API submission success if (state is PurchaseDetailsSubmitted) { - // Show success message - // ScaffoldMessenger.of(context).showSnackBar( - // const SnackBar( - // content: Text('Gift details submitted successfully!'), - // backgroundColor: Color(0xffF95F62), - // ), - // ); - - // Navigate back Navigator.of(context).pop('success'); } - // Handle API submission error if (state is PurchaseDetailsError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -213,16 +210,44 @@ class _AddDetailsViewState extends State { keyboardType: TextInputType.emailAddress, ), ), + + // ✅ NEW: Phone field with CountryCodePicker (replaces plain CustomTextField) Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( label: "Phone Number *", - hint: "Enter recipient's phone number", + hint: "Enter phone number", controller: phoneController, - maxLength: 10, - keyboardType: TextInputType.number, + keyboardType: TextInputType.phone, + maxLength: 12, + numbersOnly: true, + prefixWidget: CountryCodePicker( + onChanged: (country) { + setState(() => _selectedIsdCode = country.dialCode!); + }, + initialSelection: 'AU', + favorite: const ['+61', '+1', '+44', '+91'], + showCountryOnly: false, + showOnlyCountryWhenClosed: false, + alignLeft: false, + flagWidth: 24.w, + padding: EdgeInsets.symmetric(horizontal: 8.w), + textStyle: TextStyle( + fontSize: 13.sp, + color: const Color(0xFF2D3134), + ), + dialogTextStyle: TextStyle(fontSize: 14.sp), + searchDecoration: const InputDecoration( + hintText: 'Search country...', + prefixIcon: Icon(Icons.search), + ), + ), ), ), + // ✅ END of new phone field + + SizedBox(height: 8.h), + Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( @@ -236,67 +261,19 @@ class _AddDetailsViewState extends State { ), Padding( - padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText(text: "Country *", size: 14.sp), - SizedBox(height: 6.h), - Container( - height: 42.h, - padding: EdgeInsets.symmetric(horizontal: 24.w), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: const Color(0xBBC83B61).withOpacity(0.4), - width: 0.4.w, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedCountry, - isExpanded: true, - icon: const Icon( - Icons.keyboard_arrow_down, - color: Color(0xFF8E8E8E), - ), - hint: Text( - "Select country", - style: TextStyle( - fontSize: 12.sp, - color: const Color(0xFF8E8E8E), - ), - ), - style: TextStyle( - fontSize: 14.sp, - color: const Color(0xFF2D3134), - ), - onChanged: (value) { - setState(() { - selectedCountry = value; - }); - }, - items: ["Australia"] - .map((value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: TextStyle(fontSize: 14.sp), - ), - ); - }).toList(), - ), - ), - ), - ], + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Country *", + hint: "Enter country name", + controller: countryController, + maxLength: 50, + onlyLetters: true, + isFirstLetterCapital: true, ), ), SizedBox(height: 24.h), - // Option 1: Pass empty function when disabled (doesn't change button appearance) CustomFilledButton( onTap: () => _handleSubmit(context, isSubmitting), label: isSubmitting ? "Submitting..." : "Continue", diff --git a/lib/buy_a_pass/view/buy_pass_view.dart b/lib/buy_a_pass/view/buy_pass_view.dart index 19368c2..602b271 100644 --- a/lib/buy_a_pass/view/buy_pass_view.dart +++ b/lib/buy_a_pass/view/buy_pass_view.dart @@ -116,7 +116,7 @@ class _BuyPassContentState extends State { children: [ const Icon(Icons.arrow_back), SizedBox(width: 8.w), - CustomText(text: "Buy a Pass", size: 12.sp), + CustomText(text: "Buy a Card", size: 12.sp), ], ), ), diff --git a/lib/checkout/bloc/pass_purchase_details_bloc.dart b/lib/checkout/bloc/pass_purchase_details_bloc.dart index 6e24bcf..cfa88de 100644 --- a/lib/checkout/bloc/pass_purchase_details_bloc.dart +++ b/lib/checkout/bloc/pass_purchase_details_bloc.dart @@ -80,6 +80,7 @@ class PurchaseDetailsBloc isForSelf: event.isForSelf, recipientFirstName: event.recipientFirstName, recipientLastName: event.recipientLastName, + isdCode: event.isdCode, recipientEmail: event.recipientEmail, recipientPhone: event.recipientPhone, city: event.city, diff --git a/lib/checkout/bloc/pass_purchase_details_event.dart b/lib/checkout/bloc/pass_purchase_details_event.dart index c85e1a9..ea4de02 100644 --- a/lib/checkout/bloc/pass_purchase_details_event.dart +++ b/lib/checkout/bloc/pass_purchase_details_event.dart @@ -19,6 +19,7 @@ class SubmitUserDetailsEvent extends PassPurchaseDetailsEvent { final bool isForSelf; final String? recipientFirstName; final String? recipientLastName; + final String? isdCode; final String? recipientEmail; final String? recipientPhone; final String? city; @@ -29,6 +30,7 @@ class SubmitUserDetailsEvent extends PassPurchaseDetailsEvent { required this.isForSelf, this.recipientFirstName, this.recipientLastName, + this.isdCode, this.recipientEmail, this.recipientPhone, this.city, diff --git a/lib/checkout/repository/pass_purchase_details_repository.dart b/lib/checkout/repository/pass_purchase_details_repository.dart index 7ee0d5c..7d9fbe5 100644 --- a/lib/checkout/repository/pass_purchase_details_repository.dart +++ b/lib/checkout/repository/pass_purchase_details_repository.dart @@ -1,6 +1,3 @@ -import 'dart:developer'; -import 'package:flutter/foundation.dart'; - import '../../networkApiServices/api_urls.dart'; import '../../networkApiServices/network_api_services.dart'; @@ -8,63 +5,34 @@ class PassPurchaseDetailsRepository { final NetworkApiService _apiServices = NetworkApiService(); /// Submit user details for pass purchase - /// POST https://devapi.citycards.betadelivery.com/mobile/passes/{bookingId}/user-details Future> submitUserDetails({ required int bookingId, required bool isForSelf, String? recipientFirstName, String? recipientLastName, + String? isdCode, String? recipientEmail, String? recipientPhone, String? city, String? country, }) async { try { - log('🟢 submitUserDetails() called'); - log('📤 [SUBMIT USER DETAILS] Booking ID: $bookingId'); - log('📤 [SUBMIT USER DETAILS] Is For Self: $isForSelf'); - - // Construct URL with bookingId - final url = '${ApiUrls.baseUrl}/mobile/passes/$bookingId/user-details'; - - if (kDebugMode) { - print('📤 [SUBMIT USER DETAILS] API URL: $url'); - } - - // Request body - final requestBody = { - 'isForSelf': isForSelf, - 'recipientFirstName': recipientFirstName ?? '', - 'recipientLastName': recipientLastName ?? '', - 'recipientEmail': recipientEmail ?? '', - 'recipientPhone': recipientPhone ?? '', - 'recipientCity': city ?? '', - 'recipientCountry': country ?? '', - }; - - log('📦 Request Body: $requestBody'); - - // Send POST request final response = await _apiServices.putApi( - url: url, - data: requestBody, + url: '${ApiUrls.baseUrl}/mobile/passes/$bookingId/user-details', + data: { + 'isForSelf': isForSelf, + 'recipientFirstName': recipientFirstName ?? '', + 'recipientLastName': recipientLastName ?? '', + 'isdCode': isdCode ?? '', + 'recipientEmail': recipientEmail ?? '', + 'recipientPhone': recipientPhone ?? '', + 'recipientCity': city ?? '', + 'recipientCountry': country ?? '', + }, ); - log('✅ [SUBMIT USER DETAILS] Response Status: ${response.statusCode}'); - log('📥 [SUBMIT USER DETAILS] Response Data: ${response.data}'); - - if (kDebugMode) { - print('📤 [SUBMIT USER DETAILS] ✅ User details submission successful'); - print('📤 [SUBMIT USER DETAILS] Full Response: ${response.data}'); - } - return response.data as Map; - } catch (e, stackTrace) { - log( - '❌ submitUserDetails FAILED', - error: e, - stackTrace: stackTrace, - ); + } catch (e) { throw Exception('Failed to submit user details: $e'); } } diff --git a/lib/common_packages/custom_textfield.dart b/lib/common_packages/custom_textfield.dart index 5bb2696..4bb5407 100644 --- a/lib/common_packages/custom_textfield.dart +++ b/lib/common_packages/custom_textfield.dart @@ -13,6 +13,7 @@ class CustomTextField extends StatelessWidget { final TextInputType? keyboardType; final bool obscureText; final Widget? suffixIcon; + final Widget? prefixWidget; // ✅ NEW: optional prefix (e.g. CountryCodePicker) final void Function(String)? onChanged; final int? maxLength; @@ -26,7 +27,7 @@ class CustomTextField extends StatelessWidget { final bool noSpecialCharacters; final bool isFirstLetterCapital; final int mobileLength; - final bool isPreview; // ✅ NEW + final bool isPreview; const CustomTextField({ super.key, @@ -39,6 +40,7 @@ class CustomTextField extends StatelessWidget { this.keyboardType, this.obscureText = false, this.suffixIcon, + this.prefixWidget, // ✅ NEW this.onChanged, this.maxLength, this.numbersOnly = false, @@ -49,7 +51,7 @@ class CustomTextField extends StatelessWidget { this.noSpecialCharacters = false, this.isFirstLetterCapital = false, this.mobileLength = 10, - this.isPreview = false, // ✅ NEW + this.isPreview = false, }); void _capitalizeFirstLetter(String value) { @@ -68,7 +70,7 @@ class CustomTextField extends StatelessWidget { } String? _internalValidator(String? value) { - if (isPreview) return null; // ✅ Skip validation in preview mode + if (isPreview) return null; if (value == null || value.trim().isEmpty) { return 'Please enter $label'; @@ -106,7 +108,6 @@ class CustomTextField extends StatelessWidget { Widget build(BuildContext context) { final List inputFormatters = []; - // ✅ Block all input in preview mode if (isPreview) { inputFormatters.add( TextInputFormatter.withFunction((oldValue, newValue) => oldValue), @@ -144,91 +145,133 @@ class CustomTextField extends StatelessWidget { } } + // ✅ Determine border radius — if prefixWidget is present, only round the right side + final borderRadius = prefixWidget != null + ? BorderRadius.only( + topRight: Radius.circular(8.r), + bottomRight: Radius.circular(8.r), + ) + : BorderRadius.circular(8.r); + + // ✅ Determine fill color + final fillColor = isPreview + ? Colors.grey.shade100 + : enabled + ? const Color(0xFFFFF5F5) + : Colors.grey.shade200; + + final textFormField = TextFormField( + controller: controller, + maxLines: obscureText ? 1 : maxLines, + enabled: isPreview ? false : enabled, + obscureText: obscureText, + validator: validator ?? _internalValidator, + autovalidateMode: AutovalidateMode.onUserInteraction, + keyboardType: keyboardType ?? + (isMobileNumber + ? TextInputType.phone + : isEmail + ? TextInputType.emailAddress + : TextInputType.name), + inputFormatters: inputFormatters, + onChanged: (value) { + if (isFirstLetterCapital) { + _capitalizeFirstLetter(value); + } + if (onChanged != null) { + onChanged!(value); + } + }, + decoration: InputDecoration( + hintText: hint, + counterText: "", + hintStyle: TextStyle( + fontSize: 12.sp, + color: const Color(0xFF8E8E8E), + ), + filled: true, + fillColor: fillColor, + contentPadding: EdgeInsets.symmetric( + // ✅ Reduce left padding when prefixWidget takes up the left side + horizontal: prefixWidget != null ? 12.w : 24.w, + vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h, + ), + suffixIcon: suffixIcon, + enabledBorder: OutlineInputBorder( + borderRadius: borderRadius, + borderSide: BorderSide( + color: const Color(0xBBC83B61).withOpacity(0.4), + width: .4.w, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: borderRadius, + borderSide: BorderSide( + color: const Color(0xFFF95F62), + width: 1.w, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: borderRadius, + borderSide: BorderSide( + color: Colors.red, + width: 1.w, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: borderRadius, + borderSide: BorderSide( + color: Colors.red, + width: 1.5.w, + ), + ), + errorStyle: TextStyle( + fontSize: 11.sp, + color: Colors.red, + height: 1.3, + ), + ), + ); + return Padding( padding: EdgeInsets.only(bottom: 14.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText( - text: label, - size: 14.sp, - ), - SizedBox(height: 6.h), - TextFormField( - controller: controller, - maxLines: obscureText ? 1 : maxLines, - enabled: isPreview ? false : enabled, // ✅ Disable in preview - obscureText: obscureText, - validator: validator ?? _internalValidator, - autovalidateMode: AutovalidateMode.onUserInteraction, - keyboardType: keyboardType ?? - (isMobileNumber - ? TextInputType.phone - : isEmail - ? TextInputType.emailAddress - : TextInputType.name), - inputFormatters: inputFormatters, - onChanged: (value) { - if (isFirstLetterCapital) { - _capitalizeFirstLetter(value); - } - if (onChanged != null) { - onChanged!(value); - } - }, - decoration: InputDecoration( - hintText: hint, - counterText: "", - hintStyle: TextStyle( - fontSize: 12.sp, - color: const Color(0xFF8E8E8E), + // ✅ Only show label row if label is not empty + if (label.isNotEmpty) ...[ + CustomText(text: label, size: 14.sp), + SizedBox(height: 6.h), + ], + + // ✅ If prefixWidget provided, wrap it in a Row with the picker on the left + if (prefixWidget != null) + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Prefix container — styled to match the field + Container( + decoration: BoxDecoration( + color: fillColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8.r), + bottomLeft: Radius.circular(8.r), + ), + border: Border.all( + color: const Color(0xBBC83B61).withOpacity(0.4), + width: 0.4.w, + ), + ), + child: prefixWidget!, + ), + // TextField takes the remaining space + Expanded(child: textFormField), + ], ), - filled: true, - fillColor: isPreview - ? Colors.grey.shade100 // ✅ Distinct preview background - : enabled - ? const Color(0xFFFFF5F5) - : Colors.grey.shade200, - contentPadding: EdgeInsets.symmetric( - horizontal: 24.w, - vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h, - ), - suffixIcon: suffixIcon, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: const Color(0xBBC83B61).withOpacity(0.4), - width: .4.w, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: const Color(0xFFF95F62), - width: 1.w, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: Colors.red, - width: 1.w, - ), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.r), - borderSide: BorderSide( - color: Colors.red, - width: 1.5.w, - ), - ), - errorStyle: TextStyle( - fontSize: 11.sp, - color: Colors.red, - height: 1.3, - ), - ), - ), + ) + else + textFormField, ], ), ); diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index a06e0ec..62eeee6 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -98,6 +98,7 @@ class AppRouter { case RouteConstants.passAttractionsPage: final Map args = settings.arguments as Map; final int cityId = args['cityId'] as int; + final int bookingId = args['bookingId'] as int; final String source = args['source'] as String; return MaterialPageRoute( @@ -109,6 +110,7 @@ class AppRouter { child: PassAttractionsPage( cityXid: cityId, source: source, + bookingId: bookingId, ), ); }, diff --git a/lib/core/inside_bottom_navigator.dart b/lib/core/inside_bottom_navigator.dart index 1f555f0..2d6f833 100644 --- a/lib/core/inside_bottom_navigator.dart +++ b/lib/core/inside_bottom_navigator.dart @@ -33,6 +33,7 @@ import '../networkApiServices/noInternet/view/no_internet_screen.dart'; import '../offer_pass_detail/offer_pass_detail_view.dart'; import '../postcard/blocs/postcard_creation_bloc.dart'; import '../postcard/views/postcard_creation_page_view.dart'; +import '../profile/view/contact_us/contact_us_view.dart'; import '../profile/view/privacy/privacy_view.dart'; import '../search_offers/bloc/offers_bloc.dart'; import '../search_offers/bloc/search_offers_listing_bloc.dart'; @@ -81,6 +82,7 @@ Widget buildOffstageNavigator( case RouteConstants.passAttractionsPage: final Map args = settings.arguments as Map; final int cityId = args['cityId'] as int; + final int bookingId = args['bookingId'] as int; final String source = args['source'] as String; return MaterialPageRoute( @@ -92,6 +94,7 @@ Widget buildOffstageNavigator( child: PassAttractionsPage( cityXid: cityId, source: source, + bookingId: bookingId, ), ); }, @@ -106,28 +109,34 @@ Widget buildOffstageNavigator( ); case RouteConstants.passAttractionDetails: - final attractionID = settings.arguments as int; + final args = settings.arguments as Map; + final attractionId = args['attractionId'] as int; + final bookingId = args['bookingId'] as int; return MaterialPageRoute( - builder: (_) { - return PassAttractionDetailsView(attractionId: attractionID); - }, + builder: (_) => PassAttractionDetailsView( + attractionId: attractionId, + bookingId: bookingId, + ), ); case RouteConstants.makeBooking: + final args = settings.arguments as Map?; + return MaterialPageRoute( - builder: (_) { - return MakeBookingView( - title: 'Koh Rong Samloem', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Convallis condimentum morbi non egestas enim amet sagittis.ß', - ); - }, + builder: (_) => MakeBookingView( + title: args?['title'] ?? '', + description: args?['description'] ?? '', + validUpto: args?['validUpto'] ?? '', + attractionId: args?['attractionId'] ?? 0, + bookingId: args?['bookingId'] ?? 0, + ), ); case RouteConstants.bookingSuccessful: + final message = settings.arguments as String; return MaterialPageRoute( builder: (_) { - return BookingSuccessfulPageView(); + return BookingSuccessfulPageView(message: message,); }, ); @@ -166,6 +175,12 @@ Widget buildOffstageNavigator( return const PrivacyPolicyPage(); }, ); + case RouteConstants.contactUs: + return MaterialPageRoute( + builder: (_) { + return const ContactUsPage(); + }, + ); // 🔹 Upload Photo Page (start of postcard creation flow) case RouteConstants.uploadPhotoPage: diff --git a/lib/create_account/bloc/create_account_bloc.dart b/lib/create_account/bloc/create_account_bloc.dart index b808abe..3513bb8 100644 --- a/lib/create_account/bloc/create_account_bloc.dart +++ b/lib/create_account/bloc/create_account_bloc.dart @@ -25,6 +25,7 @@ class CreateAccountBloc extends Bloc { firstName: event.firstName, lastName: event.lastName, emailAddress: event.emailAddress, + isdCode: event.isdCode, mobileNumber: event.mobileNumber, address1: event.address1, address2: event.address2, diff --git a/lib/create_account/bloc/create_account_event.dart b/lib/create_account/bloc/create_account_event.dart index 26a484b..e562411 100644 --- a/lib/create_account/bloc/create_account_event.dart +++ b/lib/create_account/bloc/create_account_event.dart @@ -11,6 +11,7 @@ class CreateAccountSubmitted extends CreateAccountEvent { final String firstName; final String lastName; final String emailAddress; + final String isdCode; final String mobileNumber; final String address1; final String address2; @@ -23,6 +24,7 @@ class CreateAccountSubmitted extends CreateAccountEvent { required this.firstName, required this.lastName, required this.emailAddress, + required this.isdCode, required this.mobileNumber, required this.address1, required this.address2, @@ -37,6 +39,7 @@ class CreateAccountSubmitted extends CreateAccountEvent { firstName, lastName, emailAddress, + isdCode, mobileNumber, address1, address2, diff --git a/lib/create_account/repository/create_account_repository.dart b/lib/create_account/repository/create_account_repository.dart index 2f54d8c..9d6d11f 100644 --- a/lib/create_account/repository/create_account_repository.dart +++ b/lib/create_account/repository/create_account_repository.dart @@ -8,6 +8,7 @@ class CreateAccountRepository { required String firstName, required String lastName, required String emailAddress, + required String isdCode, required String mobileNumber, required String address1, required String address2, @@ -23,6 +24,7 @@ class CreateAccountRepository { "firstName": firstName, "lastName": lastName, "emailAddress": emailAddress, + "isdCode": isdCode, "mobileNumber": mobileNumber, "address1": address1, "address2": address2, diff --git a/lib/create_account/view/create_account_view.dart b/lib/create_account/view/create_account_view.dart index effaf7f..903f261 100644 --- a/lib/create_account/view/create_account_view.dart +++ b/lib/create_account/view/create_account_view.dart @@ -2,9 +2,14 @@ import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/custom_filled_button.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_textfield.dart'; +import 'package:country_code_picker/country_code_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; + +import 'package:geocoding/geocoding.dart'; + import '../../cart/blocs/myPostcardsCart/my_postcards_cart_bloc.dart'; import '../../core/route_constants.dart'; import 'package:citycards_customer/my_pass/blocs/myPasses/my_passes_event.dart'; @@ -37,18 +42,51 @@ class _CreateAccountViewState extends State { final TextEditingController cityController = TextEditingController(); final TextEditingController postalController = TextEditingController(); - String? selectedState; - String? selectedCountry; + // ── Replaced dropdowns with plain text controllers ───────────────────────── + final TextEditingController stateController = TextEditingController(); + final TextEditingController countryController = TextEditingController(); + // ────────────────────────────────────────────────────────────────────────── + + String _selectedIsdCode = '+61'; + bool _isZipLoading = false; + + // ── PRIMARY geocoding: zip → city, state, country ────────────────────────── + Future fetchLocationFromZip(String zip) async { + if (zip.trim().length < 4) return; // wait for a meaningful zip length + setState(() => _isZipLoading = true); + try { + List locations = await locationFromAddress(zip); + if (locations.isNotEmpty) { + List placemarks = await placemarkFromCoordinates( + locations.first.latitude, + locations.first.longitude, + ); + final place = placemarks.first; + setState(() { + cityController.text = place.locality ?? ''; + stateController.text = place.administrativeArea ?? ''; + countryController.text = place.country ?? ''; + }); + } + } catch (e) { + debugPrint("Zip lookup failed: $e"); + } finally { + if (mounted) setState(() => _isZipLoading = false); + } + } + // ────────────────────────────────────────────────────────────────────────── + void _submitForm(BuildContext context) { + // 1. Empty field check if (firstNameController.text.trim().isEmpty || lastNameController.text.trim().isEmpty || emailController.text.trim().isEmpty || phoneController.text.trim().isEmpty || addressController.text.trim().isEmpty || cityController.text.trim().isEmpty || - selectedState == null || - selectedCountry == null || + stateController.text.trim().isEmpty || + countryController.text.trim().isEmpty || postalController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Please fill all fields')), @@ -56,17 +94,41 @@ class _CreateAccountViewState extends State { return; } + // 2. Phone validation against selected country code + final phone = phoneController.text.trim(); + bool isValidPhone = false; + + try { + final fullNumber = '$_selectedIsdCode$phone'; + final parsed = PhoneNumber.parse(fullNumber); + isValidPhone = parsed.isValid(); + } catch (_) { + isValidPhone = false; + } + + if (!isValidPhone) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Enter a valid phone number for $_selectedIsdCode'), + backgroundColor: Colors.red, + ), + ); + return; + } + + // 3. Submit context.read().add( CreateAccountSubmitted( firstName: firstNameController.text.trim(), lastName: lastNameController.text.trim(), emailAddress: emailController.text.trim(), - mobileNumber: phoneController.text.trim(), + isdCode: _selectedIsdCode, + mobileNumber: phone, address1: addressController.text.trim(), address2: '', city: cityController.text.trim(), - state: selectedState!, - country: selectedCountry!, + state: stateController.text.trim(), + country: countryController.text.trim(), postalCode: postalController.text.trim(), ), ); @@ -81,6 +143,8 @@ class _CreateAccountViewState extends State { addressController.dispose(); cityController.dispose(); postalController.dispose(); + stateController.dispose(); + countryController.dispose(); super.dispose(); } @@ -99,15 +163,15 @@ class _CreateAccountViewState extends State { context.read().add(CheckLoginStatusEvent()); context.read().add(CheckLoginStatus()); context.read().add(CheckLoginAndFetchItinerary()); - // context.read().add(FetchDraftPostCards()); context.read().add(RefreshDraftPostCards()); context.read().add(RefreshOrderPostCards()); context.read().add(CheckLoginAndFetchPasses()); - context.read().add(CheckLoginAndFetchPostcardsCart()); + context + .read() + .add(CheckLoginAndFetchPostcardsCart()); Navigator.pop(context); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(state.message))); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(state.message))); } else if (state is CreateAccountFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -132,7 +196,7 @@ class _CreateAccountViewState extends State { ), ), - /// 🔹 Scrollable content starts here + /// Scrollable content Expanded( child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 20.w), @@ -142,9 +206,7 @@ class _CreateAccountViewState extends State { Row( children: [ GestureDetector( - onTap: () { - Navigator.pop(context); - }, + onTap: () => Navigator.pop(context), child: const Icon(Icons.arrow_back), ), SizedBox(width: 8.w), @@ -201,15 +263,38 @@ class _CreateAccountViewState extends State { keyboardType: TextInputType.emailAddress, ), ), + + // ── Phone Number ────────────────────────────────────── Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( label: "Phone Number *", - hint: "Enter your phone number", + hint: "Enter phone number", controller: phoneController, - keyboardType: TextInputType.number, - maxLength: 10, - isMobileNumber: true, + keyboardType: TextInputType.phone, + maxLength: 12, + numbersOnly: true, + prefixWidget: CountryCodePicker( + onChanged: (country) { + setState(() => _selectedIsdCode = country.dialCode!); + }, + initialSelection: 'AU', + favorite: const ['+61', '+1', '+44', '+91'], + showCountryOnly: false, + showOnlyCountryWhenClosed: false, + alignLeft: false, + flagWidth: 24.w, + padding: EdgeInsets.symmetric(horizontal: 8.w), + textStyle: TextStyle( + fontSize: 13.sp, + color: const Color(0xFF2D3134), + ), + dialogTextStyle: TextStyle(fontSize: 14.sp), + searchDecoration: const InputDecoration( + hintText: 'Search country...', + prefixIcon: Icon(Icons.search), + ), + ), ), ), @@ -223,16 +308,20 @@ class _CreateAccountViewState extends State { SizedBox(height: 16.h), + // ── Address ─────────────────────────────────────────── Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( label: "Address *", - hint: "Enter address manually or tap to search", + hint: "Enter your address", controller: addressController, maxLength: 50, - // noSpecialCharacters: true, ), ), + + SizedBox(height: 8.h), + + // ── City (unchanged) ────────────────────────────────── Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( @@ -245,144 +334,79 @@ class _CreateAccountViewState extends State { ), ), - // State Dropdown - Padding( - padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText(text: "State *", size: 14.sp), - SizedBox(height: 6.h), - Container( - height: 42.h, - padding: EdgeInsets.symmetric(horizontal: 24.w), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: const Color(0xBBC83B61).withOpacity(0.4), - width: 0.4.w, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedState, - isExpanded: true, - icon: const Icon( - Icons.keyboard_arrow_down, - color: Color(0xFF8E8E8E), - ), - hint: Text( - "Select state", - style: TextStyle( - fontSize: 12.sp, - color: const Color(0xFF8E8E8E), - ), - ), - style: TextStyle( - fontSize: 14.sp, - color: const Color(0xFF2D3134), - ), - onChanged: (value) { - setState(() { - selectedState = value; - }); - }, - items: [ - "New South Wales", - "Victoria", - "Queensland", - "South Australia", - "Western Australia", - "Tasmania", - "Northern Territory", - "Australian Capital Territory" - ].map((value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: TextStyle(fontSize: 14.sp), - ), - ); - }).toList(), - ), - ), - ), - ], - ), - ), - - // Country Dropdown - Padding( - padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText(text: "Country *", size: 14.sp), - SizedBox(height: 6.h), - Container( - height: 42.h, - padding: EdgeInsets.symmetric(horizontal: 24.w), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: const Color(0xBBC83B61).withOpacity(0.4), - width: 0.4.w, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedCountry, - isExpanded: true, - icon: const Icon( - Icons.keyboard_arrow_down, - color: Color(0xFF8E8E8E), - ), - hint: Text( - "Select country", - style: TextStyle( - fontSize: 12.sp, - color: const Color(0xFF8E8E8E), - ), - ), - style: TextStyle( - fontSize: 14.sp, - color: const Color(0xFF2D3134), - ), - onChanged: (value) { - setState(() { - selectedCountry = value; - }); - }, - items: ["Australia"].map((value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: TextStyle(fontSize: 14.sp), - ), - ); - }).toList(), - ), - ), - ), - ], - ), - ), - + // ── State – now a plain text field ──────────────────── Padding( padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "Zip Code *", - hint: "Enter the zip code you reside in", - controller: postalController, - keyboardType: TextInputType.number, - maxLength: 6, + label: "State *", + hint: "Enter your state", + maxLength: 50, + noSpace: true, + controller: stateController, + isFirstLetterCapital: true, ), ), + + // ── Country – now a plain text field ────────────────── + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Country *", + hint: "Enter your country", + maxLength: 50, + noSpace: true, + controller: countryController, + isFirstLetterCapital: true, + ), + ), + + // ── Zip Code → auto-fills City, State, Country ──────── + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: CustomTextField( + controller: postalController, + keyboardType: TextInputType.number, + maxLength: 6, + onChanged: fetchLocationFromZip, + label: 'Zip Code *', + hint: 'Enter the zip code you reside in', + ), + ), + if (_isZipLoading) + Padding( + padding: EdgeInsets.only(right: 12.w), + child: SizedBox( + width: 18.w, + height: 18.h, + child: + const CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFC83B61), + ), + ), + ), + ], + ), + SizedBox(height: 4.h), + Text( + "City, State & Country will auto-fill from zip", + style: TextStyle( + fontSize: 10.sp, + color: const Color(0xFF8E8E8E), + ), + ), + ], + ), + ), + SizedBox(height: 20.h), + BlocBuilder( builder: (context, state) { if (state is CreateAccountLoading) { diff --git a/lib/esim_offer/esim_offer_view.dart b/lib/esim_offer/esim_offer_view.dart index 1a7b1a3..3229098 100644 --- a/lib/esim_offer/esim_offer_view.dart +++ b/lib/esim_offer/esim_offer_view.dart @@ -84,7 +84,7 @@ class EsimOfferPage extends StatelessWidget { width: 350.w, child: CustomText( text: - "Stay Connected Instantly with Your Complimentary eSIM", + "Connect instantly with your free eSIM", size: 22.sp, color: Color(0xFFFFFFFF), ), @@ -94,7 +94,7 @@ class EsimOfferPage extends StatelessWidget { width: 350, child: CustomText( text: - "Because every unforgettable trip starts with seamless connectivity.", + "Every great journey begins with smooth connectivity.", size: 14.sp, color: Colors.white, ), @@ -285,7 +285,7 @@ class EsimOfferPage extends StatelessWidget { ), ), TextSpan( - text: " CityCard", + text: " CityCards", style: TextStyle( color: Color(0xFFF95F62), fontSize: 21.sp, diff --git a/lib/home/views/first_time_user_home_page.dart b/lib/home/views/first_time_user_home_page.dart index 56eed71..3eba5ec 100644 --- a/lib/home/views/first_time_user_home_page.dart +++ b/lib/home/views/first_time_user_home_page.dart @@ -109,9 +109,9 @@ class _FirstTimeUserHomePageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - "Get Your CityCard", - style: TextStyle(color: Colors.white), + Text( + "Get Your CityCards", + style: TextStyle(color: Colors.white,fontSize: 14.sp), ), SizedBox(width: 10.w), Image.asset("assets/icons/arrow.png", height: 13.h), diff --git a/lib/home/widgets/get_your_pass_card.dart b/lib/home/widgets/get_your_pass_card.dart index f94ff61..e1af9dc 100644 --- a/lib/home/widgets/get_your_pass_card.dart +++ b/lib/home/widgets/get_your_pass_card.dart @@ -25,7 +25,7 @@ class GetYourPassCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Get your Pass", + "Get Your Card", style: GoogleFonts.poppins( fontSize: 18.sp, fontWeight: FontWeight.w500, diff --git a/lib/home/widgets/pass_card_list.dart b/lib/home/widgets/pass_card_list.dart index 68289ed..30365de 100644 --- a/lib/home/widgets/pass_card_list.dart +++ b/lib/home/widgets/pass_card_list.dart @@ -52,7 +52,7 @@ class _ChooseYourPassSectionState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Choose your card", + "Choose Your Card", style: GoogleFonts.poppins( fontSize: 18.sp, fontWeight: FontWeight.w600, @@ -206,7 +206,7 @@ class _ChooseYourPassSectionState extends State { ), ), child: Text( - "Get a Pass", + "Get a Card", style: GoogleFonts.poppins( fontWeight: FontWeight.w500, fontSize: 14.sp, diff --git a/lib/hotel_offer/hotel_offer_view.dart b/lib/hotel_offer/hotel_offer_view.dart index 0d00d99..0f436c0 100644 --- a/lib/hotel_offer/hotel_offer_view.dart +++ b/lib/hotel_offer/hotel_offer_view.dart @@ -176,7 +176,7 @@ class HotelOfferView extends StatelessWidget { "Choose from a wide variety of Marriott hotels — from elegant urban hideaways and premium city-centre locations to luxurious five-star experiences — all designed to make your trip ", ), TextSpan( - text: "effortless, comfortable and memorable", + text: "effortless, comfortable", style: TextStyle( color: const Color(0xFFF95F62), fontWeight: FontWeight.w600, diff --git a/lib/itinerary_creation/views/itinerary_creation_start_view.dart b/lib/itinerary_creation/views/itinerary_creation_start_view.dart index 0b9faf4..843f523 100644 --- a/lib/itinerary_creation/views/itinerary_creation_start_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_start_view.dart @@ -85,7 +85,7 @@ class ItineraryCreationStartPage extends StatelessWidget { label: "Let’s explore together!", ), - SizedBox(height: 35.h), + SizedBox(height: 10.h), /// Footer Text CustomText( diff --git a/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart b/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart index 7e4a845..f7f6fbc 100644 --- a/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart +++ b/lib/itinerary_creation/views/itinerary_creation_steps/itinerary_completion_view.dart @@ -216,13 +216,20 @@ class _ItineraryCompletionViewState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + Lottie.asset( + 'assets/intro/itinerary_creating.json', + width: 260.w, + height: 260.w, + fit: BoxFit.contain, + ), + SizedBox(height: 24.h), RichText( textAlign: TextAlign.center, text: TextSpan( style: GoogleFonts.poppins(fontSize: 24.sp), children: const [ TextSpan( - text: 'Building\n', + text: 'Creating\n', style: TextStyle( color: Color(0xFF364153), fontWeight: FontWeight.bold, @@ -238,13 +245,6 @@ class _ItineraryCompletionViewState extends State ], ), ), - SizedBox(height: 24.h), - Lottie.asset( - 'assets/intro/itinerary_creating.json', - width: 260.w, - height: 260.w, - fit: BoxFit.contain, - ), ], ), ), diff --git a/lib/my_pass/blocs/checkIn/check_in_bloc.dart b/lib/my_pass/blocs/checkIn/check_in_bloc.dart new file mode 100644 index 0000000..ec6fbff --- /dev/null +++ b/lib/my_pass/blocs/checkIn/check_in_bloc.dart @@ -0,0 +1,40 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repository/check_in_repository.dart'; + +part 'check_in_event.dart'; +part 'check_in_state.dart'; + +class CheckInBloc extends Bloc { + final CheckInRepository _checkInRepository; + + CheckInBloc({CheckInRepository? checkInRepository}) + : _checkInRepository = checkInRepository ?? CheckInRepository(), + super(const CheckInInitial()) { + on(_onDoCheckIn); + on(_onReset); + } + + Future _onDoCheckIn( + DoCheckInEvent event, + Emitter emit, + ) async { + emit(const CheckInLoading()); + try { + final response = await _checkInRepository.checkIn( + passId: event.passId, + attractionId: event.attractionId, + ); + emit(CheckInSuccess(data: response.data)); + } catch (e) { + emit(CheckInFailure(error: e.toString())); + } + } + + void _onReset( + ResetCheckInEvent event, + Emitter emit, + ) { + emit(const CheckInInitial()); + } +} \ No newline at end of file diff --git a/lib/my_pass/blocs/checkIn/check_in_event.dart b/lib/my_pass/blocs/checkIn/check_in_event.dart new file mode 100644 index 0000000..da82aab --- /dev/null +++ b/lib/my_pass/blocs/checkIn/check_in_event.dart @@ -0,0 +1,27 @@ +part of 'check_in_bloc.dart'; + +abstract class CheckInEvent extends Equatable { + const CheckInEvent(); + + @override + List get props => []; +} + +/// Trigger check-in +class DoCheckInEvent extends CheckInEvent { + final int passId; + final int attractionId; + + const DoCheckInEvent({ + required this.passId, + required this.attractionId, + }); + + @override + List get props => [passId, attractionId]; +} + +/// Reset state back to initial +class ResetCheckInEvent extends CheckInEvent { + const ResetCheckInEvent(); +} \ No newline at end of file diff --git a/lib/my_pass/blocs/checkIn/check_in_state.dart b/lib/my_pass/blocs/checkIn/check_in_state.dart new file mode 100644 index 0000000..b49ef36 --- /dev/null +++ b/lib/my_pass/blocs/checkIn/check_in_state.dart @@ -0,0 +1,38 @@ +part of 'check_in_bloc.dart'; + +abstract class CheckInState extends Equatable { + const CheckInState(); + + @override + List get props => []; +} + +/// Initial state +class CheckInInitial extends CheckInState { + const CheckInInitial(); +} + +/// Loading state +class CheckInLoading extends CheckInState { + const CheckInLoading(); +} + +/// Success state +class CheckInSuccess extends CheckInState { + final dynamic data; + + const CheckInSuccess({required this.data}); + + @override + List get props => [data]; +} + +/// Failure state +class CheckInFailure extends CheckInState { + final String error; + + const CheckInFailure({required this.error}); + + @override + List get props => [error]; +} \ No newline at end of file diff --git a/lib/my_pass/blocs/make_booking_bloc.dart b/lib/my_pass/blocs/make_booking_bloc.dart index bbcc597..7efc4e3 100644 --- a/lib/my_pass/blocs/make_booking_bloc.dart +++ b/lib/my_pass/blocs/make_booking_bloc.dart @@ -1,35 +1,80 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'make_booking_events.dart'; import 'make_booking_state.dart'; +import '../repository/make_booking_repository.dart'; // adjust path if needed class MakeBookingBloc extends Bloc { + final BookingRepository _repository = BookingRepository(); + MakeBookingBloc() : super(const MakeBookingState(loading: true)) { on(_onLoadAvailableDates); on(_onSelectDate); + on(_onConfirmBooking); // NEW } void _onLoadAvailableDates( LoadAvailableDates event, Emitter emit) async { emit(state.copyWith(loading: true)); - // Simulate API load delay await Future.delayed(const Duration(milliseconds: 500)); - // Dummy available dates - final now = DateTime.now(); - final available = [ - now.add(const Duration(days: 2)), - now.add(const Duration(days: 5)), - now.add(const Duration(days: 7)), - now.add(const Duration(days: 10)), - now.add(const Duration(days: 11)), - now.add(const Duration(days: 13)), - ]; + // Parse "dd-MM-yyyy" → DateTime + final parts = event.validUpto.split('-'); + final validUptoDate = DateTime( + int.parse(parts[2]), // year + int.parse(parts[1]), // month + int.parse(parts[0]), // day + ); - emit(state.copyWith(availableDates: available, loading: false)); + emit(state.copyWith( + availableDates: [], + validUptoDate: validUptoDate, + loading: false, + )); } void _onSelectDate(SelectDate event, Emitter emit) { emit(state.copyWith(startDate: event.startDate, endDate: event.endDate)); } -} + + // NEW — calls repository, emits isConfirmed + successMessage on success + Future _onConfirmBooking( + ConfirmBooking event, Emitter emit) async { + emit(state.copyWith(isConfirming: true, error: null)); + + try { + // Format DateTime → "yyyy-MM-dd" as required by API + final bookingStartDate = _formatDate(event.startDate); + final bookingEndDate = _formatDate(event.endDate); + + final response = await _repository.confirmBookingDate( + attractionId: event.attractionId, + bookingId: event.bookingId, + bookingStartDate: bookingStartDate, + bookingEndDate: bookingEndDate, + ); + + // API response: { "message": "Your booking has been confirmed on 17-03-2026" } + final message = response['message'] as String? ?? 'Booking confirmed!'; + + emit(state.copyWith( + isConfirming: false, + isConfirmed: true, + successMessage: message, + )); + } catch (e) { + emit(state.copyWith( + isConfirming: false, + error: e.toString(), + )); + } + } + + /// Formats DateTime to "yyyy-MM-dd" e.g. "2026-03-20" + String _formatDate(DateTime date) { + final y = date.year.toString().padLeft(4, '0'); + final m = date.month.toString().padLeft(2, '0'); + final d = date.day.toString().padLeft(2, '0'); + return '$y-$m-$d'; + } +} \ No newline at end of file diff --git a/lib/my_pass/blocs/make_booking_events.dart b/lib/my_pass/blocs/make_booking_events.dart index 45bd0fe..b9aa555 100644 --- a/lib/my_pass/blocs/make_booking_events.dart +++ b/lib/my_pass/blocs/make_booking_events.dart @@ -5,7 +5,14 @@ abstract class MakeBookingEvent extends Equatable { List get props => []; } -class LoadAvailableDates extends MakeBookingEvent {} +class LoadAvailableDates extends MakeBookingEvent { + final String validUpto; // format: "dd-MM-yyyy" e.g. "21-03-2026" + + LoadAvailableDates({required this.validUpto}); + + @override + List get props => [validUpto]; +} class SelectDate extends MakeBookingEvent { final DateTime startDate; @@ -16,3 +23,21 @@ class SelectDate extends MakeBookingEvent { @override List get props => [startDate, endDate]; } + +// NEW — fired when user taps "Confirm Booking" +class ConfirmBooking extends MakeBookingEvent { + final int attractionId; + final int bookingId; + final DateTime startDate; + final DateTime endDate; + + ConfirmBooking({ + required this.attractionId, + required this.bookingId, + required this.startDate, + required this.endDate, + }); + + @override + List get props => [attractionId, bookingId, startDate, endDate]; +} \ No newline at end of file diff --git a/lib/my_pass/blocs/make_booking_state.dart b/lib/my_pass/blocs/make_booking_state.dart index d87a5af..5b7e35c 100644 --- a/lib/my_pass/blocs/make_booking_state.dart +++ b/lib/my_pass/blocs/make_booking_state.dart @@ -5,12 +5,24 @@ class MakeBookingState extends Equatable { final DateTime? startDate; final DateTime? endDate; final bool loading; + final DateTime? validUptoDate; + + // NEW fields + final bool isConfirming; // true while API call is in-progress + final bool isConfirmed; // true once API returns success + final String? successMessage; // e.g. "Your booking has been confirmed on 17-03-2026" + final String? error; // non-null when API call fails const MakeBookingState({ this.availableDates = const [], this.startDate, this.endDate, this.loading = false, + this.validUptoDate, + this.isConfirming = false, + this.isConfirmed = false, + this.successMessage, + this.error, }); MakeBookingState copyWith({ @@ -18,15 +30,35 @@ class MakeBookingState extends Equatable { DateTime? startDate, DateTime? endDate, bool? loading, + DateTime? validUptoDate, + bool? isConfirming, + bool? isConfirmed, + String? successMessage, + String? error, }) { return MakeBookingState( availableDates: availableDates ?? this.availableDates, startDate: startDate ?? this.startDate, endDate: endDate ?? this.endDate, loading: loading ?? this.loading, + validUptoDate: validUptoDate ?? this.validUptoDate, + isConfirming: isConfirming ?? this.isConfirming, + isConfirmed: isConfirmed ?? this.isConfirmed, + successMessage: successMessage ?? this.successMessage, + error: error ?? this.error, ); } @override - List get props => [availableDates, startDate, endDate, loading]; -} + List get props => [ + availableDates, + startDate, + endDate, + loading, + validUptoDate, + isConfirming, + isConfirmed, + successMessage, + error, + ]; +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_bloc.dart b/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_bloc.dart new file mode 100644 index 0000000..8b93696 --- /dev/null +++ b/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_bloc.dart @@ -0,0 +1,45 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../models/pass_attraction_details_model.dart'; +import '../../repository/pass_attraction_details_repository.dart'; +part 'pass_attraction_details_event.dart'; +part 'pass_attraction_details_state.dart'; + +class PassAttractionDetailsBloc + extends Bloc { + final PassAttractionDetailsRepository _repository; + + PassAttractionDetailsBloc({PassAttractionDetailsRepository? repository}) + : _repository = repository ?? PassAttractionDetailsRepository(), + super(PassAttractionDetailsInitial()) { + on(_onFetchPassAttractionDetails); + on(_onResetPassAttractionDetails); + } + + /// Handle fetching attraction details + Future _onFetchPassAttractionDetails( + FetchPassAttractionDetailsEvent event, + Emitter emit, + ) async { + emit(PassAttractionDetailsLoading()); + try { + final PassAttractionDetailsModel attractionDetails = + await _repository.fetchPassAttractionDetails( + attractionId: event.attractionId, + bookingId: event.bookingId, + ); + emit(PassAttractionDetailsLoaded(attractionDetails: attractionDetails)); + } catch (e) { + emit(PassAttractionDetailsError( + message: e.toString(), + )); + } + } + + /// Handle resetting state back to initial + void _onResetPassAttractionDetails( + ResetPassAttractionDetailsEvent event, + Emitter emit, + ) { + emit(PassAttractionDetailsInitial()); + } +} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_event.dart b/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_event.dart new file mode 100644 index 0000000..b84121a --- /dev/null +++ b/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_event.dart @@ -0,0 +1,15 @@ +part of 'pass_attraction_details_bloc.dart'; + +abstract class PassAttractionDetailsEvent {} + +class FetchPassAttractionDetailsEvent extends PassAttractionDetailsEvent { + final int attractionId; + final int bookingId; + + FetchPassAttractionDetailsEvent({ + required this.attractionId, + required this.bookingId, + }); +} + +class ResetPassAttractionDetailsEvent extends PassAttractionDetailsEvent {} \ No newline at end of file diff --git a/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_state.dart b/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_state.dart new file mode 100644 index 0000000..1ef5774 --- /dev/null +++ b/lib/my_pass/blocs/myPassesAttractionDetails/pass_attraction_details_state.dart @@ -0,0 +1,19 @@ +part of 'pass_attraction_details_bloc.dart'; + +abstract class PassAttractionDetailsState {} + +class PassAttractionDetailsInitial extends PassAttractionDetailsState {} + +class PassAttractionDetailsLoading extends PassAttractionDetailsState {} + +class PassAttractionDetailsLoaded extends PassAttractionDetailsState { + final PassAttractionDetailsModel attractionDetails; + + PassAttractionDetailsLoaded({required this.attractionDetails}); +} + +class PassAttractionDetailsError extends PassAttractionDetailsState { + final String message; + + PassAttractionDetailsError({required this.message}); +} \ No newline at end of file diff --git a/lib/my_pass/models/pass_attraction_details_model.dart b/lib/my_pass/models/pass_attraction_details_model.dart new file mode 100644 index 0000000..c5d2ab0 --- /dev/null +++ b/lib/my_pass/models/pass_attraction_details_model.dart @@ -0,0 +1,282 @@ +class PassAttractionDetailsModel { + final int id; + final String title; + final String description; + final int cityXid; + final int? cardTypeXid; + final int partnerXid; + final String? productCode; + final String subTitle; + final String urlSlug; + final bool isBookingRequired; + final bool isPartnerAccess; + final String? bookingEmail; + final String? bookingPhoneNumber; + final String address; + final double latitudeCoordinate; + final double longitudeCoordinate; + final double ticketPriceAdult; + final double ticketPriceChild; + final int durations; + final int groupSize; + final String ageRange; + final String seoTitle; + final String seoDescription; + final String attractionStatus; + final bool isActive; + final String createdAt; + final String updatedAt; + final List attractionGalleries; + final List attractionInclusions; + final List attractionFaqs; + final Qr qr; + + PassAttractionDetailsModel({ + required this.id, + required this.title, + required this.description, + required this.cityXid, + this.cardTypeXid, + required this.partnerXid, + this.productCode, + required this.subTitle, + required this.urlSlug, + required this.isBookingRequired, + required this.isPartnerAccess, + this.bookingEmail, + this.bookingPhoneNumber, + required this.address, + required this.latitudeCoordinate, + required this.longitudeCoordinate, + required this.ticketPriceAdult, + required this.ticketPriceChild, + required this.durations, + required this.groupSize, + required this.ageRange, + required this.seoTitle, + required this.seoDescription, + required this.attractionStatus, + required this.isActive, + required this.createdAt, + required this.updatedAt, + required this.attractionGalleries, + required this.attractionInclusions, + required this.attractionFaqs, + required this.qr, + }); + + factory PassAttractionDetailsModel.fromJson(Map json) { + return PassAttractionDetailsModel( + id: json['id'] ?? 0, + title: json['title'] ?? "N/A", + description: json['description'] ?? "N/A", + cityXid: json['cityXid'] ?? 0, + cardTypeXid: json['cardTypeXid'], + partnerXid: json['partnerXid'] ?? 0, + productCode: json['productCode'], + subTitle: json['subTitle'] ?? "N/A", + urlSlug: json['urlSlug'] ?? "N/A", + isBookingRequired: json['isBookingRequired'] ?? false, + isPartnerAccess: json['isPartnerAccess'] ?? false, + bookingEmail: json['bookingEmail'], + bookingPhoneNumber: json['bookingPhoneNumber'], + address: json['address'] ?? "N/A", + + latitudeCoordinate: (json['latitudeCoordinate'] is num) + ? (json['latitudeCoordinate'] as num).toDouble() + : 0.0, + + longitudeCoordinate: (json['longitudeCoordinate'] is num) + ? (json['longitudeCoordinate'] as num).toDouble() + : 0.0, + + ticketPriceAdult: (json['ticketPriceAdult'] is num) + ? (json['ticketPriceAdult'] as num).toDouble() + : 0.0, + + ticketPriceChild: (json['ticketPriceChild'] is num) + ? (json['ticketPriceChild'] as num).toDouble() + : 0.0, + + durations: json['durations'] ?? 0, + groupSize: json['groupSize'] ?? 0, + ageRange: json['ageRange'] ?? "N/A", + seoTitle: json['seoTitle'] ?? "N/A", + seoDescription: json['seoDescription'] ?? "N/A", + attractionStatus: json['attractionStatus'] ?? "N/A", + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? "N/A", + updatedAt: json['updatedAt'] ?? "N/A", + + attractionGalleries: List.from( + (json['attractionGalleries'] ?? []) + .map((e) => AttractionGallery.fromJson(e)), + ), + + attractionInclusions: List.from( + (json['attractionInclusions'] ?? []) + .map((e) => AttractionInclusion.fromJson(e)), + ), + + attractionFaqs: List.from( + (json['attractionFaqs'] ?? []) + .map((e) => AttractionFaq.fromJson(e)), + ), + + qr: json['qr'] != null ? Qr.fromJson(json['qr']) : Qr.empty(), + ); + } +} + +class AttractionGallery { + final int id; + final int attractionXid; + final String fileType; + final String filePathUrl; + final String altText; + final bool isCoverImage; + final bool isActive; + final String createdAt; + final String updatedAt; + + AttractionGallery({ + required this.id, + required this.attractionXid, + required this.fileType, + required this.filePathUrl, + required this.altText, + required this.isCoverImage, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory AttractionGallery.fromJson(Map json) { + return AttractionGallery( + id: json['id'] ?? 0, + attractionXid: json['attractionXid'] ?? 0, + fileType: json['fileType'] ?? "N/A", + filePathUrl: json['filePathUrl'] ?? "", + altText: json['altText'] ?? "", + isCoverImage: json['isCoverImage'] ?? false, + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? "", + updatedAt: json['updatedAt'] ?? "", + ); + } +} + +class AttractionInclusion { + final int id; + final int attractionXid; + final String title; + final String description; + final int? iconXid; + final bool isInclusion; + final bool isActive; + final String createdAt; + final String updatedAt; + + AttractionInclusion({ + required this.id, + required this.attractionXid, + required this.title, + required this.description, + this.iconXid, + required this.isInclusion, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + factory AttractionInclusion.fromJson(Map json) { + return AttractionInclusion( + id: json['id'] ?? 0, + attractionXid: json['attractionXid'] ?? 0, + title: json['title'] ?? "", + description: json['description'] ?? "", + iconXid: json['iconXid'], + isInclusion: json['isInclusion'] ?? false, + isActive: json['isActive'] ?? false, + createdAt: json['createdAt'] ?? "", + updatedAt: json['updatedAt'] ?? "", + ); + } +} + +class AttractionFaq { + final int id; + final int attractionXid; + final String faqQuestion; + final String faqAnswer; + final int displayOrder; + final bool isActive; + + AttractionFaq({ + required this.id, + required this.attractionXid, + required this.faqQuestion, + required this.faqAnswer, + required this.displayOrder, + required this.isActive, + }); + + factory AttractionFaq.fromJson(Map json) { + return AttractionFaq( + id: json['id'] ?? 0, + attractionXid: json['attractionXid'] ?? 0, + faqQuestion: json['faqQuestion'] ?? "", + faqAnswer: json['faqAnswer'] ?? "", + displayOrder: json['displayOrder'] ?? 0, + isActive: json['isActive'] ?? false, + ); + } +} + +class Qr { + final String qrCode; + final String qrStatus; + final String qrExpiresAt; + final bool isQrActive; + final String qrNumber; + final String? checkedInDatetime; + final int qrRemainingMinutes; + final String validUpto; + + Qr({ + required this.qrCode, + required this.qrStatus, + required this.qrExpiresAt, + required this.isQrActive, + required this.qrNumber, + this.checkedInDatetime, + required this.qrRemainingMinutes, + required this.validUpto, + }); + + factory Qr.fromJson(Map json) { + return Qr( + qrCode: json['qrCode'] ?? "N/A", + qrStatus: json['qrStatus'] ?? "N/A", + qrExpiresAt: json['qrExpiresAt'] ?? "N/A", + isQrActive: json['isQrActive'] ?? false, + qrNumber: json['qrNumber'] ?? "N/A", + checkedInDatetime: json['checkedInDatetime'], + qrRemainingMinutes: json['qrRemainingMinutes'] ?? 0, + validUpto: json['validUpto'] ?? "N/A", + ); + } + + factory Qr.empty() { + return Qr( + qrCode: "N/A", + qrStatus: "N/A", + qrExpiresAt: "N/A", + isQrActive: false, + qrNumber: "N/A", + checkedInDatetime: null, + qrRemainingMinutes: 0, + validUpto: "N/A", + ); + } +} \ No newline at end of file diff --git a/lib/my_pass/repository/check_in_repository.dart b/lib/my_pass/repository/check_in_repository.dart new file mode 100644 index 0000000..1e1680c --- /dev/null +++ b/lib/my_pass/repository/check_in_repository.dart @@ -0,0 +1,15 @@ +import 'package:dio/dio.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../../networkApiServices/network_api_services.dart'; + +class CheckInRepository { + final NetworkApiService _apiService = NetworkApiService(); + + Future checkIn({ + required int passId, + required int attractionId, + }) async { + final url = '${ApiUrls.checkIn}/$attractionId/$passId'; + return await _apiService.postApi(url: url); + } +} \ No newline at end of file diff --git a/lib/my_pass/repository/make_booking_repository.dart b/lib/my_pass/repository/make_booking_repository.dart new file mode 100644 index 0000000..41a093a --- /dev/null +++ b/lib/my_pass/repository/make_booking_repository.dart @@ -0,0 +1,27 @@ +import '../../networkApiServices/api_urls.dart'; +import '../../networkApiServices/network_api_services.dart'; + +class BookingRepository { + final NetworkApiService _apiServices = NetworkApiService(); + + Future> confirmBookingDate({ + required int attractionId, + required int bookingId, + required String bookingStartDate, + required String bookingEndDate, + }) async { + try { + final response = await _apiServices.postApi( + url: '${ApiUrls.booking}/$attractionId/$bookingId', // add this key in ApiUrls + data: { + "bookingStartDate": bookingStartDate, + "bookingEndDate": bookingEndDate, + }, + ); + + return response.data as Map; + } catch (e) { + throw Exception('Failed to confirm booking date: $e'); + } + } +} \ No newline at end of file diff --git a/lib/my_pass/repository/pass_attraction_details_repository.dart b/lib/my_pass/repository/pass_attraction_details_repository.dart new file mode 100644 index 0000000..63caac6 --- /dev/null +++ b/lib/my_pass/repository/pass_attraction_details_repository.dart @@ -0,0 +1,18 @@ +import '../../networkApiServices/network_api_services.dart'; +import '../../networkApiServices/api_urls.dart'; +import '../models/pass_attraction_details_model.dart'; +class PassAttractionDetailsRepository { + final NetworkApiService _apiService = NetworkApiService(); + + /// Fetch attraction details by attractionId + Future fetchPassAttractionDetails({ + required int attractionId, + required int bookingId, + }) async { + final response = await _apiService.getApi( + url: '${ApiUrls.passAttractionDetails}/$attractionId/$bookingId', + ); + + return PassAttractionDetailsModel.fromJson(response.data); + } +} diff --git a/lib/my_pass/views/booking_page_view.dart b/lib/my_pass/views/booking_page_view.dart index 35ac874..3362d6c 100644 --- a/lib/my_pass/views/booking_page_view.dart +++ b/lib/my_pass/views/booking_page_view.dart @@ -10,224 +10,392 @@ import 'package:syncfusion_flutter_datepicker/datepicker.dart'; import '../blocs/make_booking_bloc.dart'; import '../blocs/make_booking_events.dart'; import '../blocs/make_booking_state.dart'; +import '../blocs/myPassesAttractionDetails/pass_attraction_details_bloc.dart'; -class MakeBookingView extends StatelessWidget { +class MakeBookingView extends StatefulWidget { final String title; final String description; + final String validUpto; + final int attractionId; + final int bookingId; const MakeBookingView({ super.key, required this.title, required this.description, + required this.validUpto, + required this.attractionId, + required this.bookingId, }); + @override + State createState() => _MakeBookingViewState(); +} + +class _MakeBookingViewState extends State { + // true = user tapped Confirm without selecting both dates → show red border + message + bool _showValidationError = false; + + // true = user picked start date but hasn't picked end date yet → show orange hint + bool _onlyStartSelected = false; + @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => MakeBookingBloc()..add(LoadAvailableDates()), - child: BlocBuilder( - builder: (context, state) { - if (state.loading) { - return const Center(child: CircularProgressIndicator(color: Color(0xffF95F62))); + create: (_) => MakeBookingBloc() + ..add(LoadAvailableDates(validUpto: widget.validUpto)), + + child: BlocListener( + listenWhen: (previous, current) => + previous.isConfirmed != current.isConfirmed || + previous.error != current.error, + listener: (context, state) { + if (state.isConfirmed && state.successMessage != null) { + Navigator.of(context).pushReplacementNamed( + RouteConstants.bookingSuccessful, + arguments: state.successMessage, + ); + // context.read().add( + // FetchPassAttractionDetailsEvent( + // attractionId: widget.attractionId, + // bookingId: widget.bookingId, + // ), + // ); } + if (state.error != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error!), + backgroundColor: Colors.red, + ), + ); + } + }, - final bloc = context.read(); - final now = DateTime.now(); + child: BlocBuilder( + builder: (context, state) { + if (state.loading) { + return const Center( + child: CircularProgressIndicator(color: Color(0xffF95F62)), + ); + } - return SafeArea( - child: Scaffold( - backgroundColor: Colors.white, - body: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 20.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + final bloc = context.read(); + final now = DateTime.now(); - CommonAppBar(isWhiteLogo: false, isProfilePage: false, showDivider: true,), - backWidget(context, "Make Booking", Colors.black), - SizedBox( - height: 20.h, - ), + final bool hasStartDate = state.startDate != null; + final bool hasEndDate = state.endDate != null; + final bool bothSelected = hasStartDate && hasEndDate; - // 🏝 Title - Text( - title, - style: GoogleFonts.poppins( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: Colors.black, + return SafeArea( + child: Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: 20.w, vertical: 20.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonAppBar( + isWhiteLogo: false, + isProfilePage: false, + showDivider: true, ), - ), - SizedBox(height: 4.h), + backWidget(context, "Make Booking", Colors.black), + SizedBox(height: 20.h), - // 📄 Description - Text( - description, - style: GoogleFonts.poppins( - fontSize: 12.sp, - color: Colors.black54, + Text( + widget.title, + style: GoogleFonts.poppins( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), ), - ), - SizedBox(height: 24.h), + SizedBox(height: 4.h), - // 📅 Calendar Container - Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 10.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16.r), - boxShadow: [ - BoxShadow( - color: Colors.black12.withOpacity(0.06), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], + Text( + widget.description, + style: GoogleFonts.poppins( + fontSize: 12.sp, + color: Colors.black54, + ), ), - child: Column( - children: [ - Text( - "When are you visiting?", - style: GoogleFonts.poppins( - fontSize: 14.sp, - fontWeight: FontWeight.w500, - color: Colors.black, + SizedBox(height: 24.h), + + // ── Calendar Card ────────────────────────────────────── + Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + vertical: 12.h, horizontal: 10.w), + decoration: BoxDecoration( + color: Colors.white, + // VALIDATION 1: red border when user tapped Confirm without both dates + border: _showValidationError + ? Border.all( + color: Colors.red.shade300, width: 1.2) + : null, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: Colors.black12.withOpacity(0.06), + blurRadius: 12, + offset: const Offset(0, 4), ), - ), - SizedBox(height: 8.h), - - // 🗓 SfDateRangePicker - SfDateRangePicker( - view: DateRangePickerView.month, - selectionMode: DateRangePickerSelectionMode.range, - minDate: now, - maxDate: now.add(const Duration(days: 365)), - enablePastDates: false, - backgroundColor: Colors.white, - showNavigationArrow: true, - - // ✅ Put the background color here - headerStyle: DateRangePickerHeaderStyle( - backgroundColor: Colors.white, // <-- removes the purple strip - textAlign: TextAlign.center, - textStyle: GoogleFonts.poppins( - fontSize: 13.sp, - fontWeight: FontWeight.w600, - color: Colors.black, - ), + ], ), - - monthViewSettings: DateRangePickerMonthViewSettings( - firstDayOfWeek: 7, - viewHeaderStyle: DateRangePickerViewHeaderStyle( - textStyle: GoogleFonts.poppins( - color: Colors.grey.shade600, - fontSize: 11.sp, - fontWeight: FontWeight.w500, - ), - ), - blackoutDates: _getUnavailableDates(state.availableDates, now), - ), - - monthCellStyle: DateRangePickerMonthCellStyle( - textStyle: GoogleFonts.poppins(fontSize: 12.sp, color: Colors.black87), - todayTextStyle: GoogleFonts.poppins( - fontSize: 12.sp, color: Colors.black, fontWeight: FontWeight.w500), - blackoutDateTextStyle: GoogleFonts.poppins( - fontSize: 12.sp, color: Colors.grey.shade400, - decoration: TextDecoration.lineThrough), - ), - - rangeTextStyle: GoogleFonts.poppins( - fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.w500), - startRangeSelectionColor: const Color(0xffFF5A5F), - endRangeSelectionColor: const Color(0xffFF5A5F), - rangeSelectionColor: const Color(0xffFF5A5F).withOpacity(0.15), - selectionTextStyle: GoogleFonts.poppins( - fontSize: 12.sp, color: Colors.white, fontWeight: FontWeight.w500), - - initialSelectedRange: state.startDate != null && state.endDate != null - ? PickerDateRange(state.startDate, state.endDate) - : null, - onSelectionChanged: (DateRangePickerSelectionChangedArgs args) { - if (args.value is PickerDateRange) { - final start = args.value.startDate; - final end = args.value.endDate; - if (start != null && end != null) { - bloc.add(SelectDate(start, end)); - } - } - }, - ), - ], - ), - ), - - SizedBox(height: 40.h), - - // ✅ Confirm button - GestureDetector( - onTap: () { - if (state.startDate != null && state.endDate != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Booking confirmed from " - "${state.startDate!.toLocal().toString().split(' ')[0]} " - "to ${state.endDate!.toLocal().toString().split(' ')[0]}", + child: Column( + children: [ + Text( + "When are you visiting?", + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: Colors.black, ), ), - ); - Navigator.of(context).pushNamed(RouteConstants.bookingSuccessful); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please select a valid date range"), + SizedBox(height: 8.h), + + SfDateRangePicker( + view: DateRangePickerView.month, + selectionMode: + DateRangePickerSelectionMode.range, + minDate: now.add(const Duration(days: 1)), + maxDate: state.validUptoDate ?? + now.add(const Duration(days: 365)), + enablePastDates: false, + backgroundColor: Colors.white, + showNavigationArrow: true, + headerStyle: DateRangePickerHeaderStyle( + backgroundColor: Colors.white, + textAlign: TextAlign.center, + textStyle: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + ), + monthViewSettings: + DateRangePickerMonthViewSettings( + firstDayOfWeek: 7, + viewHeaderStyle: + DateRangePickerViewHeaderStyle( + textStyle: GoogleFonts.poppins( + color: Colors.grey.shade600, + fontSize: 11.sp, + fontWeight: FontWeight.w500, + ), + ), + blackoutDates: const [], + ), + monthCellStyle: DateRangePickerMonthCellStyle( + textStyle: GoogleFonts.poppins( + fontSize: 12.sp, color: Colors.black87), + todayTextStyle: GoogleFonts.poppins( + fontSize: 12.sp, + color: Colors.black, + fontWeight: FontWeight.w500), + blackoutDateTextStyle: GoogleFonts.poppins( + fontSize: 12.sp, + color: Colors.grey.shade400, + decoration: TextDecoration.lineThrough), + ), + rangeTextStyle: GoogleFonts.poppins( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.w500), + startRangeSelectionColor: + const Color(0xffFF5A5F), + endRangeSelectionColor: + const Color(0xffFF5A5F), + rangeSelectionColor: + const Color(0xffFF5A5F).withOpacity(0.15), + selectionTextStyle: GoogleFonts.poppins( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.w500), + initialSelectedRange: bothSelected + ? PickerDateRange( + state.startDate, state.endDate) + : null, + + // VALIDATION 2: detect partial vs full selection + onSelectionChanged: + (DateRangePickerSelectionChangedArgs args) { + if (args.value is PickerDateRange) { + final start = args.value.startDate; + final end = args.value.endDate; + + if (start != null && end != null) { + // ✅ Both selected — clear all error states + setState(() { + _onlyStartSelected = false; + _showValidationError = false; + }); + bloc.add(SelectDate(start, end)); + } else if (start != null && end == null) { + // ⚠️ Only start tapped — guide user to pick end + setState(() { + _onlyStartSelected = true; + _showValidationError = false; + }); + } else { + // Selection cleared + setState(() { + _onlyStartSelected = false; + }); + } + } + }, ), - ); - } - }, - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 14.h), - decoration: BoxDecoration( - color: const Color(0xffFF5A5F), - borderRadius: BorderRadius.circular(30.r), + + // VALIDATION 3: inline hint message below calendar + if (_onlyStartSelected || _showValidationError) + Padding( + padding: EdgeInsets.only( + top: 8.h, left: 4.w, right: 4.w), + child: Row( + children: [ + Icon( + Icons.info_outline_rounded, + size: 14.sp, + // orange for guidance, red for submit error + color: _showValidationError + ? Colors.red + : Colors.orange.shade700, + ), + SizedBox(width: 6.w), + Expanded( + child: Text( + _showValidationError && !hasStartDate + ? "Please select a check-in date to continue" + : _showValidationError && + hasStartDate && + !hasEndDate + ? "Please also select a check-out date" + : "Now tap an end date to complete your range", + style: GoogleFonts.poppins( + fontSize: 11.sp, + color: _showValidationError + ? Colors.red + : Colors.orange.shade700, + ), + ), + ), + ], + ), + ), + ], ), - child: Center( - child: Text( - "Confirm Booking", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, + ), + + // VALIDATION 4: selected range summary chip (only when both selected) + if (bothSelected) ...[ + SizedBox(height: 12.h), + Center( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 14.w, vertical: 8.h), + decoration: BoxDecoration( + color: + const Color(0xffFF5A5F).withOpacity(0.08), + borderRadius: BorderRadius.circular(10.r), + border: Border.all( + color: const Color(0xffFF5A5F) + .withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.check_circle_outline_rounded, + size: 15.sp, + color: const Color(0xffFF5A5F)), + SizedBox(width: 6.w), + Text( + "${_fmt(state.startDate!)} → ${_fmt(state.endDate!)}", + style: GoogleFonts.poppins( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: const Color(0xffFF5A5F), + ), + ), + ], + ), + ), + ), // close Center + ], + + SizedBox(height: 40.h), + + // ── Confirm Button ───────────────────────────────────── + GestureDetector( + onTap: state.isConfirming + ? null + : () { + // VALIDATION 5: check both dates before firing API + if (!hasStartDate || !hasEndDate) { + setState(() { + _showValidationError = true; + _onlyStartSelected = false; + }); + return; // stop here — don't call API + } + // ✅ Both dates present — dispatch to bloc + bloc.add(ConfirmBooking( + attractionId: widget.attractionId, + bookingId: widget.bookingId, + startDate: state.startDate!, + endDate: state.endDate!, + )); + }, + child: Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 14.h), + decoration: BoxDecoration( + color: state.isConfirming + ? const Color(0xffFF5A5F).withOpacity(0.6) + : const Color(0xffFF5A5F), + borderRadius: BorderRadius.circular(30.r), + ), + child: Center( + child: state.isConfirming + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + "Confirm Booking", + style: GoogleFonts.poppins( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w600, + ), ), ), ), ), - ), - ], + ], + ), ), ), - ), - ); - }, + ); + }, + ), ), ); } - /// Marks unavailable days (those not in availableDates) as blackout - List _getUnavailableDates(List available, DateTime start) { - final end = start.add(const Duration(days: 365)); - final allDays = List.generate( - end.difference(start).inDays, - (i) => DateTime(start.year, start.month, start.day + i), - ); - - return allDays - .where((day) => !available.any((a) => - a.year == day.year && a.month == day.month && a.day == day.day)) - .toList(); + /// "20 Mar 2026" + String _fmt(DateTime d) { + const months = [ + 'Jan','Feb','Mar','Apr','May','Jun', + 'Jul','Aug','Sep','Oct','Nov','Dec' + ]; + return '${d.day} ${months[d.month - 1]} ${d.year}'; } -} +} \ No newline at end of file diff --git a/lib/my_pass/views/booking_successful_page_view.dart b/lib/my_pass/views/booking_successful_page_view.dart index 9a85c55..542ce3f 100644 --- a/lib/my_pass/views/booking_successful_page_view.dart +++ b/lib/my_pass/views/booking_successful_page_view.dart @@ -6,7 +6,10 @@ import 'package:google_fonts/google_fonts.dart'; import '../../common_packages/back_widget.dart'; class BookingSuccessfulPageView extends StatelessWidget { - const BookingSuccessfulPageView({super.key}); + + final String message; + + const BookingSuccessfulPageView({super.key, required this.message}); @override Widget build(BuildContext context) { @@ -39,7 +42,7 @@ class BookingSuccessfulPageView extends StatelessWidget { SizedBox(height: 20.h), Text( - "Your booking has been Confirmed on 08/01/2025", + message, textAlign: TextAlign.center, style: TextStyle( fontSize: 16.sp, diff --git a/lib/my_pass/views/pass_attraction_details_view.dart b/lib/my_pass/views/pass_attraction_details_view.dart index fb967ac..50ee390 100644 --- a/lib/my_pass/views/pass_attraction_details_view.dart +++ b/lib/my_pass/views/pass_attraction_details_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:citycards_customer/attraction_details/widgets/share_bottomsheet.dart'; import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; @@ -7,30 +9,92 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:latlong2/latlong.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; -import '../../attraction_details/bloc/attraction_details_bloc.dart'; -import '../../attraction_details/bloc/attraction_details_event.dart'; -import '../../attraction_details/bloc/attraction_details_state.dart'; -import '../../attraction_details/repository/attraction_details_repository.dart'; import '../../core/route_constants.dart'; +import '../blocs/myPassesAttractionDetails/pass_attraction_details_bloc.dart'; +import '../repository/pass_attraction_details_repository.dart'; +import '../widgets/check_in_bottom_sheet.dart'; +import '../widgets/how_to_redeem_bottomsheet.dart'; -class PassAttractionDetailsView extends StatelessWidget { - final int? attractionId; +class PassAttractionDetailsView extends StatefulWidget { + final int attractionId; + final int bookingId; const PassAttractionDetailsView({ super.key, required this.attractionId, + required this.bookingId, }); + @override + State createState() => + _PassAttractionDetailsViewState(); +} + +class _PassAttractionDetailsViewState extends State { + bool _isCheckedIn = false; + int _remainingSeconds = 0; + Timer? _countdownTimer; + + @override + void dispose() { + _countdownTimer?.cancel(); + super.dispose(); + } + + void _startCountdown(int minutes) { + _countdownTimer?.cancel(); + setState(() { + _isCheckedIn = true; + _remainingSeconds = minutes * 60; + }); + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) { + timer.cancel(); + return; + } + setState(() { + if (_remainingSeconds > 0) { + _remainingSeconds--; + } else { + timer.cancel(); + if (!mounted) return; + setState(() { + _isCheckedIn = false; + }); + context.read().add( + FetchPassAttractionDetailsEvent( + attractionId: widget.attractionId, + bookingId: widget.bookingId, + ), + ); + } + }); + }); + } + + String get _timerLabel { + final m = _remainingSeconds ~/ 60; + final s = _remainingSeconds % 60; + return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; + } + @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => AttractionDetailsBloc( - repository: AttractionDetailsRepository(), - )..add(FetchAttractionDetails(attractionId: attractionId??0)), - child: BlocBuilder( + create: (_) => + PassAttractionDetailsBloc( + repository: PassAttractionDetailsRepository(), + )..add( + FetchPassAttractionDetailsEvent( + attractionId: widget.attractionId ?? 0, + bookingId: widget.bookingId ?? 0, + ), + ), + child: BlocBuilder( builder: (context, state) { - if (state is AttractionDetailsLoading) { + if (state is PassAttractionDetailsLoading) { return Scaffold( backgroundColor: Colors.white, body: Center( @@ -39,25 +103,22 @@ class PassAttractionDetailsView extends StatelessWidget { ); } - if (state is AttractionDetailsError) { + if (state is PassAttractionDetailsError) { return Scaffold( backgroundColor: Colors.white, body: Center( - child: Text( - state.message, - style: TextStyle(color: Colors.red), - ), + child: Text(state.message, style: TextStyle(color: Colors.red)), ), ); } - if (state is AttractionDetailsLoaded) { + if (state is PassAttractionDetailsLoaded) { final attraction = state.attractionDetails; final coverImage = attraction.attractionGalleries .firstWhere( (gallery) => gallery.isCoverImage, - orElse: () => attraction.attractionGalleries.first, - ) + orElse: () => attraction.attractionGalleries.first, + ) .filePathUrl; return Scaffold( @@ -90,7 +151,9 @@ class PassAttractionDetailsView extends StatelessWidget { child: SafeArea( child: Padding( padding: EdgeInsets.symmetric( - horizontal: 20.w, vertical: 10.h), + horizontal: 20.w, + vertical: 10.h, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -133,7 +196,8 @@ class PassAttractionDetailsView extends StatelessWidget { Positioned( bottom: 31.h, left: 12.w, - right: 60.w, // Add this - leaves space for share button + right: 60 + .w, // Add this - leaves space for share button child: Text( attraction.title, style: TextStyle( @@ -177,7 +241,10 @@ class PassAttractionDetailsView extends StatelessWidget { ), Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h), + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 24.h, + ), child: Container( width: double.infinity, padding: EdgeInsets.all(20.w), @@ -191,14 +258,48 @@ class PassAttractionDetailsView extends StatelessWidget { ), child: Column( children: [ + if (_isCheckedIn) + Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 6.h, + ), + decoration: BoxDecoration( + color: const Color( + 0xFFF95F62, + ).withOpacity(0.1), + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: const Color(0xFFF95F62), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.access_time_rounded, + color: const Color(0xFFF95F62), + size: 16.sp, + ), + SizedBox(width: 6.w), + Text( + _timerLabel, + style: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.w700, + color: const Color(0xFFF95F62), + ), + ), + ], + ), + ), + Text( "Scan this at the site of the attraction", style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, color: Color(0xFFF95F62), ), - textAlign: TextAlign.center, ), SizedBox(height: 20.h), // QR Code Image @@ -208,11 +309,11 @@ class PassAttractionDetailsView extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(12.r), ), - child: Image.asset( - 'assets/images/qr_image.png', - height: 200.h, - width: 200.w, - fit: BoxFit.contain, + child: QrImageView( + data: + "Details:\nQR No. : ${attraction.qr.qrNumber}\nQR Code : ${attraction.qr.qrCode}\nStatus : ${attraction.qr.qrStatus}\nExpires At: ${attraction.qr.qrExpiresAt}\nChecked In: ${attraction.qr.checkedInDatetime}\nRemaining : ${attraction.qr.qrRemainingMinutes} mins\nIs Active : ${attraction.qr.isQrActive ? "Yes" : "No"}", + version: QrVersions.auto, + size: 200.w, ), ), SizedBox(height: 16.h), @@ -221,7 +322,7 @@ class PassAttractionDetailsView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - "IYFHHVN254ADSD", + attraction.qr.qrNumber, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, @@ -232,10 +333,18 @@ class PassAttractionDetailsView extends StatelessWidget { SizedBox(width: 8.w), GestureDetector( onTap: () { - Clipboard.setData(ClipboardData(text: "IYFHHVN254ADSD")); - ScaffoldMessenger.of(context).showSnackBar( + Clipboard.setData( + ClipboardData( + text: attraction.qr.qrNumber, + ), + ); + ScaffoldMessenger.of( + context, + ).showSnackBar( SnackBar( - content: Text('Code copied to clipboard'), + content: Text( + 'Code copied to clipboard', + ), duration: Duration(seconds: 2), backgroundColor: Color(0xFFF95F62), ), @@ -251,27 +360,92 @@ class PassAttractionDetailsView extends StatelessWidget { ), SizedBox(height: 20.h), // Check in Button + // AFTER SizedBox( width: double.infinity, height: 50.h, child: ElevatedButton( - onPressed: () { - // Add your check-in logic here - }, + onPressed: _isCheckedIn + ? null // ← not tappable after check-in + : () async { + final result = + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical( + top: Radius.circular( + 20.r, + ), + ), + ), + builder: (_) => + CheckInBottomSheet( + attractionName: + attraction.title, + minuteTime: attraction + .qr + .qrRemainingMinutes, + bookingId: + widget.bookingId, + attractionId: + widget.attractionId, + ), + ); + if (result == true) { + context + .read< + PassAttractionDetailsBloc + >() + .add( + FetchPassAttractionDetailsEvent( + attractionId: + widget.attractionId, + bookingId: widget.bookingId, + ), + ); + _startCountdown( + attraction.qr.qrRemainingMinutes, + ); + } + }, style: ElevatedButton.styleFrom( - backgroundColor: Color(0xFFF95F62), + backgroundColor: _isCheckedIn + ? Colors + .grey + .shade400 // ← greyed out + : const Color(0xFFF95F62), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25.r), ), elevation: 0, + disabledBackgroundColor: + Colors.grey.shade400, ), - child: Text( - "Check in", - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_isCheckedIn) ...[ + Icon( + Icons.check_circle_outline, + color: Colors.white, + size: 18.sp, + ), + SizedBox(width: 8.w), + ], + Text( + _isCheckedIn + ? "Checked In" + : "Check in", + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], ), ), ), @@ -289,7 +463,18 @@ class PassAttractionDetailsView extends StatelessWidget { ), GestureDetector( onTap: () { - // Add your help/support navigation here + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20.r), + ), + ), + builder: (_) => HowToRedeemBottomSheet( + attractionName: attraction.title, + ), + ); }, child: Text( "Click Here", @@ -310,8 +495,7 @@ class PassAttractionDetailsView extends StatelessWidget { // About Section Padding( - padding: - EdgeInsets.only(left: 16.w, right: 16.w,), + padding: EdgeInsets.only(left: 16.w, right: 16.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -371,7 +555,7 @@ class PassAttractionDetailsView extends StatelessWidget { Expanded( child: Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ CustomText( text: "Contact Number", @@ -381,7 +565,9 @@ class PassAttractionDetailsView extends StatelessWidget { ), SizedBox(height: 6.h), CustomText( - text: attraction.bookingPhoneNumber??"N/A", + text: + attraction.bookingPhoneNumber ?? + "N/A", color: Colors.black, size: 14.sp, weight: FontWeight.w600, @@ -420,7 +606,7 @@ class PassAttractionDetailsView extends StatelessWidget { Expanded( child: Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ CustomText( text: "Email", @@ -430,7 +616,8 @@ class PassAttractionDetailsView extends StatelessWidget { ), SizedBox(height: 6.h), CustomText( - text: attraction.bookingEmail??"N/A", + text: + attraction.bookingEmail ?? "N/A", color: Colors.black, size: 14.sp, weight: FontWeight.w600, @@ -451,8 +638,16 @@ class PassAttractionDetailsView extends StatelessWidget { SizedBox(height: 16.h), InkWell( onTap: () { - Navigator.of(context) - .pushNamed(RouteConstants.makeBooking); + Navigator.of(context).pushNamed( + RouteConstants.makeBooking, + arguments: { + "title": attraction.title, + "description": attraction.description, + "validUpto": attraction.qr.validUpto, + "attractionId": attraction.id, + "bookingId": widget.bookingId, + }, + ); }, child: Container( padding: EdgeInsets.symmetric( @@ -465,12 +660,12 @@ class PassAttractionDetailsView extends StatelessWidget { ), child: Row( mainAxisAlignment: - MainAxisAlignment.spaceBetween, + MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ CustomText( text: "Via CityCards", @@ -516,11 +711,11 @@ class PassAttractionDetailsView extends StatelessWidget { .where((inclusion) => inclusion.isInclusion) .map( (inclusion) => includedBox( - "assets/icons/bus.png", - inclusion.title, - inclusion.description, - ), - ) + "assets/icons/bus.png", + inclusion.title, + inclusion.description, + ), + ) .toList(), ), SizedBox(height: 30.h), @@ -559,13 +754,17 @@ class PassAttractionDetailsView extends StatelessWidget { ), initialZoom: 15.0, interactionOptions: InteractionOptions( - flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + flags: + InteractiveFlag.all & + ~InteractiveFlag.rotate, ), ), children: [ TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'com.example.citycards_customer', + urlTemplate: + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: + 'com.example.citycards_customer', ), MarkerLayer( markers: [ @@ -616,7 +815,6 @@ class PassAttractionDetailsView extends StatelessWidget { ); }).toList(), ), - ], ), ), @@ -630,9 +828,7 @@ class PassAttractionDetailsView extends StatelessWidget { return Scaffold( backgroundColor: Colors.white, - body: Center( - child: Text("Something went wrong"), - ), + body: Center(child: Text("Something went wrong")), ); }, ), @@ -680,10 +876,7 @@ class PassAttractionDetailsView extends StatelessWidget { ); } - Widget faqBox({ - required String title, - required String desc, - }) { + Widget faqBox({required String title, required String desc}) { return Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), decoration: BoxDecoration( @@ -713,13 +906,9 @@ class PassAttractionDetailsView extends StatelessWidget { ], ), SizedBox(height: 9.h), - CustomText( - text: desc, - size: 11.sp, - color: const Color(0xFF7D7D7D), - ), + CustomText(text: desc, size: 11.sp, color: const Color(0xFF7D7D7D)), ], ), ); } -} \ No newline at end of file +} diff --git a/lib/my_pass/views/pass_attractions_page_view.dart b/lib/my_pass/views/pass_attractions_page_view.dart index 90a5136..1af7a5c 100644 --- a/lib/my_pass/views/pass_attractions_page_view.dart +++ b/lib/my_pass/views/pass_attractions_page_view.dart @@ -14,11 +14,13 @@ import '../repository/my_passes_attractions_repository.dart'; class PassAttractionsPage extends StatelessWidget { final int cityXid; + final int bookingId; final String source; const PassAttractionsPage({ super.key, required this.cityXid, + required this.bookingId, required this.source, }); @@ -156,7 +158,7 @@ class PassAttractionsPage extends StatelessWidget { children: state.filteredAttractions .map( (attraction) => PassAttractionCard( - attraction: attraction, + attraction: attraction, bookingId: bookingId, ), ) .toList(), diff --git a/lib/my_pass/views/pass_details_page_view.dart b/lib/my_pass/views/pass_details_page_view.dart index e806e05..614bb6f 100644 --- a/lib/my_pass/views/pass_details_page_view.dart +++ b/lib/my_pass/views/pass_details_page_view.dart @@ -229,7 +229,10 @@ class _PassDetailsViewState extends State { onTap: () { Navigator.of(context).pushNamed( RouteConstants.passAttractionDetails, - arguments: attraction.id, + arguments: { + 'attractionId': attraction.id, + 'bookingId': widget.bookingId, // pass your actual bookingId here + }, ); }, child: _attractionCard( @@ -261,7 +264,7 @@ class _PassDetailsViewState extends State { Navigator.pushNamed( context, RouteConstants.passAttractionsPage, - arguments: {'cityId': city?.id, 'source': 'my_passes'}, + arguments: {'cityId': city?.id, 'source': 'my_passes', 'bookingId': widget.bookingId}, ); }), diff --git a/lib/my_pass/widgets/check_in_bottom_sheet.dart b/lib/my_pass/widgets/check_in_bottom_sheet.dart new file mode 100644 index 0000000..bfa52b4 --- /dev/null +++ b/lib/my_pass/widgets/check_in_bottom_sheet.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../../common_packages/custom_filled_button.dart'; +import '../blocs/checkIn/check_in_bloc.dart'; +import '../repository/check_in_repository.dart'; + +class CheckInBottomSheet extends StatelessWidget { + final String attractionName; + final int minuteTime; + final int bookingId; + final int attractionId; + + const CheckInBottomSheet({ + super.key, + required this.attractionName, + required this.minuteTime, + required this.bookingId, + required this.attractionId, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CheckInBloc(checkInRepository: CheckInRepository()), + child: BlocConsumer( + listener: (context, state) { + if (state is CheckInSuccess) { + Navigator.pop(context, true); // close sheet + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Checked In Successful"), + backgroundColor: const Color(0xFF22C55E), + behavior: SnackBarBehavior.floating, + ), + ); + } else if (state is CheckInFailure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.error), + backgroundColor: const Color(0xFFF95F62), + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + builder: (context, state) { + final isLoading = state is CheckInLoading; + + return AnimatedPadding( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + padding: EdgeInsets.only( + top: 24.h, + left: 20.w, + right: 20.w, + bottom: MediaQuery.of(context).viewInsets.bottom + 24.h, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + /// --- Drag Handle --- + Container( + height: 4.h, + width: 40.w, + decoration: BoxDecoration( + color: const Color(0xFF2D3134), + borderRadius: BorderRadius.circular(4.r), + ), + ), + SizedBox(height: 20.h), + + /// --- Title --- + CustomText( + text: "Ready to check in?", + size: 22.sp, + weight: FontWeight.w700, + ), + SizedBox(height: 16.h), + + /// --- Subtitle with attraction name highlighted --- + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xFF6B7280), + height: 1.5, + ), + children: [ + const TextSpan( + text: "Only activate when you are at the entrance of ", + ), + TextSpan( + text: "$attractionName.", + style: TextStyle( + color: const Color(0xFFF95F62), + fontWeight: FontWeight.w700, + fontSize: 15.sp, + ), + ), + ], + ), + ), + SizedBox(height: 20.h), + + /// --- Timer Info Card --- + Container( + width: double.infinity, + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 14.h, + ), + decoration: BoxDecoration( + color: const Color(0xFFF95F62).withOpacity(0.08), + borderRadius: BorderRadius.circular(14.r), + border: Border.all(color: const Color(0xFFF95F62)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 36.h, + width: 36.w, + decoration: BoxDecoration( + color: const Color(0xFFF95F62).withOpacity(0.12), + shape: BoxShape.circle, + ), + child: Center( + child: Icon( + Icons.access_time_rounded, + color: const Color(0xFFF95F62), + size: 20.sp, + ), + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomText( + text: "$minuteTime minute timer", + size: 15.sp, + weight: FontWeight.w700, + color: const Color(0xFFF95F62), + ), + SizedBox(height: 4.h), + CustomText( + text: + "Once activated, the pass is valid for $minuteTime minutes. This action cannot be undone", + size: 11.sp, + weight: FontWeight.w400, + color: const Color(0xFFF95F62).withOpacity(0.8), + maxLines: 3, + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 24.h), + + /// --- Activate Button --- + isLoading + ? SizedBox( + height: 52.h, + child: const Center( + child: CircularProgressIndicator( + color: Color(0xFFF95F62), + ), + ), + ) + : CustomFilledButton( + label: "Activate Pass Now", + width: double.infinity, + height: 52.h, + showArrow: true, + onTap: () { + context.read().add( + DoCheckInEvent( + passId: bookingId, + attractionId: attractionId, + ), + ); + }, + ), + + SizedBox(height: 16.h), + + /// --- Dismiss Text Button --- + GestureDetector( + onTap: isLoading ? null : () => Navigator.pop(context), + child: CustomText( + text: "I'm not at the entrance yet", + size: 14.sp, + weight: FontWeight.w500, + color: isLoading + ? const Color(0xFFF95F62).withOpacity(0.4) + : const Color(0xFFF95F62), + ), + ), + ], + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/my_pass/widgets/how_to_redeem_bottomsheet.dart b/lib/my_pass/widgets/how_to_redeem_bottomsheet.dart new file mode 100644 index 0000000..c4f4a61 --- /dev/null +++ b/lib/my_pass/widgets/how_to_redeem_bottomsheet.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:citycards_customer/common_packages/custom_text.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../common_packages/custom_filled_button.dart'; +import '../../core/route_constants.dart'; + +class HowToRedeemBottomSheet extends StatelessWidget { + final String attractionName; + + const HowToRedeemBottomSheet({ + super.key, + required this.attractionName, + }); + + @override + Widget build(BuildContext context) { + return AnimatedPadding( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + padding: EdgeInsets.only( + top: 24.h, + left: 20.w, + right: 20.w, + bottom: MediaQuery.of(context).viewInsets.bottom + 24.h, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + /// --- Drag Handle --- + Container( + height: 4.h, + width: 40.w, + decoration: BoxDecoration( + color: const Color(0xFF2D3134), + borderRadius: BorderRadius.circular(4.r), + ), + ), + SizedBox(height: 20.h), + + /// --- Title --- + CustomText( + text: "How to redeem my attraction pass?", + size: 20.sp, + weight: FontWeight.w700, + textAlign: TextAlign.center, + ), + SizedBox(height: 20.h), + + /// --- Body with attraction name highlighted --- + RichText( + textAlign: TextAlign.start, + text: TextSpan( + style: GoogleFonts.poppins( + fontSize: 14.sp, + fontWeight: FontWeight.w400, + color: const Color(0xFF3D3D3D), + height: 1.6, + ), + children: [ + const TextSpan( + text: + "To redeem your attraction pass, present the QR code at the entrance. Our staff will scan it, granting you access to the wonders within. Enjoy your adventure at ", + ), + TextSpan( + text: "$attractionName!", + style: GoogleFonts.poppins( + color: const Color(0xFFF95F62), + fontWeight: FontWeight.w700, + fontSize: 14.sp, + ), + ), + ], + ), + ), + SizedBox(height: 24.h), + + /// --- Trouble text --- + CustomText( + text: "Having trouble redeeming the pass?", + size: 14.sp, + weight: FontWeight.w400, + color: Colors.black54, + textAlign: TextAlign.center, + ), + SizedBox(height: 16.h), + + /// --- Contact Support Button --- + CustomFilledButton( + label: "Contact Support", + width: double.infinity, + height: 52.h, + onTap: () { + Navigator.pushNamed(context, RouteConstants.contactUs); + }, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/my_pass/widgets/pass_attraction_card.dart b/lib/my_pass/widgets/pass_attraction_card.dart index b3f42c7..99fc8fc 100644 --- a/lib/my_pass/widgets/pass_attraction_card.dart +++ b/lib/my_pass/widgets/pass_attraction_card.dart @@ -8,7 +8,8 @@ import '../../core/route_constants.dart'; class PassAttractionCard extends StatelessWidget { final Attraction attraction; - const PassAttractionCard({super.key, required this.attraction}); + final int bookingId; + const PassAttractionCard({super.key, required this.attraction, required this.bookingId}); @override Widget build(BuildContext context) { @@ -36,7 +37,10 @@ class PassAttractionCard extends StatelessWidget { onTap: () { Navigator.of(context).pushNamed( RouteConstants.passAttractionDetails, - arguments: attraction.id, + arguments: { + 'attractionId': attraction.id, + 'bookingId': bookingId, // pass your actual bookingId here + }, ); }, child: Container( diff --git a/lib/networkApiServices/api_urls.dart b/lib/networkApiServices/api_urls.dart index 52592e0..2e1ee15 100644 --- a/lib/networkApiServices/api_urls.dart +++ b/lib/networkApiServices/api_urls.dart @@ -12,6 +12,7 @@ class ApiUrls { static const attractionsList = "$baseUrl/mobile/list/all"; static const passAttractionsList = "$baseUrl/mobile/passes/mobile/list"; static const attractionDetails = "$baseUrl/mobile/list"; + static const passAttractionDetails = "$baseUrl/mobile/passes/attractionDetail"; static const home = "$baseUrl/mobile"; static const faqPrivacyTerms = "$baseUrl/mobile/user/cms-data"; static const userProfile = "$baseUrl/mobile/user"; @@ -37,5 +38,7 @@ class ApiUrls { static const submitTicket = "$baseUrl/mobile/user/support"; static const createPostCard = "$baseUrl/mobile/postcards"; static const addToCartPasses = "$baseUrl/mobile/passes/add-to-cart"; + static const checkIn = "$baseUrl/mobile/passes/start-checkin"; + static const booking = "$baseUrl/mobile/passes/booking-date-confirm"; static const createItinerary = "$baseUrl/mobile/itinerary"; } diff --git a/lib/networkApiServices/network_api_services.dart b/lib/networkApiServices/network_api_services.dart index 73d79da..10a65ab 100644 --- a/lib/networkApiServices/network_api_services.dart +++ b/lib/networkApiServices/network_api_services.dart @@ -19,8 +19,8 @@ class NetworkApiService { NetworkApiService._internal() { _dio = Dio( BaseOptions( - connectTimeout: const Duration(seconds: 30), - receiveTimeout: const Duration(seconds: 30), + connectTimeout: const Duration(seconds: 60), + receiveTimeout: const Duration(seconds: 60), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', diff --git a/lib/postcard/blocs/postcard_creation_bloc.dart b/lib/postcard/blocs/postcard_creation_bloc.dart index 4742a4a..c091b44 100644 --- a/lib/postcard/blocs/postcard_creation_bloc.dart +++ b/lib/postcard/blocs/postcard_creation_bloc.dart @@ -239,6 +239,7 @@ class PostcardCreationBloc userProfileFullName: event.fullName, userProfileEmail: event.email, userProfilePhone: event.phone, + isdCode: event.isdCode, userProfileAddress: event.address, userProfileCity: event.city, userProfileState: event.state, diff --git a/lib/postcard/blocs/postcard_creation_events.dart b/lib/postcard/blocs/postcard_creation_events.dart index 5772fa5..8924947 100644 --- a/lib/postcard/blocs/postcard_creation_events.dart +++ b/lib/postcard/blocs/postcard_creation_events.dart @@ -82,6 +82,7 @@ class StoreUserProfileData extends PostcardCreationEvent { final String? fullName; final String? email; final String? phone; + final String? isdCode; final String? address; final String? city; final String? state; @@ -92,6 +93,7 @@ class StoreUserProfileData extends PostcardCreationEvent { this.fullName, this.email, this.phone, + this.isdCode, this.address, this.city, this.state, diff --git a/lib/postcard/blocs/postcard_creation_state.dart b/lib/postcard/blocs/postcard_creation_state.dart index 7ac9e34..d6fbf4e 100644 --- a/lib/postcard/blocs/postcard_creation_state.dart +++ b/lib/postcard/blocs/postcard_creation_state.dart @@ -15,6 +15,7 @@ class PostcardCreationState { final String? fullName; final String? emailId; final String? phoneNumber; + final String? isdCode; final String address; final String? city; final String? country; @@ -51,6 +52,7 @@ class PostcardCreationState { this.fullName, this.emailId, this.phoneNumber, + this.isdCode, this.city, this.country, this.state, @@ -86,6 +88,7 @@ class PostcardCreationState { String? fullName, String? emailId, String? phoneNumber, + String? isdCode, String? address, String? city, String? country, @@ -120,6 +123,7 @@ class PostcardCreationState { fullName: fullName ?? this.fullName, emailId: emailId ?? this.emailId, phoneNumber: phoneNumber ?? this.phoneNumber, + isdCode: isdCode ?? this.isdCode, address: address ?? this.address, city: city ?? this.city, country: country ?? this.country, diff --git a/lib/postcard/views/postcard_creation_page_view.dart b/lib/postcard/views/postcard_creation_page_view.dart index 5ea15fa..399dbb2 100644 --- a/lib/postcard/views/postcard_creation_page_view.dart +++ b/lib/postcard/views/postcard_creation_page_view.dart @@ -62,8 +62,9 @@ class PostcardCreationPage extends StatelessWidget { initialSenderFullName: state.isGift ? state.userProfileFullName : null, // ⬅️ ADD initialSenderCity: state.isGift ? state.userProfileCity : null, // ⬅️ ADD initialSenderCountry: state.isGift ? state.userProfileCountry : null, - initialSenderEmail: state.isGift ? state.userProfileEmail : null, - initialSenderPhone: state.isGift ? state.userProfilePhone : null, + initialSenderEmail: state.userProfileEmail, + initialSenderPhone: state.userProfilePhone, + initialSenderisdCode: state.isdCode, ); break; case PostcardStep.checkout: diff --git a/lib/postcard/views/postcard_purchase_form_page_view.dart b/lib/postcard/views/postcard_purchase_form_page_view.dart index 18e9481..5a2f133 100644 --- a/lib/postcard/views/postcard_purchase_form_page_view.dart +++ b/lib/postcard/views/postcard_purchase_form_page_view.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:intl/intl.dart'; +import 'package:geocoding/geocoding.dart'; import '../../common_packages/app_bar.dart'; import '../blocs/addToCartPostcard/add_to_cart_postcard_bloc.dart'; import '../blocs/addToCartPostcard/add_to_cart_postcard_event.dart'; @@ -20,10 +21,11 @@ class PostcardPurchaseFormPageView extends StatefulWidget { final String? initialState; final String? initialZipCode; final String? initialCountry; - final String? initialSenderFullName; // ⬅️ ADD - final String? initialSenderCity; // ⬅️ ADD + final String? initialSenderFullName; + final String? initialSenderCity; final String? initialSenderCountry; final String? initialSenderEmail; + final String? initialSenderisdCode; final String? initialSenderPhone; const PostcardPurchaseFormPageView({ @@ -31,6 +33,7 @@ class PostcardPurchaseFormPageView extends StatefulWidget { this.initialFullName, this.initialSenderEmail, this.initialSenderPhone, + this.initialSenderisdCode, this.initialAddress, this.initialCity, this.initialState, @@ -42,41 +45,46 @@ class PostcardPurchaseFormPageView extends StatefulWidget { }); @override - State createState() => _PostcardPurchaseFormPageViewState(); + State createState() => + _PostcardPurchaseFormPageViewState(); } -class _PostcardPurchaseFormPageViewState extends State { +class _PostcardPurchaseFormPageViewState + extends State { final _formKey = GlobalKey(); - + // Sender controllers final _senderFullNameController = TextEditingController(); final _senderCityController = TextEditingController(); final _senderEmailController = TextEditingController(); final _senderPhoneController = TextEditingController(); - String? _senderSelectedCountry; - // Controllers + final _senderCountryController = TextEditingController(); // ← was dropdown + + // Recipient controllers final _titleController = TextEditingController(); final _recipientFullNameController = TextEditingController(); final _recipientAddressController = TextEditingController(); final _recipientCityController = TextEditingController(); final _recipientZipCodeController = TextEditingController(); - String? _recipientSelectedCountry; - String? _recipientSelectedState; + final _recipientStateController = TextEditingController(); // ← was dropdown + final _recipientCountryController = TextEditingController(); // ← was dropdown + + // Zip auto-fill loading flag + bool _isZipLoading = false; @override void initState() { super.initState(); - // Initialize controllers with prefill values _recipientFullNameController.text = widget.initialFullName ?? ''; _recipientAddressController.text = widget.initialAddress ?? ''; _recipientCityController.text = widget.initialCity ?? ''; _recipientZipCodeController.text = widget.initialZipCode ?? ''; - _recipientSelectedState = widget.initialState; - _recipientSelectedCountry = widget.initialCountry; + _recipientStateController.text = widget.initialState ?? ''; + _recipientCountryController.text = widget.initialCountry ?? ''; _senderFullNameController.text = widget.initialSenderFullName ?? ''; _senderCityController.text = widget.initialSenderCity ?? ''; - _senderSelectedCountry = widget.initialSenderCountry; + _senderCountryController.text = widget.initialSenderCountry ?? ''; _senderEmailController.text = widget.initialSenderEmail ?? ''; _senderPhoneController.text = widget.initialSenderPhone ?? ''; } @@ -90,9 +98,40 @@ class _PostcardPurchaseFormPageViewState extends State _fetchLocationFromZip(String zip) async { + if (zip.trim().length < 4) return; + setState(() => _isZipLoading = true); + try { + final locations = await locationFromAddress(zip); + if (locations.isNotEmpty) { + final placemarks = await placemarkFromCoordinates( + locations.first.latitude, + locations.first.longitude, + ); + final place = placemarks.first; + setState(() { + _recipientCityController.text = place.locality ?? ''; + _recipientStateController.text = place.administrativeArea ?? ''; + _recipientCountryController.text = place.country ?? ''; + }); + } + } catch (e) { + debugPrint("Zip lookup failed: $e"); + } finally { + if (mounted) setState(() => _isZipLoading = false); + } + } + // ───────────────────────────────────────────────────────────────────────── + @override Widget build(BuildContext context) { return BlocBuilder( @@ -102,13 +141,9 @@ class _PostcardPurchaseFormPageViewState extends State( listener: (context, cartState) { if (cartState is AddToCartPostCardSuccess) { - // Update the postcard number in creation bloc creationBloc.add(UpdatePostcardNumber(cartState.pcNumber)); - - // Navigate to next step (checkout) creationBloc.add(GoToNextStep()); } else if (cartState is AddToCartPostCardFailure) { - // Show error message ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(cartState.message), @@ -132,13 +167,15 @@ class _PostcardPurchaseFormPageViewState extends State().add(GoToPreviousStep()); + context + .read() + .add(GoToPreviousStep()); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Row( children: [ - Icon(Icons.arrow_back, size: 20), + const Icon(Icons.arrow_back, size: 20), const SizedBox(width: 8), Text( "Back", @@ -151,6 +188,8 @@ class _PostcardPurchaseFormPageViewState extends State( builder: (context, cartState) { - final isLoading = cartState is AddToCartPostCardLoading; - final addToCartBloc = context.read(); + final isLoading = + cartState is AddToCartPostCardLoading; + final addToCartBloc = + context.read(); return SizedBox( width: double.infinity, @@ -390,50 +515,79 @@ class _PostcardPurchaseFormPageViewState extends State? onChanged, }) { return Padding( padding: const EdgeInsets.only(bottom: 18), @@ -516,6 +670,7 @@ class _PostcardPurchaseFormPageViewState extends State( - value: value, - decoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(vertical: 14, horizontal: 12), - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Color(0xffFDCDCE)), - borderRadius: BorderRadius.circular(8), - ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Color(0xffFDCDCE)), - borderRadius: BorderRadius.circular(8), - ), - errorBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.red), - borderRadius: BorderRadius.circular(8), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.red), - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon(Icons.keyboard_arrow_down, - color: Color(0xffFDCDCE)), - hint: Text( - hint, - style: GoogleFonts.poppins( - color: const Color(0xff999999), - fontSize: 14.sp, - ), - ), - items: label == "Country *" - ? const [ - DropdownMenuItem(value: "Australia", child: Text("Australia")), - ] - : label == "State *" - ? const [ - DropdownMenuItem(value: "New South Wales", child: Text("New South Wales")), - DropdownMenuItem(value: "Victoria", child: Text("Victoria")), - DropdownMenuItem(value: "Queensland", child: Text("Queensland")), - DropdownMenuItem(value: "South Australia", child: Text("South Australia")), - DropdownMenuItem(value: "Western Australia", child: Text("Western Australia")), - DropdownMenuItem(value: "Tasmania", child: Text("Tasmania")), - DropdownMenuItem(value: "Northern Territory", child: Text("Northern Territory")), - DropdownMenuItem(value: "Australian Capital Territory", child: Text("Australian Capital Territory")), - ] - : const [ - DropdownMenuItem(value: "Lorem Ipsum", child: Text("Lorem Ipsum")), - ], - onChanged: onChanged, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please select $label'; - } return null; }, ), diff --git a/lib/postcard/widgets/edit_post_card/your_details.dart b/lib/postcard/widgets/edit_post_card/your_details.dart index bc2aa47..f94cd4d 100644 --- a/lib/postcard/widgets/edit_post_card/your_details.dart +++ b/lib/postcard/widgets/edit_post_card/your_details.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:geocoding/geocoding.dart'; class EditYourdetails extends StatefulWidget { final TextEditingController fullNameController; @@ -18,6 +19,7 @@ class EditYourdetails extends StatefulWidget { final TextEditingController senderCityController; final String selectedSenderCountry; final Function(String) selectSenderCountry; + const EditYourdetails({ super.key, required this.fullNameController, @@ -41,51 +43,76 @@ class EditYourdetails extends StatefulWidget { } class _EditYourdetailsState extends State { - String? _selectedState; - String? _selectedCountry; - String? _selectedSenderCountry; + late TextEditingController _stateController; + late TextEditingController _countryController; + late TextEditingController _senderCountryController; - final List countries = ['Australia']; - - final List states = [ - 'New South Wales', - 'Victoria', - 'Queensland', - 'South Australia', - 'Western Australia', - 'Tasmania', - 'Northern Territory', - 'Australian Capital Territory', - ]; + bool _isZipLoading = false; @override void initState() { - setState(() { - _selectedState = states.contains(widget.selectedState) - ? widget.selectedState - : null; - _selectedCountry = countries.contains(widget.selectedCountry) - ? widget.selectedCountry - : null; - _selectedSenderCountry = countries.contains(widget.selectedSenderCountry) - ? widget.selectedSenderCountry - : null; - }); super.initState(); + _stateController = TextEditingController(text: widget.selectedState); + _countryController = TextEditingController(text: widget.selectedCountry); + _senderCountryController = + TextEditingController(text: widget.selectedSenderCountry); } + @override + void dispose() { + _stateController.dispose(); + _countryController.dispose(); + _senderCountryController.dispose(); + super.dispose(); + } + + // ── Zip → City, State, Country (mirrors CreateAccountView logic) ────────── + Future _fetchLocationFromZip(String zip) async { + if (zip.trim().length < 4) return; + setState(() => _isZipLoading = true); + try { + final locations = await locationFromAddress(zip); + if (locations.isNotEmpty) { + final placemarks = await placemarkFromCoordinates( + locations.first.latitude, + locations.first.longitude, + ); + final place = placemarks.first; + + final city = place.locality ?? ''; + final state = place.administrativeArea ?? ''; + final country = place.country ?? ''; + + setState(() { + widget.cityController.text = city; + _stateController.text = state; + _countryController.text = country; + }); + + // Notify parent of new values + if (state.isNotEmpty) widget.selectState(state); + if (country.isNotEmpty) widget.selectCountry(country); + } + } catch (e) { + debugPrint("Zip lookup failed: $e"); + } finally { + if (mounted) setState(() => _isZipLoading = false); + } + } + // ───────────────────────────────────────────────────────────────────────── + @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // At the top of the Column children list, BEFORE the existing fields: + // ── Sender section (only when isForSelf == false) ────────────────── if (!widget.isForSelf) ...[ Text( "Your Details", style: GoogleFonts.poppins( - color: Color(0XFF212121), + color: const Color(0XFF212121), fontSize: 18.sp, fontWeight: FontWeight.w500, ), @@ -94,7 +121,7 @@ class _EditYourdetailsState extends State { Text( "Enter your details as the sender of this postcard", style: GoogleFonts.poppins( - color: Color(0XFF000000).withValues(alpha: 0.6), + color: const Color(0XFF000000).withValues(alpha: 0.6), fontSize: 14.sp, fontWeight: FontWeight.w400, ), @@ -115,22 +142,22 @@ class _EditYourdetailsState extends State { onlyLetters: true, noSpace: true, ), - _buildDropdownField( + _buildInputField( label: "Country *", - hint: "Select your country", - value: _selectedSenderCountry, - items: countries, - onChanged: (val) { - setState(() => _selectedSenderCountry = val); - widget.selectSenderCountry(val!); - }, + hint: "Enter your country", + controller: _senderCountryController, + maxLength: 50, + onlyLetters: true, + onChanged: (val) => widget.selectSenderCountry(val), ), const SizedBox(height: 8), ], + + // ── Recipient / Self section ─────────────────────────────────────── Text( widget.isForSelf ? "Your Details" : "Recipient Details", style: GoogleFonts.poppins( - color: Color(0XFF212121), + color: const Color(0XFF212121), fontSize: 18.sp, fontWeight: FontWeight.w500, ), @@ -141,7 +168,7 @@ class _EditYourdetailsState extends State { ? "Enter your address to receive this postcard" : "Enter the address of the person who will receive this postcard", style: GoogleFonts.poppins( - color: Color(0XFF000000).withValues(alpha: 0.6), + color: const Color(0XFF000000).withValues(alpha: 0.6), fontSize: 14.sp, fontWeight: FontWeight.w400, ), @@ -160,8 +187,110 @@ class _EditYourdetailsState extends State { hint: "Enter the recipient's Address", controller: widget.addressController, maxLength: 50, - // noSpecialCharacters: true, ), + + // ── Zip Code with spinner + auto-fill hint ───────────────────────── + Padding( + padding: const EdgeInsets.only(bottom: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + text: 'Zip Code', + style: GoogleFonts.poppins( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: const Color(0xff1A1A1A), + ), + children: [ + TextSpan( + text: ' *', + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + color: Colors.red, + ), + ), + ], + ), + ), + const SizedBox(height: 6), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextFormField( + controller: widget.zipCodeController, + keyboardType: TextInputType.number, + maxLength: 6, + onChanged: _fetchLocationFromZip, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + decoration: InputDecoration( + hintText: "Enter the Zip Code you reside in", + counterText: "", + hintStyle: GoogleFonts.poppins( + color: const Color(0xff999999), + fontSize: 14.sp, + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 14, horizontal: 12), + enabledBorder: OutlineInputBorder( + borderSide: + const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: + const BorderSide(color: Color(0xffFDCDCE)), + borderRadius: BorderRadius.circular(8), + ), + errorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.red), + borderRadius: BorderRadius.circular(8), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter Zip Code'; + } + return null; + }, + ), + ), + if (_isZipLoading) + const Padding( + padding: EdgeInsets.only(left: 10), + child: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFC83B61), + ), + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + "City, State & Country will auto-fill from zip", + style: TextStyle( + fontSize: 10.sp, + color: const Color(0xFF8E8E8E), + ), + ), + ], + ), + ), + + // ── City, State, Country — auto-filled but still editable ────────── _buildInputField( label: "City *", hint: "Enter the name of your city", @@ -169,36 +298,21 @@ class _EditYourdetailsState extends State { maxLength: 50, onlyLetters: true, ), - _buildDropdownField( - label: "Country *", - hint: "Select your country", - value: _selectedCountry, - items: countries, - onChanged: (val) { - setState(() { - _selectedCountry = val; - }); - widget.selectCountry(val!); - }, - ), - _buildDropdownField( + _buildInputField( label: "State *", - hint: "Select your state", - value: _selectedState, - items: states, - onChanged: (val) { - setState(() { - _selectedState = val; - }); - widget.selectState(val!); - }, + hint: "Enter your state", + controller: _stateController, + maxLength: 50, + onlyLetters: true, + onChanged: (val) => widget.selectState(val), ), _buildInputField( - label: "Zip Code *", - hint: "Enter the Zip Code you reside in", - controller: widget.zipCodeController, - keyboardType: TextInputType.number, - maxLength: 6, + label: "Country *", + hint: "Enter your country", + controller: _countryController, + maxLength: 50, + onlyLetters: true, + onChanged: (val) => widget.selectCountry(val), ), ], ); @@ -217,7 +331,8 @@ class _EditYourdetailsState extends State { bool onlyLetters = false, bool noSpace = false, bool isFirstLetterCapital = false, - bool noSpecialCharacters = false, // ✅ NEW + bool noSpecialCharacters = false, + ValueChanged? onChanged, }) { return Padding( padding: const EdgeInsets.only(bottom: 18), @@ -249,35 +364,20 @@ class _EditYourdetailsState extends State { const SizedBox(height: 6), TextFormField( controller: controller, + onChanged: onChanged, keyboardType: keyboardType ?? - (isMobileNumber - ? TextInputType.phone - : TextInputType.text), + (isMobileNumber ? TextInputType.phone : TextInputType.text), maxLength: maxLength ?? (isMobileNumber ? mobileLength : null), textCapitalization: isFirstLetterCapital ? TextCapitalization.words : TextCapitalization.none, inputFormatters: [ - if (isMobileNumber) - FilteringTextInputFormatter.digitsOnly, - + if (isMobileNumber) FilteringTextInputFormatter.digitsOnly, if (onlyLetters) - FilteringTextInputFormatter.allow( - RegExp(r'[a-zA-Z ]'), - ), - - if (noSpace) - FilteringTextInputFormatter.deny( - RegExp(r'\s'), - ), - - // ✅ NO SPECIAL CHARACTERS + FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z ]')), + if (noSpace) FilteringTextInputFormatter.deny(RegExp(r'\s')), if (noSpecialCharacters) - FilteringTextInputFormatter.allow( - RegExp(r'[a-zA-Z0-9 ]'), - ), - - // ✅ Capitalize first letter of each word + FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 ]')), if (isFirstLetterCapital) TextInputFormatter.withFunction((oldValue, newValue) { if (newValue.text.isEmpty) return newValue; @@ -327,7 +427,6 @@ class _EditYourdetailsState extends State { if (value == null || value.trim().isEmpty) { return 'Please enter $label'; } - if (isEmail) { final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); @@ -335,7 +434,6 @@ class _EditYourdetailsState extends State { return 'Please enter a valid email address'; } } - if (isMobileNumber) { if (!RegExp(r'^\d+$').hasMatch(value)) { return 'Only numbers are allowed'; @@ -344,24 +442,19 @@ class _EditYourdetailsState extends State { return 'Mobile number must be $mobileLength digits'; } } - if (onlyLetters) { if (!RegExp(r'^[a-zA-Z ]+$').hasMatch(value)) { return 'Only letters are allowed'; } } - if (noSpace && value.contains(' ')) { return 'Spaces are not allowed'; } - - // ✅ VALIDATION FOR SPECIAL CHARACTERS if (noSpecialCharacters) { if (!RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(value)) { return 'Special characters are not allowed'; } } - return null; }, ), @@ -369,92 +462,4 @@ class _EditYourdetailsState extends State { ), ); } - - - - Widget _buildDropdownField({ - required String label, - required String hint, - required String? value, - required List items, - required Function(String?) onChanged, - }) { - return Padding( - padding: const EdgeInsets.only(bottom: 18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - text: label.replaceAll(' *', ''), - style: GoogleFonts.poppins( - fontSize: 13.sp, - fontWeight: FontWeight.w500, - color: const Color(0xff1A1A1A), - ), - children: label.contains('*') - ? [ - TextSpan( - text: ' *', - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w500, - color: Colors.red, - ), - ), - ] - : [], - ), - ), - const SizedBox(height: 6), - DropdownButtonFormField( - value: value, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric( - vertical: 14, - horizontal: 12, - ), - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Color(0xffFDCDCE)), - borderRadius: BorderRadius.circular(8), - ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Color(0xffFDCDCE)), - borderRadius: BorderRadius.circular(8), - ), - errorBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.red), - borderRadius: BorderRadius.circular(8), - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.red), - borderRadius: BorderRadius.circular(8), - ), - ), - icon: const Icon( - Icons.keyboard_arrow_down, - color: Color(0xffFDCDCE), - ), - hint: Text( - hint, - style: GoogleFonts.poppins( - color: const Color(0xff999999), - fontSize: 14.sp, - ), - ), - items: items.map((String item) { - return DropdownMenuItem(value: item, child: Text(item)); - }).toList(), - onChanged: onChanged, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please select $label'; - } - return null; - }, - ), - ], - ), - ); - } -} +} \ No newline at end of file diff --git a/lib/postcard/widgets/purchase_details_bottom_sheet.dart b/lib/postcard/widgets/purchase_details_bottom_sheet.dart index d0f3ed7..30044eb 100644 --- a/lib/postcard/widgets/purchase_details_bottom_sheet.dart +++ b/lib/postcard/widgets/purchase_details_bottom_sheet.dart @@ -231,6 +231,7 @@ class PurchaseDetailsBottomSheet { state: profile.stateName, zipCode: profile.zipCode, country: profile.country, + isdCode: profile.isdCode, )); } diff --git a/lib/profile/bloc/contactUs/contact_us_bloc.dart b/lib/profile/bloc/contactUs/contact_us_bloc.dart index caca266..8e879a0 100644 --- a/lib/profile/bloc/contactUs/contact_us_bloc.dart +++ b/lib/profile/bloc/contactUs/contact_us_bloc.dart @@ -21,6 +21,7 @@ class ContactUsBloc extends Bloc { final response = await repository.submitTicket( firstName: event.firstName, lastName: event.lastName, + isdCode: event.isdCode, emailAddress: event.emailAddress, mobileNumber: event.mobileNumber, description: event.description, diff --git a/lib/profile/bloc/contactUs/contact_us_event.dart b/lib/profile/bloc/contactUs/contact_us_event.dart index 4d31a82..06bf3fb 100644 --- a/lib/profile/bloc/contactUs/contact_us_event.dart +++ b/lib/profile/bloc/contactUs/contact_us_event.dart @@ -11,6 +11,7 @@ abstract class ContactUsEvent extends Equatable { class SubmitContactUsEvent extends ContactUsEvent { final String firstName; final String lastName; + final String isdCode; final String emailAddress; final String mobileNumber; final String description; @@ -18,6 +19,7 @@ class SubmitContactUsEvent extends ContactUsEvent { const SubmitContactUsEvent({ required this.firstName, required this.lastName, + required this.isdCode, required this.emailAddress, required this.mobileNumber, required this.description, @@ -27,6 +29,7 @@ class SubmitContactUsEvent extends ContactUsEvent { List get props => [ firstName, lastName, + isdCode, emailAddress, mobileNumber, description, diff --git a/lib/profile/bloc/profile/profile_event.dart b/lib/profile/bloc/profile/profile_event.dart index bd10d29..ea6aed3 100644 --- a/lib/profile/bloc/profile/profile_event.dart +++ b/lib/profile/bloc/profile/profile_event.dart @@ -25,6 +25,7 @@ class UpdateProfileEvent extends ProfileEvent { final String firstName; final String lastName; final String mobileNumber; + final String? isdCode; final String? address1; final String? address2; final String? city; // ⭐ NEW @@ -38,6 +39,7 @@ class UpdateProfileEvent extends ProfileEvent { required this.firstName, required this.lastName, required this.mobileNumber, + this.isdCode, this.address1, this.address2, this.city, // ⭐ NEW @@ -53,6 +55,7 @@ class UpdateProfileEvent extends ProfileEvent { firstName, lastName, mobileNumber, + isdCode, address1, address2, city, // ⭐ NEW @@ -67,6 +70,7 @@ class UpdateProfileEvent extends ProfileEvent { 'firstName': firstName, 'lastName': lastName, 'mobileNumber': mobileNumber, + if (isdCode != null && isdCode!.isNotEmpty) 'isdCode': isdCode, if (address1 != null && address1!.isNotEmpty) 'address1': address1, if (address2 != null && address2!.isNotEmpty) 'address2': address2, if (city != null && city!.isNotEmpty) 'city': city, // ⭐ NEW diff --git a/lib/profile/models/profile_model.dart b/lib/profile/models/profile_model.dart index 13c53bc..e260540 100644 --- a/lib/profile/models/profile_model.dart +++ b/lib/profile/models/profile_model.dart @@ -4,9 +4,9 @@ class ProfileModel { final String lastName; final int roleXid; final String emailAddress; - final String isdCode; + final String? isdCode; final String mobileNumber; - final String? profileImage; // ✅ NEW + final String? profileImage; final String? address1; final String? address2; final String? cityName; @@ -26,7 +26,7 @@ class ProfileModel { required this.lastName, required this.roleXid, required this.emailAddress, - required this.isdCode, + this.isdCode, required this.mobileNumber, this.profileImage, this.address1, @@ -50,9 +50,9 @@ class ProfileModel { lastName: json['lastName'] ?? 'N/A', roleXid: json['roleXid'] ?? 0, emailAddress: json['emailAddress'] ?? 'N/A', - isdCode: json['isdCode'] ?? 'N/A', + isdCode: json['isdCode'], mobileNumber: json['mobileNumber'] ?? 'N/A', - profileImage: json['profileImage'], // ✅ added + profileImage: json['profileImage'], address1: json['address1'], address2: json['address2'], cityName: json['cityName'], diff --git a/lib/profile/repository/contact_us_repository.dart b/lib/profile/repository/contact_us_repository.dart index 6c5a756..4424148 100644 --- a/lib/profile/repository/contact_us_repository.dart +++ b/lib/profile/repository/contact_us_repository.dart @@ -8,6 +8,7 @@ class ContactUsRepository { Future> submitTicket({ required String firstName, required String lastName, + required String isdCode, required String emailAddress, required String mobileNumber, required String description, @@ -18,6 +19,7 @@ class ContactUsRepository { data: { "firstName": firstName, "lastName": lastName, + "isdCode": isdCode, "emailAddress": emailAddress, "mobileNumber": mobileNumber, "description": description, diff --git a/lib/profile/repository/profile_repository.dart b/lib/profile/repository/profile_repository.dart index bf9cb48..c0a6f3b 100644 --- a/lib/profile/repository/profile_repository.dart +++ b/lib/profile/repository/profile_repository.dart @@ -60,6 +60,9 @@ class ProfileRepository { if (data['address1'] != null && data['address1'].toString().isNotEmpty) MapEntry('address1', data['address1']), + if (data['isdCode'] != null && data['isdCode'].toString().isNotEmpty) + MapEntry('isdCode', data['isdCode']), + if (data['address2'] != null && data['address2'].toString().isNotEmpty) MapEntry('address2', data['address2']), diff --git a/lib/profile/view/contact_us/contact_us_view.dart b/lib/profile/view/contact_us/contact_us_view.dart index cf5132c..9e5c82d 100644 --- a/lib/profile/view/contact_us/contact_us_view.dart +++ b/lib/profile/view/contact_us/contact_us_view.dart @@ -2,9 +2,11 @@ import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/back_widget.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:citycards_customer/common_packages/custom_textfield.dart'; +import 'package:country_code_picker/country_code_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import '../../bloc/contactUs/contact_us_bloc.dart'; import '../../bloc/contactUs/contact_us_event.dart'; @@ -23,19 +25,36 @@ class ContactUsPage extends StatelessWidget { } } -class _ContactUsView extends StatelessWidget { +// ✅ Changed to StatefulWidget to hold _selectedIsdCode state +class _ContactUsView extends StatefulWidget { const _ContactUsView(); + @override + State<_ContactUsView> createState() => _ContactUsViewState(); +} + +class _ContactUsViewState extends State<_ContactUsView> { + final firstNameController = TextEditingController(); + final lastNameController = TextEditingController(); + final emailController = TextEditingController(); + final phoneController = TextEditingController(); + final messageController = TextEditingController(); + final formKey = GlobalKey(); + + String _selectedIsdCode = '+61'; // ✅ tracks selected dial code + + @override + void dispose() { + firstNameController.dispose(); + lastNameController.dispose(); + emailController.dispose(); + phoneController.dispose(); + messageController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - final firstNameController = TextEditingController(); - final lastNameController = TextEditingController(); - final emailController = TextEditingController(); - final phoneController = TextEditingController(); - final messageController = TextEditingController(); - - final formKey = GlobalKey(); - return Scaffold( backgroundColor: Colors.white, body: SafeArea( @@ -48,7 +67,6 @@ class _ContactUsView extends StatelessWidget { backgroundColor: Colors.green, ), ); - firstNameController.clear(); lastNameController.clear(); emailController.clear(); @@ -155,8 +173,6 @@ class _ContactUsView extends StatelessWidget { isFirstLetterCapital: true, keyboardType: TextInputType.name, ), - - /// EMAIL VALIDATION ADDED CustomTextField( label: "Email *", hint: "Enter your email address", @@ -175,22 +191,48 @@ class _ContactUsView extends StatelessWidget { }, ), - /// PHONE NUMBER VALIDATION ADDED + // ✅ Phone field with CountryCodePicker via prefixWidget CustomTextField( label: "Phone Number *", hint: "Enter your phone number", controller: phoneController, - keyboardType: TextInputType.number, - maxLength: 10, + keyboardType: TextInputType.phone, + maxLength: 12, + numbersOnly: true, validator: (value) { if (value == null || value.trim().isEmpty) { return "Phone number is required"; } - if (value.trim().length != 10) { - return "Enter a valid 10-digit phone number"; + try { + final parsed = PhoneNumber.parse( + '$_selectedIsdCode${value.trim()}'); + if (!parsed.isValid()) throw Exception(); + } catch (_) { + return "Enter a valid phone number for $_selectedIsdCode"; } return null; }, + prefixWidget: CountryCodePicker( + onChanged: (country) { + setState(() => _selectedIsdCode = country.dialCode!); + }, + initialSelection: 'AU', + favorite: const ['+61', '+1', '+44', '+91'], + showCountryOnly: false, + showOnlyCountryWhenClosed: false, + alignLeft: false, + flagWidth: 24.w, + padding: EdgeInsets.symmetric(horizontal: 8.w), + textStyle: TextStyle( + fontSize: 13.sp, + color: const Color(0xFF2D3134), + ), + dialogTextStyle: TextStyle(fontSize: 14.sp), + searchDecoration: const InputDecoration( + hintText: 'Search country...', + prefixIcon: Icon(Icons.search), + ), + ), ), CustomTextField( @@ -221,22 +263,17 @@ class _ContactUsView extends StatelessWidget { onPressed: isLoading ? null : () { - if (!formKey.currentState!.validate()) { - return; - } + if (!formKey.currentState!.validate()) return; context.read().add( SubmitContactUsEvent( firstName: firstNameController.text.trim(), - lastName: - lastNameController.text.trim(), - emailAddress: - emailController.text.trim(), - mobileNumber: - phoneController.text.trim(), - description: - messageController.text.trim(), + lastName: lastNameController.text.trim(), + isdCode: _selectedIsdCode, + emailAddress: emailController.text.trim(), + mobileNumber: phoneController.text.trim(), + description: messageController.text.trim(), ), ); }, @@ -269,7 +306,6 @@ class _ContactUsView extends StatelessWidget { ); } - /// Support Box Widget static Widget _supportBox({ required IconData icon, required String title, diff --git a/lib/profile/view/edit_profile/edit_profile_view.dart b/lib/profile/view/edit_profile/edit_profile_view.dart index 1063bd0..9c6de37 100644 --- a/lib/profile/view/edit_profile/edit_profile_view.dart +++ b/lib/profile/view/edit_profile/edit_profile_view.dart @@ -2,12 +2,15 @@ import 'dart:io'; import 'package:citycards_customer/common_packages/app_bar.dart'; import 'package:citycards_customer/common_packages/back_widget.dart'; import 'package:citycards_customer/common_packages/custom_textfield.dart'; +import 'package:country_code_picker/country_code_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:citycards_customer/common_packages/custom_text.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:geocoding/geocoding.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import '../../../localPreference/local_preference.dart'; import '../../../networkApiServices/api_urls.dart'; @@ -33,10 +36,14 @@ class _EditProfilePageState extends State { final TextEditingController address2Controller = TextEditingController(); final TextEditingController cityController = TextEditingController(); final TextEditingController zipCodeController = TextEditingController(); + final TextEditingController stateController = TextEditingController(); + final TextEditingController countryController = TextEditingController(); // Dropdown values - String? selectedState; - String? selectedCountry; + String _selectedIsdCode = ''; + Key _countryPickerKey = UniqueKey(); // ADD + String _selectedCountryCode = 'AU'; + bool _isZipLoading = false; final _formKey = GlobalKey(); final ImagePicker _picker = ImagePicker(); @@ -47,6 +54,30 @@ class _EditProfilePageState extends State { _fetchProfile(); } + Future fetchLocationFromZip(String zip) async { + if (zip.trim().length < 4) return; + setState(() => _isZipLoading = true); + try { + List locations = await locationFromAddress(zip); + if (locations.isNotEmpty) { + List placemarks = await placemarkFromCoordinates( + locations.first.latitude, + locations.first.longitude, + ); + final place = placemarks.first; + setState(() { + cityController.text = place.locality ?? ''; + stateController.text = place.administrativeArea ?? ''; + countryController.text = place.country ?? ''; + }); + } + } catch (e) { + debugPrint("Zip lookup failed: $e"); + } finally { + if (mounted) setState(() => _isZipLoading = false); + } + } + Future _fetchProfile() async { if (kDebugMode) { print('🔵 [EDIT PROFILE] Fetching profile...'); @@ -74,11 +105,13 @@ class _EditProfilePageState extends State { address2Controller.text = profile.address2 ?? ''; cityController.text = profile.cityName ?? ''; zipCodeController.text = profile.zipCode ?? ''; - + stateController.text = profile.stateName ?? ''; + countryController.text = profile.country ?? ''; // Set dropdown values from fetched data setState(() { - selectedState = profile.stateName; - selectedCountry = profile.country; + _selectedIsdCode = profile.isdCode??""; + _selectedCountryCode = profile.isdCode ?? '+61'; // ADD + _countryPickerKey = UniqueKey(); }); // ⭐ REMOVED setState - image is now managed by BLoC state @@ -313,6 +346,28 @@ class _EditProfilePageState extends State { if (!mounted) return; + // Phone validation + final phone = phoneController.text.trim(); + bool isValidPhone = false; + + try { + final fullNumber = '$_selectedIsdCode$phone'; + final parsed = PhoneNumber.parse(fullNumber); + isValidPhone = parsed.isValid(); + } catch (_) { + isValidPhone = false; + } + + if (!isValidPhone) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Enter a valid phone number for $_selectedIsdCode'), + backgroundColor: Colors.red, + ), + ); + return; + } + // ⭐ Get selectedImageFile from current BLoC state File? imageFileToSend; final currentState = context.read().state; @@ -330,7 +385,8 @@ class _EditProfilePageState extends State { userId: userId, firstName: firstNameController.text.trim(), lastName: lastNameController.text.trim(), - mobileNumber: phoneController.text.trim(), + mobileNumber: phone, + isdCode: _selectedIsdCode, address1: address1Controller.text.trim().isEmpty ? null : address1Controller.text.trim(), @@ -341,8 +397,8 @@ class _EditProfilePageState extends State { city: cityController.text.trim().isEmpty ? null : cityController.text.trim(), - state: selectedState, - country: selectedCountry, + state: stateController.text.trim().isEmpty ? null : stateController.text.trim(), + country: countryController.text.trim().isEmpty ? null : countryController.text.trim(), postalCode: zipCodeController.text.trim().isEmpty ? null : zipCodeController.text.trim(), @@ -360,6 +416,8 @@ class _EditProfilePageState extends State { address2Controller.dispose(); cityController.dispose(); zipCodeController.dispose(); + stateController.dispose(); + countryController.dispose(); super.dispose(); } @@ -516,18 +574,36 @@ class _EditProfilePageState extends State { label: "Phone Number *", hint: "Enter your phone number", controller: phoneController, + keyboardType: TextInputType.phone, + maxLength: 12, + numbersOnly: true, enabled: !isLoading, - keyboardType: TextInputType.number, - maxLength: 10, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return "Phone number is required"; - } - if (value.trim().length != 10) { - return "Enter a valid 10-digit phone number"; - } - return null; - }, + prefixWidget: CountryCodePicker( + key: _countryPickerKey, + onChanged: isLoading + ? null + : (country) { + setState(() => _selectedIsdCode = country.dialCode!); + }, + initialSelection: _selectedCountryCode, + favorite: const ['+61', '+1', '+44', '+91'], + showCountryOnly: false, + showOnlyCountryWhenClosed: false, + alignLeft: false, + flagWidth: 24.w, + padding: EdgeInsets.symmetric(horizontal: 8.w), + textStyle: TextStyle( + fontSize: 13.sp, + color: isLoading + ? const Color(0xFF8E8E8E) + : const Color(0xFF2D3134), + ), + dialogTextStyle: TextStyle(fontSize: 14.sp), + searchDecoration: const InputDecoration( + hintText: 'Search country...', + prefixIcon: Icon(Icons.search), + ), + ), ), ), @@ -568,129 +644,45 @@ class _EditProfilePageState extends State { ), Padding( - padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), + padding: EdgeInsets.symmetric(horizontal: 12.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomText(text: "State *", size: 14.sp), - SizedBox(height: 6.h), - Container( - height: 42.h, - padding: EdgeInsets.symmetric(horizontal: 24.w), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: const Color(0xBBC83B61).withOpacity(0.4), - width: 0.4.w, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedState, - isExpanded: true, - icon: const Icon( - Icons.keyboard_arrow_down, - color: Color(0xFF8E8E8E), + Row( + children: [ + Expanded( + child: CustomTextField( + controller: zipCodeController, + enabled: !isLoading, + keyboardType: TextInputType.number, + maxLength: 6, + onChanged: fetchLocationFromZip, + label: 'Zip Code *', + hint: 'Enter the ZIP code you reside in', ), - hint: Text( - "Select state", - style: TextStyle( - fontSize: 12.sp, - color: const Color(0xFF8E8E8E), + ), + if (_isZipLoading) + Padding( + padding: EdgeInsets.only(right: 12.w), + child: SizedBox( + width: 18.w, + height: 18.h, + child: const CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFF95F62), + ), ), ), - style: TextStyle( - fontSize: 14.sp, - color: const Color(0xFF2D3134), - ), - onChanged: isLoading ? null : (value) { - setState(() { - selectedState = value; - }); - }, - items: [ - "New South Wales", - "Victoria", - "Queensland", - "South Australia", - "Western Australia", - "Tasmania", - "Northern Territory", - "Australian Capital Territory" - ].map((value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: TextStyle(fontSize: 14.sp), - ), - ); - }).toList(), - ), - ), + ], ), + // Text( + // "City, State & Country will auto-fill from zip", + // style: TextStyle(fontSize: 10.sp, color: const Color(0xFF8E8E8E)), + // ), ], ), ), - Padding( - padding: EdgeInsets.only(bottom: 12.h, left: 12.w, right: 12.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomText(text: "Country *", size: 14.sp), - SizedBox(height: 6.h), - Container( - height: 42.h, - padding: EdgeInsets.symmetric(horizontal: 24.w), - decoration: BoxDecoration( - color: const Color(0xFFFFF5F5), - borderRadius: BorderRadius.circular(8.r), - border: Border.all( - color: const Color(0xBBC83B61).withOpacity(0.4), - width: 0.4.w, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedCountry, - isExpanded: true, - icon: const Icon( - Icons.keyboard_arrow_down, - color: Color(0xFF8E8E8E), - ), - hint: Text( - "Select country", - style: TextStyle( - fontSize: 12.sp, - color: const Color(0xFF8E8E8E), - ), - ), - style: TextStyle( - fontSize: 14.sp, - color: const Color(0xFF2D3134), - ), - onChanged: isLoading ? null : (value) { - setState(() { - selectedCountry = value; - }); - }, - items: ["Australia"].map((value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: TextStyle(fontSize: 14.sp), - ), - ); - }).toList(), - ), - ), - ), - ], - ), - ), Padding( padding: EdgeInsets.symmetric(horizontal: 12.0.w), @@ -701,21 +693,37 @@ class _EditProfilePageState extends State { enabled: !isLoading, maxLength: 50, onlyLetters: true, + isPreview: true, ), ), Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0.w), + padding: EdgeInsets.symmetric(horizontal: 12.w), child: CustomTextField( - label: "ZIP Code *", - hint: "Enter the ZIP code you reside in", - controller: zipCodeController, + label: "State *", + hint: "Enter your state", + controller: stateController, enabled: !isLoading, - keyboardType: TextInputType.number, - maxLength: 6, + maxLength: 50, + isFirstLetterCapital: true, + isPreview: true, ), ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: CustomTextField( + label: "Country *", + hint: "Enter your country", + controller: countryController, + enabled: !isLoading, + maxLength: 50, + isPreview: true, + isFirstLetterCapital: true, + ), + ), + + SizedBox(height: 26.h), // Buttons diff --git a/lib/your_itinerary/models/your_itinerary_details_model.dart b/lib/your_itinerary/models/your_itinerary_details_model.dart index 53481ed..7dbb609 100644 --- a/lib/your_itinerary/models/your_itinerary_details_model.dart +++ b/lib/your_itinerary/models/your_itinerary_details_model.dart @@ -1,5 +1,7 @@ class YourItineraryDetailsModel { final int id; + final String userFirstName; + final String validUpto; final String title; final String city; final String cityBanner; @@ -11,6 +13,8 @@ class YourItineraryDetailsModel { YourItineraryDetailsModel({ required this.id, + required this.userFirstName, + required this.validUpto, required this.title, required this.city, required this.cityBanner, @@ -21,17 +25,19 @@ class YourItineraryDetailsModel { required this.days, }); - factory YourItineraryDetailsModel.fromJson(Map? json) { + factory YourItineraryDetailsModel.fromJson(Map json) { return YourItineraryDetailsModel( - id: json?['id'] ?? 0, - title: json?['title'] ?? "", - city: json?['city'] ?? "", - cityBanner: json?['cityBanner'] ?? "", - totalDays: json?['totalDays'] ?? 0, - totalStops: json?['totalStops'] ?? 0, - adults: json?['adults'] ?? 0, - children: json?['children'] ?? 0, - days: (json?['days'] as List?) + id: json['id'] ?? 0, + userFirstName: json['userFirstName'] ?? "", + validUpto: json['validUpto'] ?? "", + title: json['title'] ?? "", + city: json['city'] ?? "", + cityBanner: json['cityBanner'] ?? "", + totalDays: json['totalDays'] ?? 0, + totalStops: json['totalStops'] ?? 0, + adults: json['adults'] ?? 0, + children: json['children'] ?? 0, + days: (json['days'] as List?) ?.map((e) => ItineraryDay.fromJson(e)) .toList() ?? [], @@ -52,12 +58,12 @@ class ItineraryDay { required this.items, }); - factory ItineraryDay.fromJson(Map? json) { + factory ItineraryDay.fromJson(Map json) { return ItineraryDay( - dayNumber: json?['dayNumber'] ?? 0, - title: json?['title'] ?? "", - date: json?['date'] ?? "", - items: (json?['items'] as List?) + dayNumber: json['dayNumber'] ?? 0, + title: json['title'] ?? "", + date: json['date'] ?? "", + items: (json['items'] as List?) ?.map((e) => ItineraryItem.fromJson(e)) .toList() ?? [], @@ -92,21 +98,20 @@ class ItineraryItem { required this.attractionXid, }); - factory ItineraryItem.fromJson(Map? json) { + factory ItineraryItem.fromJson(Map json) { return ItineraryItem( - id: json?['id'] ?? 0, - itineraryDayXid: json?['itineraryDayXid'] ?? 0, - timeSlot: json?['timeSlot'] ?? "", - title: json?['title'] ?? "", - description: json?['description'] ?? "", - locationName: json?['locationName'] ?? "", + id: json['id'] ?? 0, + itineraryDayXid: json['itineraryDayXid'] ?? 0, + timeSlot: json['timeSlot'] ?? "", + title: json['title'] ?? "", + description: json['description'] ?? "", + locationName: json['locationName'] ?? "", categories: - (json?['categories'] as List?)?.map((e) => e.toString()).toList() ?? - [], - imageUrl: json?['imageUrl'] ?? "", - latitude: (json?['latitude'] ?? 0).toDouble(), - longitude: (json?['longitude'] ?? 0).toDouble(), - attractionXid: json?['attractionXid'], + (json['categories'] as List?)?.map((e) => e.toString()).toList() ?? [], + imageUrl: json['imageUrl'] ?? "", + latitude: (json['latitude'] ?? 0).toDouble(), + longitude: (json['longitude'] ?? 0).toDouble(), + attractionXid: json['attractionXid'], ); } } \ No newline at end of file diff --git a/lib/your_itinerary/view/your_itinerary_view.dart b/lib/your_itinerary/view/your_itinerary_view.dart index f8ccef0..6f90933 100644 --- a/lib/your_itinerary/view/your_itinerary_view.dart +++ b/lib/your_itinerary/view/your_itinerary_view.dart @@ -165,7 +165,7 @@ class _YourItineraryViewState extends State { // Title Text( - 'Your', + "${itinerary.userFirstName}'s", style: TextStyle( fontSize: 28.sp, fontWeight: FontWeight.w700, @@ -256,9 +256,7 @@ class _YourItineraryViewState extends State { ), SizedBox(width: 2.w), Text( - itinerary.days.isNotEmpty - ? itinerary.days.first.date - : 'N/A', + itinerary.validUpto, style: TextStyle( fontSize: 10.5.sp, color: Color(0xFF6B7280)), diff --git a/lib/your_itinerary/widgets/summary_card_view.dart b/lib/your_itinerary/widgets/summary_card_view.dart index f1c068b..71c1028 100644 --- a/lib/your_itinerary/widgets/summary_card_view.dart +++ b/lib/your_itinerary/widgets/summary_card_view.dart @@ -60,25 +60,25 @@ class SummaryCard extends StatelessWidget { weight: FontWeight.w500, color: const Color(0xFF212121), ), - SizedBox(width: 16.w), + Spacer(), // 👈 Add this Row( children: [ Image.asset( "assets/icons/calender_filled.png", color: const Color(0xFFF95F62), - width: 20.sp, + width: 16.sp, // 👈 slightly smaller (was 20.sp) ), SizedBox(width: 4.w), CustomText( text: date, color: const Color(0xFFF95F62), - size: 16.sp, + size: 14.sp, // 👈 slightly smaller (was 16.sp) weight: FontWeight.w500, ), ], ), ], - ), + ), SizedBox(height: 15.h), diff --git a/pubspec.lock b/pubspec.lock index ade63ca..ed38889 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -113,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + country_code_picker: + dependency: "direct main" + description: + name: country_code_picker + sha256: f0411f4833b6f98e8b7215f4fa3813bcc88e50f13925f70a170dbd36e3e447f5 + url: "https://pub.dev" + source: hosted + version: "3.4.1" cross_file: dependency: transitive description: @@ -177,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + diacritic: + dependency: transitive + description: + name: diacritic + sha256: "12981945ec38931748836cd76f2b38773118d0baef3c68404bdfde9566147876" + url: "https://pub.dev" + source: hosted + version: "0.1.6" dio: dependency: "direct main" description: @@ -685,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" json_annotation: dependency: transitive description: @@ -725,6 +749,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + libphonenumber_platform_interface: + dependency: transitive + description: + name: libphonenumber_platform_interface + sha256: f801f6c65523f56504b83f0890e6dad584ab3a7507dca65fec0eed640afea40f + url: "https://pub.dev" + source: hosted + version: "0.4.2" + libphonenumber_plugin: + dependency: "direct main" + description: + name: libphonenumber_plugin + sha256: c615021d9816fbda2b2587881019ed595ecdf54d999652d7e4cce0e1f026368c + url: "https://pub.dev" + source: hosted + version: "0.3.3" + libphonenumber_web: + dependency: transitive + description: + name: libphonenumber_web + sha256: "8186f420dbe97c3132283e52819daff1e55d60d6db46f7ea5ac42f42a28cc2ef" + url: "https://pub.dev" + source: hosted + version: "0.3.2" lints: dependency: transitive description: @@ -909,6 +957,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + phone_numbers_parser: + dependency: "direct main" + description: + name: phone_numbers_parser + sha256: c30ec1a8ee216da8631eb32d6c3ce0fec85c9accb221c8868bb0aa90c0ce5e95 + url: "https://pub.dev" + source: hosted + version: "9.0.20" platform: dependency: transitive description: @@ -949,6 +1005,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" rxdart: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e38f70c..7fda351 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,10 @@ dependencies: google_mlkit_translation: ^0.13.1 url_launcher: ^6.3.2 open_filex: ^4.7.0 + country_code_picker: ^3.4.1 + libphonenumber_plugin: ^0.3.3 + phone_numbers_parser: ^9.0.20 + qr_flutter: ^4.1.0 dev_dependencies: flutter_test: