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:
pradeepkumar
2026-03-15 00:57:14 +05:30
parent d5a92e0b94
commit 84e9cd9680
5 changed files with 99 additions and 70 deletions

View File

@@ -47,6 +47,10 @@
</array> </array>
</dict> </dict>
</dict> </dict>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>mailto</string>
</array>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photo library to set your profile photo.</string> <string>We need access to your photo library to set your profile photo.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>

View File

@@ -341,11 +341,7 @@ class _AgentEditProfileScreenState
Widget build(BuildContext context) { Widget build(BuildContext context) {
final asyncData = ref.watch(_editProfileDataProvider); final asyncData = ref.watch(_editProfileDataProvider);
return Scaffold( return Column(
backgroundColor: const Color(0xFFF5F7FA),
body: SafeArea(
bottom: false,
child: Column(
children: [ children: [
// Header with circular back button (SVG) matching Figma // Header with circular back button (SVG) matching Figma
Padding( Padding(
@@ -375,8 +371,6 @@ class _AgentEditProfileScreenState
), ),
Expanded(child: _buildBody(asyncData)), Expanded(child: _buildBody(asyncData)),
], ],
),
),
); );
} }
@@ -667,6 +661,13 @@ class _AgentEditProfileScreenState
onTap: () { onTap: () {
setState(() { setState(() {
entries.removeAt(i); 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( child: const Icon(
@@ -688,6 +689,7 @@ class _AgentEditProfileScreenState
entries[i][slug] = val; entries[i][slug] = val;
}); });
}, },
controllerKeySuffix: 'entry$i',
); );
}), }),
], ],
@@ -731,6 +733,7 @@ class _AgentEditProfileScreenState
Map<String, dynamic> field, { Map<String, dynamic> field, {
dynamic Function()? getValue, dynamic Function()? getValue,
void Function(dynamic)? setValue, void Function(dynamic)? setValue,
String? controllerKeySuffix,
}) { }) {
final slug = field['slug'] as String? ?? ''; final slug = field['slug'] as String? ?? '';
final fieldType = field['fieldType'] as String? ?? 'TEXT'; final fieldType = field['fieldType'] as String? ?? 'TEXT';
@@ -770,6 +773,7 @@ class _AgentEditProfileScreenState
placeholder, placeholder,
validation, validation,
false, false,
controllerKeySuffix: controllerKeySuffix,
); );
break; break;
case 'TEXTAREA': case 'TEXTAREA':
@@ -780,6 +784,7 @@ class _AgentEditProfileScreenState
placeholder, placeholder,
validation, validation,
true, true,
controllerKeySuffix: controllerKeySuffix,
); );
break; break;
case 'NUMBER': case 'NUMBER':
@@ -789,6 +794,7 @@ class _AgentEditProfileScreenState
updateValue, updateValue,
placeholder, placeholder,
validation, validation,
controllerKeySuffix: controllerKeySuffix,
); );
break; break;
case 'DATE': case 'DATE':
@@ -851,6 +857,7 @@ class _AgentEditProfileScreenState
currentValue, currentValue,
updateValue, updateValue,
placeholder, placeholder,
controllerKeySuffix: controllerKeySuffix,
); );
break; break;
case 'RANGE': case 'RANGE':
@@ -870,6 +877,7 @@ class _AgentEditProfileScreenState
placeholder, placeholder,
validation, validation,
false, false,
controllerKeySuffix: controllerKeySuffix,
); );
} }
@@ -937,9 +945,12 @@ class _AgentEditProfileScreenState
void Function(dynamic) setValue, void Function(dynamic) setValue,
String placeholder, String placeholder,
Map<String, dynamic>? validation, Map<String, dynamic>? validation,
bool multiline, bool multiline, {
) { String? controllerKeySuffix,
final key = '${slug}_text'; }) {
final key = controllerKeySuffix != null
? '${slug}_${controllerKeySuffix}_text'
: '${slug}_text';
if (!_tagControllers.containsKey(key)) { if (!_tagControllers.containsKey(key)) {
final controller = TextEditingController(text: '${getValue() ?? ''}'); final controller = TextEditingController(text: '${getValue() ?? ''}');
_tagControllers[key] = controller; _tagControllers[key] = controller;
@@ -974,9 +985,12 @@ class _AgentEditProfileScreenState
dynamic Function() getValue, dynamic Function() getValue,
void Function(dynamic) setValue, void Function(dynamic) setValue,
String placeholder, String placeholder,
Map<String, dynamic>? validation, Map<String, dynamic>? validation, {
) { String? controllerKeySuffix,
final key = '${slug}_number'; }) {
final key = controllerKeySuffix != null
? '${slug}_${controllerKeySuffix}_number'
: '${slug}_number';
if (!_tagControllers.containsKey(key)) { if (!_tagControllers.containsKey(key)) {
final val = getValue(); final val = getValue();
final controller = TextEditingController(text: val != null ? '$val' : ''); final controller = TextEditingController(text: val != null ? '$val' : '');
@@ -1377,16 +1391,20 @@ class _AgentEditProfileScreenState
String slug, String slug,
dynamic Function() getValue, dynamic Function() getValue,
void Function(dynamic) setValue, void Function(dynamic) setValue,
String placeholder, String placeholder, {
) { String? controllerKeySuffix,
}) {
final tags = List<String>.from((getValue() ?? <String>[]) as List); 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(); final controller = TextEditingController();
_tagControllers[slug] = controller; _tagControllers[tagKey] = controller;
_controllers.add(controller); _controllers.add(controller);
} }
final controller = _tagControllers[slug]!; final controller = _tagControllers[tagKey]!;
void addTag(String text) { void addTag(String text) {
final trimmed = text.trim(); final trimmed = text.trim();

View File

@@ -400,17 +400,15 @@ class _StillNeedHelpSection extends ConsumerWidget {
_buildSupportButton( _buildSupportButton(
icon: Icons.email_outlined, icon: Icons.email_outlined,
label: 'Email Support', label: 'Email Support',
onTap: () async { onTap: () {
final uri = Uri( final uri = Uri(
scheme: 'mailto', scheme: 'mailto',
path: 'support@requesn.com', path: 'support@re-quest.co',
queryParameters: { queryParameters: {
'subject': 'Support Request', 'subject': 'Support Request',
}, },
); );
if (await canLaunchUrl(uri)) { launchUrl(uri, mode: LaunchMode.externalApplication);
await launchUrl(uri);
}
}, },
), ),
], ],

View File

@@ -1158,7 +1158,7 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
void _showImageFullScreen(BuildContext context, String imageUrl) { void _showImageFullScreen(BuildContext context, String imageUrl) {
final isDirectUrl = imageUrl.startsWith('http://') || imageUrl.startsWith('https://'); final isDirectUrl = imageUrl.startsWith('http://') || imageUrl.startsWith('https://');
Navigator.of(context).push( Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => Scaffold( builder: (_) => Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,

View File

@@ -173,7 +173,7 @@ class _BillingTabState extends ConsumerState<BillingTab> {
text: TextSpan( text: TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: '\$${plan!['amount']}', text: '\$${_formatAmount(plan!['amount'])}',
style: const TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 35, fontSize: 35,
@@ -340,26 +340,25 @@ class _BillingTabState extends ConsumerState<BillingTab> {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( ElevatedButton(
width: 155,
height: 43,
child: ElevatedButton(
onPressed: _startCheckout, onPressed: _startCheckout,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange, backgroundColor: AppColors.accentOrange,
side: const BorderSide(color: AppColors.accentOrange), side: const BorderSide(color: AppColors.accentOrange),
padding: EdgeInsets.zero, padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)), borderRadius: BorderRadius.circular(15)),
), ),
child: const Text('Get Premium Access', child: const Text('Get Premium Access',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: Colors.white)), color: Colors.white)),
), ),
),
const Spacer(), const Spacer(),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(70), borderRadius: BorderRadius.circular(70),
@@ -556,7 +555,7 @@ class _BillingTabState extends ConsumerState<BillingTab> {
final repo = ref.read(profileRepositoryProvider); final repo = ref.read(profileRepositoryProvider);
final url = await repo.createPortalSession(); final url = await repo.createPortalSession();
if (!mounted) return; if (!mounted) return;
await Navigator.of(context).push<Map<String, dynamic>?>( await Navigator.of(context, rootNavigator: true).push<Map<String, dynamic>?>(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => StripeCheckoutScreen( builder: (_) => StripeCheckoutScreen(
url: url, url: url,
@@ -583,7 +582,7 @@ class _BillingTabState extends ConsumerState<BillingTab> {
final planId = plans.first['id'] as String; final planId = plans.first['id'] as String;
final url = await repo.createCheckoutSession(planId); final url = await repo.createCheckoutSession(planId);
if (!mounted) return; 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( MaterialPageRoute(
builder: (_) => StripeCheckoutScreen( builder: (_) => StripeCheckoutScreen(
url: url, 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) { String _formatDate(String dateStr) {
try { try {
final date = DateTime.parse(dateStr); final date = DateTime.parse(dateStr);