feat: Enhance billing display with currency formatting, improve dynamic form field management by clearing cached controllers and using unique keys, and add mailto URL scheme support.
This commit is contained in:
@@ -341,42 +341,36 @@ class _AgentEditProfileScreenState
|
||||
Widget build(BuildContext context) {
|
||||
final asyncData = ref.watch(_editProfileDataProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F7FA),
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
// Header with circular back button (SVG) matching Figma
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => context.pop(),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/back_circle_icon.svg',
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
// Header with circular back button (SVG) matching Figma
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => context.pop(),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/back_circle_icon.svg',
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Edit Profile',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Edit Profile',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(child: _buildBody(asyncData)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: _buildBody(asyncData)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -667,6 +661,13 @@ class _AgentEditProfileScreenState
|
||||
onTap: () {
|
||||
setState(() {
|
||||
entries.removeAt(i);
|
||||
// Clear cached controllers for this section's entries
|
||||
// so they get recreated with correct values after
|
||||
// re-indexing
|
||||
final fieldSlugs =
|
||||
fields.map((f) => f['slug'] as String? ?? '');
|
||||
_tagControllers.removeWhere((key, _) =>
|
||||
fieldSlugs.any((s) => key.startsWith('${s}_entry')));
|
||||
});
|
||||
},
|
||||
child: const Icon(
|
||||
@@ -688,6 +689,7 @@ class _AgentEditProfileScreenState
|
||||
entries[i][slug] = val;
|
||||
});
|
||||
},
|
||||
controllerKeySuffix: 'entry$i',
|
||||
);
|
||||
}),
|
||||
],
|
||||
@@ -731,6 +733,7 @@ class _AgentEditProfileScreenState
|
||||
Map<String, dynamic> field, {
|
||||
dynamic Function()? getValue,
|
||||
void Function(dynamic)? setValue,
|
||||
String? controllerKeySuffix,
|
||||
}) {
|
||||
final slug = field['slug'] as String? ?? '';
|
||||
final fieldType = field['fieldType'] as String? ?? 'TEXT';
|
||||
@@ -770,6 +773,7 @@ class _AgentEditProfileScreenState
|
||||
placeholder,
|
||||
validation,
|
||||
false,
|
||||
controllerKeySuffix: controllerKeySuffix,
|
||||
);
|
||||
break;
|
||||
case 'TEXTAREA':
|
||||
@@ -780,6 +784,7 @@ class _AgentEditProfileScreenState
|
||||
placeholder,
|
||||
validation,
|
||||
true,
|
||||
controllerKeySuffix: controllerKeySuffix,
|
||||
);
|
||||
break;
|
||||
case 'NUMBER':
|
||||
@@ -789,6 +794,7 @@ class _AgentEditProfileScreenState
|
||||
updateValue,
|
||||
placeholder,
|
||||
validation,
|
||||
controllerKeySuffix: controllerKeySuffix,
|
||||
);
|
||||
break;
|
||||
case 'DATE':
|
||||
@@ -851,6 +857,7 @@ class _AgentEditProfileScreenState
|
||||
currentValue,
|
||||
updateValue,
|
||||
placeholder,
|
||||
controllerKeySuffix: controllerKeySuffix,
|
||||
);
|
||||
break;
|
||||
case 'RANGE':
|
||||
@@ -870,6 +877,7 @@ class _AgentEditProfileScreenState
|
||||
placeholder,
|
||||
validation,
|
||||
false,
|
||||
controllerKeySuffix: controllerKeySuffix,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -937,9 +945,12 @@ class _AgentEditProfileScreenState
|
||||
void Function(dynamic) setValue,
|
||||
String placeholder,
|
||||
Map<String, dynamic>? validation,
|
||||
bool multiline,
|
||||
) {
|
||||
final key = '${slug}_text';
|
||||
bool multiline, {
|
||||
String? controllerKeySuffix,
|
||||
}) {
|
||||
final key = controllerKeySuffix != null
|
||||
? '${slug}_${controllerKeySuffix}_text'
|
||||
: '${slug}_text';
|
||||
if (!_tagControllers.containsKey(key)) {
|
||||
final controller = TextEditingController(text: '${getValue() ?? ''}');
|
||||
_tagControllers[key] = controller;
|
||||
@@ -974,9 +985,12 @@ class _AgentEditProfileScreenState
|
||||
dynamic Function() getValue,
|
||||
void Function(dynamic) setValue,
|
||||
String placeholder,
|
||||
Map<String, dynamic>? validation,
|
||||
) {
|
||||
final key = '${slug}_number';
|
||||
Map<String, dynamic>? validation, {
|
||||
String? controllerKeySuffix,
|
||||
}) {
|
||||
final key = controllerKeySuffix != null
|
||||
? '${slug}_${controllerKeySuffix}_number'
|
||||
: '${slug}_number';
|
||||
if (!_tagControllers.containsKey(key)) {
|
||||
final val = getValue();
|
||||
final controller = TextEditingController(text: val != null ? '$val' : '');
|
||||
@@ -1377,16 +1391,20 @@ class _AgentEditProfileScreenState
|
||||
String slug,
|
||||
dynamic Function() getValue,
|
||||
void Function(dynamic) setValue,
|
||||
String placeholder,
|
||||
) {
|
||||
String placeholder, {
|
||||
String? controllerKeySuffix,
|
||||
}) {
|
||||
final tags = List<String>.from((getValue() ?? <String>[]) as List);
|
||||
|
||||
if (!_tagControllers.containsKey(slug)) {
|
||||
final tagKey = controllerKeySuffix != null
|
||||
? '${slug}_${controllerKeySuffix}'
|
||||
: slug;
|
||||
if (!_tagControllers.containsKey(tagKey)) {
|
||||
final controller = TextEditingController();
|
||||
_tagControllers[slug] = controller;
|
||||
_tagControllers[tagKey] = controller;
|
||||
_controllers.add(controller);
|
||||
}
|
||||
final controller = _tagControllers[slug]!;
|
||||
final controller = _tagControllers[tagKey]!;
|
||||
|
||||
void addTag(String text) {
|
||||
final trimmed = text.trim();
|
||||
|
||||
@@ -400,17 +400,15 @@ class _StillNeedHelpSection extends ConsumerWidget {
|
||||
_buildSupportButton(
|
||||
icon: Icons.email_outlined,
|
||||
label: 'Email Support',
|
||||
onTap: () async {
|
||||
onTap: () {
|
||||
final uri = Uri(
|
||||
scheme: 'mailto',
|
||||
path: 'support@requesn.com',
|
||||
path: 'support@re-quest.co',
|
||||
queryParameters: {
|
||||
'subject': 'Support Request',
|
||||
},
|
||||
);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
}
|
||||
launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1158,7 +1158,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
|
||||
void _showImageFullScreen(BuildContext context, String imageUrl) {
|
||||
final isDirectUrl = imageUrl.startsWith('http://') || imageUrl.startsWith('https://');
|
||||
|
||||
Navigator.of(context).push(
|
||||
Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
|
||||
@@ -173,7 +173,7 @@ class _BillingTabState extends ConsumerState<BillingTab> {
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '\$${plan!['amount']}',
|
||||
text: '\$${_formatAmount(plan!['amount'])}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 35,
|
||||
@@ -340,25 +340,24 @@ class _BillingTabState extends ConsumerState<BillingTab> {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 155,
|
||||
height: 43,
|
||||
child: ElevatedButton(
|
||||
onPressed: _startCheckout,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accentOrange,
|
||||
side: const BorderSide(color: AppColors.accentOrange),
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: const Text('Get Premium Access',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white)),
|
||||
ElevatedButton(
|
||||
onPressed: _startCheckout,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.accentOrange,
|
||||
side: const BorderSide(color: AppColors.accentOrange),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: const Text('Get Premium Access',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Fractul',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white)),
|
||||
),
|
||||
const Spacer(),
|
||||
ClipRRect(
|
||||
@@ -556,7 +555,7 @@ class _BillingTabState extends ConsumerState<BillingTab> {
|
||||
final repo = ref.read(profileRepositoryProvider);
|
||||
final url = await repo.createPortalSession();
|
||||
if (!mounted) return;
|
||||
await Navigator.of(context).push<Map<String, dynamic>?>(
|
||||
await Navigator.of(context, rootNavigator: true).push<Map<String, dynamic>?>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StripeCheckoutScreen(
|
||||
url: url,
|
||||
@@ -583,7 +582,7 @@ class _BillingTabState extends ConsumerState<BillingTab> {
|
||||
final planId = plans.first['id'] as String;
|
||||
final url = await repo.createCheckoutSession(planId);
|
||||
if (!mounted) return;
|
||||
final result = await Navigator.of(context).push<Map<String, dynamic>?>(
|
||||
final result = await Navigator.of(context, rootNavigator: true).push<Map<String, dynamic>?>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StripeCheckoutScreen(
|
||||
url: url,
|
||||
@@ -643,6 +642,16 @@ class _BillingTabState extends ConsumerState<BillingTab> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stripe amounts are in cents — convert to dollars.
|
||||
String _formatAmount(dynamic amount) {
|
||||
final cents = amount is num ? amount : num.tryParse('$amount') ?? 0;
|
||||
final dollars = cents / 100;
|
||||
// Show whole dollars if no cents, otherwise 2 decimal places
|
||||
return dollars == dollars.roundToDouble()
|
||||
? dollars.toInt().toString()
|
||||
: dollars.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
String _formatDate(String dateStr) {
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
|
||||
Reference in New Issue
Block a user