439 lines
19 KiB
Dart
439 lines
19 KiB
Dart
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),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} |