Files
Kifi/kifi-app/lib/features/auth/presentation/setup_wizard_screen.dart

672 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import '../../../../core/network/dio_client.dart';
import '../../dashboard/presentation/dashboard_screen.dart';
import 'package:url_launcher/url_launcher.dart';
class SetupWizardScreen extends StatefulWidget {
const SetupWizardScreen({super.key});
@override
State<SetupWizardScreen> createState() => _SetupWizardScreenState();
}
class _SetupWizardScreenState extends State<SetupWizardScreen>
with SingleTickerProviderStateMixin {
bool _isLoading = false;
int _currentStep = 0;
String _accountType = 'INDIVIDUAL';
// Step 1: Personal Info
final _nameController = TextEditingController();
final _usernameController = TextEditingController();
String _mobileNumber = '';
bool _isUsernameAvailable = false;
bool _checkingUsername = false;
// Step 2: Business Info (if BUSINESS)
String? _natureOfBusiness;
final _businessNameController = TextEditingController();
final _addressController = TextEditingController();
final _emailController = TextEditingController();
final _gstController = TextEditingController();
final _panController = TextEditingController();
final _msmeController = TextEditingController();
// Step 3: Consent
bool _termsAccepted = false;
bool _privacyAccepted = false;
int get _totalSteps => _accountType == 'BUSINESS' ? 4 : 3;
Future<void> _checkUsername(String username) async {
if (username.length < 3) return;
setState(() => _checkingUsername = true);
try {
final res = await DioClient().dio.get(
'/account/username/availability',
queryParameters: {'username': username},
);
if (mounted) {
setState(() {
_isUsernameAvailable = res.data['available'] == true;
_checkingUsername = false;
});
}
} catch (e) {
if (mounted) setState(() => _checkingUsername = false);
}
}
Future<void> _submitSetup() async {
if (!_termsAccepted || !_privacyAccepted) return;
setState(() => _isLoading = true);
try {
final data = {
'accountType': _accountType,
'name': _nameController.text,
'username': _usernameController.text,
'mobileNumber': _mobileNumber,
'termsAccepted': _termsAccepted,
'privacyAccepted': _privacyAccepted,
};
if (_accountType == 'BUSINESS') {
data.addAll({
'businessName': _businessNameController.text.isNotEmpty
? _businessNameController.text
: _nameController.text,
'natureOfBusiness': _natureOfBusiness!,
'address': _addressController.text,
'emailId': _emailController.text,
'gstin': _gstController.text,
'panNumber': _panController.text,
'msmeNumber': _msmeController.text,
});
}
await DioClient().dio.post('/account/setup', data: data);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const DashboardScreen()),
);
}
} catch (e) {
if (mounted) setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Setup failed: $e')));
}
}
}
Future<void> _launchUrl(String path) async {
final url = Uri.parse('http://192.168.0.104:8080/public/legal/$path');
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
}
void _nextStep() {
if (_currentStep == 1) {
if (_nameController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter your Full Name')),
);
return;
}
if (_usernameController.text.trim().length < 3) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Username must be at least 3 characters'),
),
);
return;
}
if (!_isUsernameAvailable) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Username is unavailable or invalid')),
);
return;
}
}
if (_accountType == 'BUSINESS' && _currentStep == 2) {
if (_natureOfBusiness == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select Nature of Business')),
);
return;
}
}
if (_currentStep < _totalSteps - 1) {
setState(() => _currentStep++);
} else {
_submitSetup();
}
}
void _prevStep() {
if (_currentStep > 0) {
setState(() => _currentStep--);
}
}
InputDecoration _buildInputDecoration(String label) {
return InputDecoration(
labelText: label,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
);
}
Widget _buildProgressIndicator() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Step ${_currentStep + 1} of $_totalSteps',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: (_currentStep + 1) / _totalSteps,
minHeight: 6,
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor,
),
),
),
],
),
);
}
Widget _buildStep0AccountType() {
return Column(
key: const ValueKey(0),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Welcome to Kifi!',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'How will you be using the application?',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 32),
_buildAccountTypeCard(
'INDIVIDUAL',
'Individual',
'For personal use and expenses',
LucideIcons.user,
),
const SizedBox(height: 16),
_buildAccountTypeCard(
'BUSINESS',
'Business',
'For managing business and inventory',
LucideIcons.building2,
),
],
);
}
Widget _buildAccountTypeCard(
String type,
String title,
String subtitle,
IconData icon,
) {
final isSelected = _accountType == type;
return InkWell(
onTap: () => setState(() => _accountType = type),
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).primaryColor.withOpacity(0.08)
: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? Theme.of(context).primaryColor
: Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
boxShadow: isSelected
? []
: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).primaryColor
: Colors.grey.shade100,
shape: BoxShape.circle,
),
child: Icon(
icon,
color: isSelected ? Colors.white : Colors.grey[600],
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(color: Colors.grey[600], fontSize: 14),
),
],
),
),
Icon(
isSelected ? LucideIcons.checkCircle2 : LucideIcons.circle,
color: isSelected
? Theme.of(context).primaryColor
: Colors.grey[300],
size: 28,
),
],
),
),
);
}
Widget _buildStep1Personal() {
return Column(
key: const ValueKey(1),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Personal Details',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Tell us a bit about yourself.',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 32),
TextFormField(
controller: _nameController,
decoration: _buildInputDecoration('Full Name *'),
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _usernameController,
decoration: _buildInputDecoration('Username *').copyWith(
suffixIcon: SizedBox(
width: 48,
height: 48,
child: Center(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: _checkingUsername
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(
_isUsernameAvailable
? LucideIcons.checkCircle2
: LucideIcons.xCircle,
key: ValueKey(_isUsernameAvailable),
color: _usernameController.text.isEmpty
? Colors.transparent
: (_isUsernameAvailable
? Colors.green
: Colors.red),
),
),
),
),
),
onChanged: _checkUsername,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
IntlPhoneField(
decoration: _buildInputDecoration('Mobile Number'),
initialCountryCode: 'IN',
onChanged: (phone) => _mobileNumber = phone.completeNumber,
),
],
);
}
Widget _buildStep2Business() {
return Column(
key: const ValueKey(2),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Business Profile',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Set up your company details for invoicing and tracking.',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 32),
DropdownButtonFormField<String>(
decoration: _buildInputDecoration('Nature of Business *'),
dropdownColor: Colors.white,
value: _natureOfBusiness,
items: const [
DropdownMenuItem(
value: 'JEWELLERY',
child: Text('Jewellery', style: TextStyle(color: Colors.black87)),
),
DropdownMenuItem(
value: 'PROJECT_MANAGEMENT',
child: Text(
'Project Management',
style: TextStyle(color: Colors.black87),
),
),
DropdownMenuItem(
value: 'INVENTORY_MANAGEMENT',
child: Text(
'Inventory Management',
style: TextStyle(color: Colors.black87),
),
),
],
onChanged: (val) => setState(() => _natureOfBusiness = val),
),
const SizedBox(height: 20),
TextFormField(
controller: _businessNameController,
decoration: _buildInputDecoration('Business Name (Optional)'),
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _addressController,
decoration: _buildInputDecoration('Business Address (Optional)'),
maxLines: 3,
textInputAction: TextInputAction.newline,
),
const SizedBox(height: 20),
TextFormField(
controller: _emailController,
decoration: _buildInputDecoration('Official Email (Optional)'),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _gstController,
decoration: _buildInputDecoration('GST Number (Optional)'),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _panController,
decoration: _buildInputDecoration('PAN (Optional)'),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 20),
TextFormField(
controller: _msmeController,
decoration: _buildInputDecoration('MSME/Udyam Number (Optional)'),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.done,
),
],
);
}
Widget _buildStep3Consent() {
return Column(
key: const ValueKey(3),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Final Step',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Please review and accept our policies to continue.',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 32),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.02),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
CheckboxListTile(
title: InkWell(
onTap: () => _launchUrl('terms'),
child: const Text(
'I accept the Terms & Conditions',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
),
value: _termsAccepted,
controlAffinity: ListTileControlAffinity.leading,
activeColor: Theme.of(context).primaryColor,
onChanged: (val) =>
setState(() => _termsAccepted = val ?? false),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
Divider(height: 1, indent: 56, color: Colors.grey.shade200),
CheckboxListTile(
title: InkWell(
onTap: () => _launchUrl('privacy'),
child: const Text(
'I accept the Privacy Policy',
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
),
value: _privacyAccepted,
controlAffinity: ListTileControlAffinity.leading,
activeColor: Theme.of(context).primaryColor,
onChanged: (val) =>
setState(() => _privacyAccepted = val ?? false),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
],
),
),
],
);
}
Widget _buildCurrentStep() {
if (_currentStep == 0) return _buildStep0AccountType();
if (_currentStep == 1) return _buildStep1Personal();
if (_accountType == 'BUSINESS' && _currentStep == 2)
return _buildStep2Business();
return _buildStep3Consent(); // Step 2 (Individual) or Step 3 (Business)
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
leading: _currentStep > 0
? IconButton(
icon: const Icon(LucideIcons.chevronLeft),
onPressed: _prevStep,
)
: null,
),
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildProgressIndicator(),
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (child, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.05, 0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
);
},
child: SingleChildScrollView(
key: ValueKey(_currentStep),
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 16.0,
),
child: _buildCurrentStep(),
),
),
),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 24.0,
),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade200)),
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _nextStep,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 20),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: _isLoading
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
_currentStep == _totalSteps - 1
? 'Complete Setup'
: 'Continue',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
),
),
),
),
),
],
),
),
);
}
}