Files
CityCards_Customer_Flutter/lib/create_account/view/create_account_view.dart

439 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
import '../../itinerary_creation/bloc/get_itinerary_bloc.dart';
import '../../localPreference/local_preference.dart';
import '../../my_pass/blocs/myPasses/my_passes_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_bloc.dart';
import '../../postcard/blocs/myPostCards/my_postcard_event.dart';
import '../../profile/bloc/profile/profile_bloc.dart';
import '../../profile/bloc/profile/profile_event.dart';
import '../bloc/create_account_bloc.dart';
import '../bloc/create_account_event.dart';
import '../bloc/create_account_state.dart';
import '../repository/create_account_repository.dart';
class CreateAccountView extends StatefulWidget {
final String email;
const CreateAccountView({super.key, required this.email});
@override
State<CreateAccountView> createState() => _CreateAccountViewState();
}
class _CreateAccountViewState extends State<CreateAccountView> {
final TextEditingController firstNameController = TextEditingController();
final TextEditingController lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController addressController = TextEditingController();
final TextEditingController cityController = TextEditingController();
final TextEditingController postalController = TextEditingController();
// ── 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<void> fetchLocationFromZip(String zip) async {
if (zip.trim().length < 4) return; // wait for a meaningful zip length
setState(() => _isZipLoading = true);
try {
List<Location> locations = await locationFromAddress(zip);
if (locations.isNotEmpty) {
List<Placemark> 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 ||
stateController.text.trim().isEmpty ||
countryController.text.trim().isEmpty ||
postalController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please fill all fields')),
);
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<CreateAccountBloc>().add(
CreateAccountSubmitted(
firstName: firstNameController.text.trim(),
lastName: lastNameController.text.trim(),
emailAddress: emailController.text.trim(),
isdCode: _selectedIsdCode,
mobileNumber: phone,
address1: addressController.text.trim(),
address2: '',
city: cityController.text.trim(),
state: stateController.text.trim(),
country: countryController.text.trim(),
postalCode: postalController.text.trim(),
),
);
}
@override
void dispose() {
firstNameController.dispose();
lastNameController.dispose();
emailController.dispose();
phoneController.dispose();
addressController.dispose();
cityController.dispose();
postalController.dispose();
stateController.dispose();
countryController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
emailController.text = widget.email;
return BlocProvider(
create: (context) =>
CreateAccountBloc(repository: CreateAccountRepository()),
child: BlocListener<CreateAccountBloc, CreateAccountState>(
listener: (ctx, state) async {
if (state is CreateAccountSuccess) {
await LocalPreference.setLogin(true);
final userId = await LocalPreference.getUserId();
context.read<ProfileBloc>().add(FetchProfileEvent(userId: userId!));
context.read<ProfileBloc>().add(CheckLoginStatusEvent());
context.read<MyPostCardBloc>().add(CheckLoginStatus());
context.read<GetItineraryBloc>().add(CheckLoginAndFetchItinerary());
context.read<MyPostCardBloc>().add(RefreshDraftPostCards());
context.read<MyPostCardBloc>().add(RefreshOrderPostCards());
context.read<MyPassesBloc>().add(CheckLoginAndFetchPasses());
context
.read<MyPostCardsCartBloc>()
.add(CheckLoginAndFetchPostcardsCart());
Navigator.pop(context);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
} else if (state is CreateAccountFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage),
backgroundColor: Colors.red,
),
);
}
},
child: Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: CommonAppBar(
isWhiteLogo: false,
isProfilePage: false,
showCart: false,
showDivider: true,
),
),
/// Scrollable content
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(Icons.arrow_back),
),
SizedBox(width: 8.w),
CustomText(
text: "Create your account",
size: 12.sp,
),
],
),
SizedBox(height: 26.h),
CustomText(
text: "Personal Information",
size: 18.sp,
weight: FontWeight.w500,
),
SizedBox(height: 12.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "First Name *",
hint: "Enter your first name",
controller: firstNameController,
onlyLetters: true,
noSpace: true,
maxLength: 50,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Last Name *",
hint: "Enter your last name",
controller: lastNameController,
onlyLetters: true,
maxLength: 50,
noSpace: true,
keyboardType: TextInputType.name,
isFirstLetterCapital: true,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Email *",
hint: "Enter your email address",
controller: emailController,
enabled: false,
keyboardType: TextInputType.emailAddress,
),
),
// ── Phone Number ──────────────────────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Phone Number *",
hint: "Enter phone number",
controller: phoneController,
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),
),
),
),
),
SizedBox(height: 12.h),
CustomText(
text: "Location Details *",
size: 18.sp,
weight: FontWeight.w500,
),
SizedBox(height: 16.h),
// ── Address ───────────────────────────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "Address *",
hint: "Enter your address",
controller: addressController,
maxLength: 50,
),
),
SizedBox(height: 8.h),
// ── City (unchanged) ──────────────────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
label: "City *",
hint: "Enter your city",
maxLength: 50,
noSpace: true,
controller: cityController,
isFirstLetterCapital: true,
),
),
// ── State now a plain text field ────────────────────
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: CustomTextField(
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<CreateAccountBloc, CreateAccountState>(
builder: (context, state) {
if (state is CreateAccountLoading) {
return CustomFilledButton(
width: double.infinity,
onTap: () {},
label: "Creating...",
);
}
return CustomFilledButton(
width: double.infinity,
onTap: () => _submitForm(context),
label: "Create Account",
);
},
),
SizedBox(height: 20.h),
],
),
),
),
],
),
),
),
),
);
}
}