feat: enhance profile subscription UI with new assets, font adjustments, and SVG support.

This commit is contained in:
pradeepkumar
2026-03-09 07:40:57 +05:30
parent ef1df39520
commit 88c71d6b6e
8 changed files with 485 additions and 303 deletions

View File

@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
import 'package:real_estate_mobile/core/widgets/s3_image.dart';
@@ -1085,7 +1086,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Subscription & Payments',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 22,
fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
@@ -1112,7 +1113,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Current Plan',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
@@ -1265,132 +1266,206 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Annual Membership Plans',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 16),
// Plan card matching Figma
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.5),
width: 0.5,
Stack(
clipBehavior: Clip.none,
children: [
Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.5),
width: 0.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Annual Subscription badge
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(15),
),
child: const Text(
'Annual Subscription',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: Colors.white,
),
),
),
const SizedBox(height: 24),
// Professional Annual Plan
const Text(
'Professional Annual Plan',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
const Text(
'Complete solutions for real estate\nagents and lenders to grow their\nbusiness.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 20),
// Price
RichText(
text: TextSpan(
children: [
const TextSpan(
text: '\$499',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 35,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
const TextSpan(
text: ' / ',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
TextSpan(
text: 'Year',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.black.withValues(alpha: 0.5),
),
),
],
),
),
const SizedBox(height: 20),
// Get Premium Access button + illustration
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)),
),
),
const Spacer(),
// Wallet illustration
ClipRRect(
borderRadius: BorderRadius.circular(70),
child: Image.asset(
'assets/icons/wallet_illustration.jpg',
width: 120,
height: 120,
fit: BoxFit.cover,
),
),
],
),
const SizedBox(height: 16),
// Include Features
const Text(
'Include Features',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 16),
..._buildPlanFeatures(),
],
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Annual Subscription badge
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(15),
// "Best value" ribbon on top-right corner
Positioned(
top: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
),
child: const Text(
'Annual Subscription',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: Colors.white,
child: SizedBox(
width: 62,
height: 134,
child: Stack(
children: [
CustomPaint(
size: const Size(62, 134),
painter: _RibbonPainter(),
),
// Orange circle at top of ribbon
Positioned(
top: 6,
left: 12,
child: Container(
width: 15,
height: 14,
decoration: const BoxDecoration(
color: AppColors.accentOrange,
shape: BoxShape.circle,
),
),
),
// "Best value" text rotated ~67 degrees
Positioned(
top: 24,
left: 6,
child: Transform.rotate(
angle: 1.17,
child: const Text(
'Best value',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
color: Colors.white,
),
),
),
),
],
),
),
),
const SizedBox(height: 24),
// Professional Annual Plan
const Text(
'Professional Annual Plan',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
const Text(
'Complete solutions for real estate agents and lenders to grow their business.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 20),
// Price
RichText(
text: TextSpan(
children: [
const TextSpan(
text: '\$499',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 35,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const TextSpan(
text: ' / ',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
TextSpan(
text: 'Year',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
],
),
),
const SizedBox(height: 20),
// Get Premium Access button
SizedBox(
width: 155,
child: ElevatedButton(
onPressed: _startCheckout,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
),
child: const Text('Get Premium Access',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white)),
),
),
const SizedBox(height: 24),
// Include Features
const Text(
'Include Features',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 16),
..._buildPlanFeatures(),
],
),
),
],
),
],
@@ -1398,7 +1473,8 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
// Need Help section matching Figma
Container(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
clipBehavior: Clip.antiAlias,
padding: const EdgeInsets.fromLTRB(20, 30, 20, 0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
@@ -1419,7 +1495,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
),
const SizedBox(height: 10),
const Text(
'Our billing experts are here to help you\nwith any questions about your plan.',
'Our billing experts are here to help you\nwith any questions\n about your plan.',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'SourceSerif4',
@@ -1439,7 +1515,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'support@re-quest.co',
'support@example.com',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
@@ -1449,15 +1525,26 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
decorationColor: AppColors.accentOrange,
),
),
const SizedBox(width: 6),
const SizedBox(width: 4),
Transform.rotate(
angle: 0.7, // ~40 degrees
angle: 0.72, // ~41 degrees matching Figma
child: const Icon(Icons.arrow_upward,
size: 16, color: AppColors.accentOrange),
),
],
),
),
const SizedBox(height: 20),
// Businessman illustration
ClipRRect(
borderRadius: BorderRadius.circular(57),
child: Image.asset(
'assets/icons/need_help_illustration.jpg',
width: 261,
height: 261,
fit: BoxFit.cover,
),
),
],
),
),
@@ -1467,29 +1554,25 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
List<Widget> _buildPlanFeatures() {
const features = [
'Agent Co-Marketing',
'Priority 24/7 Support',
'Whitelabel Client Portals',
'Advanced CRM Tools & Analytics',
('Agent -Co-Marketing', true), // first uses diamond icon
('Priority 24/7 Support', false),
('Whitelabel Client Portals', false),
('Advanced CRM Tools & Analytics', false),
];
return features
.map((f) => Padding(
padding: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.only(bottom: 20),
child: Row(
children: [
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppColors.accentOrange, width: 1.5),
),
child: const Icon(Icons.check,
size: 14, color: AppColors.accentOrange),
SvgPicture.asset(
f.$2
? 'assets/icons/diamond_icon.svg'
: 'assets/icons/feature_check_icon.svg',
width: 18,
height: 18,
),
const SizedBox(width: 12),
Text(f,
Text(f.$1,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
@@ -1634,121 +1717,156 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
return ListView(
padding: const EdgeInsets.fromLTRB(24, 10, 24, 30),
children: [
const Text(
'Add Testimonials',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
// Title matching Figma
const Center(
child: Text(
'Add Testimonials',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
const SizedBox(height: 8),
const Text(
'Generate a link for your clients to submit testimonials. Share it via email or messaging.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
const SizedBox(height: 10),
const Center(
child: Text(
'What people are saying about you',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
const SizedBox(height: 24),
const SizedBox(height: 20),
// Generate link section
_buildSectionCard(
title: 'Testimonial Collection Link',
subtitle: 'Share this link with your clients',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_testimonialLink != null) ...[
// Create Testimonials section
const Center(
child: Text(
'Create Testimonials',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
const SizedBox(height: 16),
// Generate Link button (full-width, orange)
SizedBox(
width: double.infinity,
height: 43,
child: ElevatedButton(
onPressed: _isGeneratingLink ? null : _generateTestimonialLink,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
),
child: _isGeneratingLink
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('Generate Link',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white)),
),
),
const SizedBox(height: 16),
// Link URL field with Copy button
if (_testimonialLink != null)
Container(
height: 43,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
),
),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
_testimonialLink!,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w300,
color: Colors.black,
),
),
),
),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.primaryDark.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1)),
),
child: Text(
_testimonialLink!,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 13,
color: AppColors.primaryDark,
),
),
width: 1,
height: 43,
color: AppColors.primaryDark.withValues(alpha: 0.1),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
Clipboard.setData(
ClipboardData(text: _testimonialLink!));
setState(() => _linkCopied = true);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _linkCopied = false);
});
},
icon: Icon(
_linkCopied ? Icons.check : Icons.copy, size: 16),
label: Text(_linkCopied ? 'Copied!' : 'Copy Link'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.accentOrange,
side: const BorderSide(color: AppColors.accentOrange),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: _testimonialLink!));
setState(() => _linkCopied = true);
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _linkCopied = false);
});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
children: [
Icon(
_linkCopied ? Icons.check : Icons.copy_outlined,
size: 18,
color: Colors.black,
),
const SizedBox(width: 4),
Text(
_linkCopied ? 'Copied!' : 'Copy',
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: Colors.black,
),
),
],
),
),
),
] else ...[
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isGeneratingLink ? null : _generateTestimonialLink,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange,
foregroundColor: AppColors.primaryDark,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15)),
),
child: _isGeneratingLink
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2, color: AppColors.primaryDark))
: const Text('Generate Link',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700)),
),
),
],
],
),
),
),
const SizedBox(height: 24),
const SizedBox(height: 30),
// Testimonials list header
// Reviews header with sort
Row(
children: [
const Text(
'Received Testimonials',
'Reviews',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
const Spacer(),
// Sort toggle
GestureDetector(
onTap: () {
setState(() {
@@ -1759,25 +1877,26 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
},
child: Row(
children: [
Icon(Icons.sort,
size: 16,
color: AppColors.primaryDark.withValues(alpha: 0.6)),
const SizedBox(width: 4),
Text(
_testimonialSort == 'latest' ? 'Latest' : 'Oldest',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 13,
color: AppColors.primaryDark.withValues(alpha: 0.6),
'Sort By : ${_testimonialSort == 'latest' ? 'Latest' : 'Oldest'}',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
const SizedBox(width: 4),
const Icon(Icons.arrow_downward,
size: 14, color: Colors.black),
],
),
),
],
),
const SizedBox(height: 12),
const SizedBox(height: 16),
// Reviews list in grey container
if (_isLoadingTestimonials)
const Center(
child: Padding(
@@ -1786,7 +1905,11 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
))
else if (_testimonials.isEmpty)
Container(
padding: const EdgeInsets.symmetric(vertical: 40),
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9).withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(7),
),
child: Center(
child: Column(
children: [
@@ -1818,8 +1941,18 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
),
)
else
..._testimonials
.map((t) => _buildTestimonialCard(t)),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9).withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(7),
),
child: Column(
children: _testimonials
.map((t) => _buildTestimonialCard(t))
.toList(),
),
),
],
);
}
@@ -1836,76 +1969,62 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1)),
boxShadow: [
BoxShadow(
color: const Color(0xFFD9D9D9).withValues(alpha: 0.3),
offset: const Offset(0, 4),
blurRadius: 10,
),
],
borderRadius: BorderRadius.circular(7),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Stars + date
// Stars
Row(
children: [
...List.generate(
5,
(i) => Icon(
i < rating ? Icons.star : Icons.star_border,
size: 16,
color: i < rating
? AppColors.accentOrange
: AppColors.primaryDark.withValues(alpha: 0.3),
),
children: List.generate(
5,
(i) => Icon(
i < rating ? Icons.star : Icons.star_border,
size: 16,
color: i < rating
? AppColors.accentOrange
: AppColors.primaryDark.withValues(alpha: 0.3),
),
const Spacer(),
if (createdAt != null)
Text(
_formatRelativeTime(createdAt),
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 12,
color: AppColors.primaryDark.withValues(alpha: 0.5),
),
),
],
),
),
const SizedBox(height: 10),
// Text
// Review text
Text(
text,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
color: AppColors.primaryDark,
color: Colors.black,
height: 1.5,
),
),
const SizedBox(height: 12),
// Author
Text(
authorName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
// Author line: "Name , Role · time ago"
RichText(
text: TextSpan(
children: [
TextSpan(
text: authorName,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
if (authorRole.isNotEmpty || createdAt != null)
TextSpan(
text: ' , ${authorRole.isNotEmpty ? authorRole : ''}${createdAt != null ? ' · ${_formatRelativeTime(createdAt)}' : ''}',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w300,
color: AppColors.primaryDark,
),
),
],
),
),
if (authorRole.isNotEmpty)
Text(
authorRole,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 13,
color: AppColors.primaryDark.withValues(alpha: 0.6),
),
),
],
),
);
@@ -2526,3 +2645,27 @@ class _TabItem {
final IconData icon;
const _TabItem({required this.label, required this.icon});
}
/// Custom painter for the "Best value" ribbon triangle (matches Figma SVG).
/// Ribbon painter matching Figma SVG: M28.5 0H0L62 134V73.99L28.5 0Z
/// Shape: a pennant/flag anchored top-right of the plan card.
class _RibbonPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = AppColors.accentOrange;
// Figma SVG path within 62×134 viewport:
// M28.5,0 → H0 → L62,134 → V73.99 → L28.5,0 → Z
final w = size.width; // 62
final h = size.height; // 134
final path = Path()
..moveTo(w * (28.5 / 62), 0) // (28.5, 0) top point of ribbon
..lineTo(0, 0) // (0, 0) top-left corner
..lineTo(w, h) // (62, 134) bottom-right tip
..lineTo(w, h * (73.99 / 134)) // (62, ~74) right edge notch
..close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}