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:
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user