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

4
.gitignore vendored
View File

@@ -43,3 +43,7 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# Signing
/android/keystores/
/android/key.properties

View File

@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@@ -6,6 +9,12 @@ plugins {
id("com.google.gms.google-services") id("com.google.gms.google-services")
} }
val keyProperties = Properties()
val keyPropertiesFile = rootProject.file("key.properties")
if (keyPropertiesFile.exists()) {
keyProperties.load(FileInputStream(keyPropertiesFile))
}
android { android {
namespace = "com.requestn.real_estate_mobile" namespace = "com.requestn.real_estate_mobile"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -29,6 +38,21 @@ android {
versionName = flutter.versionName versionName = flutter.versionName
} }
signingConfigs {
create("devRelease") {
storeFile = file(keyProperties.getProperty("devStoreFile", ""))
storePassword = keyProperties.getProperty("devStorePassword", "")
keyAlias = keyProperties.getProperty("devKeyAlias", "")
keyPassword = keyProperties.getProperty("devKeyPassword", "")
}
create("demoRelease") {
storeFile = file(keyProperties.getProperty("demoStoreFile", ""))
storePassword = keyProperties.getProperty("demoStorePassword", "")
keyAlias = keyProperties.getProperty("demoKeyAlias", "")
keyPassword = keyProperties.getProperty("demoKeyPassword", "")
}
}
flavorDimensions += "flavor-type" flavorDimensions += "flavor-type"
productFlavors { productFlavors {
@@ -41,11 +65,13 @@ android {
dimension = "flavor-type" dimension = "flavor-type"
applicationIdSuffix = ".demo" applicationIdSuffix = ".demo"
resValue("string", "app_name", "RE-QuestN Demo") resValue("string", "app_name", "RE-QuestN Demo")
signingConfig = signingConfigs.getByName("demoRelease")
} }
create("dev") { create("dev") {
dimension = "flavor-type" dimension = "flavor-type"
applicationIdSuffix = ".dev" applicationIdSuffix = ".dev"
resValue("string", "app_name", "RE-QuestN Dev") resValue("string", "app_name", "RE-QuestN Dev")
signingConfig = signingConfigs.getByName("devRelease")
} }
} }

View File

@@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.00025 5.49532H15.9652M12.224 1C12.4563 1 12.6854 1.05416 12.8932 1.15819C13.101 1.26223 13.2818 1.41328 13.4212 1.59938L15.6659 4.59625C15.8584 4.85336 15.9633 5.16562 15.9651 5.48696C15.9668 5.80831 15.8654 6.1217 15.6757 6.38089L9.69714 14.6118C9.55853 14.8047 9.37609 14.9619 9.16486 15.0702C8.95364 15.1786 8.7197 15.2352 8.48236 15.2352C8.24502 15.2352 8.01108 15.1786 7.79986 15.0702C7.58864 14.9619 7.40619 14.8047 7.26758 14.6118L1.28907 6.38089C1.09947 6.12162 0.99814 5.80818 1.00003 5.48684C1.00191 5.1655 1.10692 4.85328 1.29955 4.59625L3.5428 1.60162C3.68211 1.41491 3.863 1.2633 4.07109 1.15886C4.27917 1.05442 4.50873 1.00003 4.74149 1H12.224Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 856 B

View File

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.97899 0C4.02782 0 0 4.03305 0 8.99063C0 13.9482 4.02782 17.9813 8.97899 17.9813C13.9301 17.9813 17.958 13.9482 17.958 8.99063H15.9377C15.9377 12.8326 12.816 15.9584 8.97899 15.9584C5.14197 15.9584 2.02027 12.8326 2.02027 8.99063C2.02027 5.14863 5.14197 2.02289 8.97899 2.02289V0ZM14.516 1.93523L8.90042 8.95617L6.27631 6.80291L4.77832 8.62801L8.32576 11.541C8.44628 11.6405 8.58538 11.715 8.73496 11.7602C8.88453 11.8053 9.04159 11.8202 9.19697 11.804C9.35235 11.7877 9.50295 11.7407 9.63998 11.6655C9.77701 11.5904 9.89773 11.4887 9.99511 11.3664L16.3582 3.41194L14.516 1.93523Z" fill="#43A047"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

View File

@@ -343,11 +343,13 @@ class _AgentEditProfileScreenState
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: const Color(0xFFF5F7FA),
body: Column( body: SafeArea(
children: [ bottom: false,
// Header with circular back button (SVG) matching Figma child: Column(
Padding( children: [
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), // Header with circular back button (SVG) matching Figma
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: Row( child: Row(
children: [ children: [
GestureDetector( GestureDetector(
@@ -371,8 +373,9 @@ class _AgentEditProfileScreenState
], ],
), ),
), ),
Expanded(child: _buildBody(asyncData)), Expanded(child: _buildBody(asyncData)),
], ],
),
), ),
); );
} }

View File

@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.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/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart';
@@ -1085,7 +1086,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Subscription & Payments', 'Subscription & Payments',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 22, fontSize: 25,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
@@ -1112,7 +1113,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Current Plan', 'Current Plan',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 18, fontSize: 20,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
@@ -1265,132 +1266,206 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
'Annual Membership Plans', 'Annual Membership Plans',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 18, fontSize: 20,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.primaryDark, color: AppColors.primaryDark,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Plan card matching Figma // Plan card matching Figma
Container( Stack(
padding: const EdgeInsets.all(20), clipBehavior: Clip.none,
decoration: BoxDecoration( children: [
borderRadius: BorderRadius.circular(10), Container(
border: Border.all( padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
color: AppColors.primaryDark.withValues(alpha: 0.5), decoration: BoxDecoration(
width: 0.5, 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(),
],
),
), ),
), // "Best value" ribbon on top-right corner
child: Column( Positioned(
crossAxisAlignment: CrossAxisAlignment.start, top: 0,
children: [ right: 0,
// Annual Subscription badge child: ClipRRect(
Container( borderRadius: const BorderRadius.only(
padding: topRight: Radius.circular(10),
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(15),
), ),
child: const Text( child: SizedBox(
'Annual Subscription', width: 62,
style: TextStyle( height: 134,
fontFamily: 'SourceSerif4', child: Stack(
fontSize: 14, children: [
color: Colors.white, 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 // Need Help section matching Figma
Container( Container(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), clipBehavior: Clip.antiAlias,
padding: const EdgeInsets.fromLTRB(20, 30, 20, 0),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(15),
border: Border.all( border: Border.all(
@@ -1419,7 +1495,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
const Text( 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, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
@@ -1439,7 +1515,7 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'support@re-quest.co', 'support@example.com',
style: TextStyle( style: TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -1449,15 +1525,26 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
decorationColor: AppColors.accentOrange, decorationColor: AppColors.accentOrange,
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 4),
Transform.rotate( Transform.rotate(
angle: 0.7, // ~40 degrees angle: 0.72, // ~41 degrees matching Figma
child: const Icon(Icons.arrow_upward, child: const Icon(Icons.arrow_upward,
size: 16, color: AppColors.accentOrange), 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() { List<Widget> _buildPlanFeatures() {
const features = [ const features = [
'Agent Co-Marketing', ('Agent -Co-Marketing', true), // first uses diamond icon
'Priority 24/7 Support', ('Priority 24/7 Support', false),
'Whitelabel Client Portals', ('Whitelabel Client Portals', false),
'Advanced CRM Tools & Analytics', ('Advanced CRM Tools & Analytics', false),
]; ];
return features return features
.map((f) => Padding( .map((f) => Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 20),
child: Row( child: Row(
children: [ children: [
Container( SvgPicture.asset(
width: 22, f.$2
height: 22, ? 'assets/icons/diamond_icon.svg'
decoration: BoxDecoration( : 'assets/icons/feature_check_icon.svg',
shape: BoxShape.circle, width: 18,
border: Border.all( height: 18,
color: AppColors.accentOrange, width: 1.5),
),
child: const Icon(Icons.check,
size: 14, color: AppColors.accentOrange),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(f, Text(f.$1,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
@@ -1634,121 +1717,156 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
return ListView( return ListView(
padding: const EdgeInsets.fromLTRB(24, 10, 24, 30), padding: const EdgeInsets.fromLTRB(24, 10, 24, 30),
children: [ children: [
const Text( // Title matching Figma
'Add Testimonials', const Center(
style: TextStyle( child: Text(
fontFamily: 'Fractul', 'Add Testimonials',
fontSize: 15, style: TextStyle(
fontWeight: FontWeight.w700, fontFamily: 'Fractul',
color: AppColors.primaryDark, fontSize: 25,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 10),
const Text( const Center(
'Generate a link for your clients to submit testimonials. Share it via email or messaging.', child: Text(
style: TextStyle( 'What people are saying about you',
fontFamily: 'SourceSerif4', textAlign: TextAlign.center,
fontSize: 14, style: TextStyle(
fontWeight: FontWeight.w400, fontFamily: 'SourceSerif4',
color: AppColors.primaryDark, fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 20),
// Generate link section // Create Testimonials section
_buildSectionCard( const Center(
title: 'Testimonial Collection Link', child: Text(
subtitle: 'Share this link with your clients', 'Create Testimonials',
child: Column( style: TextStyle(
crossAxisAlignment: CrossAxisAlignment.start, fontFamily: 'Fractul',
children: [ fontSize: 25,
if (_testimonialLink != null) ...[ 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( Container(
width: double.infinity, width: 1,
padding: const EdgeInsets.all(12), height: 43,
decoration: BoxDecoration( color: AppColors.primaryDark.withValues(alpha: 0.1),
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,
),
),
), ),
const SizedBox(height: 12), GestureDetector(
SizedBox( onTap: () {
width: double.infinity, Clipboard.setData(
child: OutlinedButton.icon( ClipboardData(text: _testimonialLink!));
onPressed: () { setState(() => _linkCopied = true);
Clipboard.setData( Future.delayed(const Duration(seconds: 2), () {
ClipboardData(text: _testimonialLink!)); if (mounted) setState(() => _linkCopied = false);
setState(() => _linkCopied = true); });
Future.delayed(const Duration(seconds: 2), () { },
if (mounted) setState(() => _linkCopied = false); child: Padding(
}); padding: const EdgeInsets.symmetric(horizontal: 10),
}, child: Row(
icon: Icon( children: [
_linkCopied ? Icons.check : Icons.copy, size: 16), Icon(
label: Text(_linkCopied ? 'Copied!' : 'Copy Link'), _linkCopied ? Icons.check : Icons.copy_outlined,
style: OutlinedButton.styleFrom( size: 18,
foregroundColor: AppColors.accentOrange, color: Colors.black,
side: const BorderSide(color: AppColors.accentOrange), ),
shape: RoundedRectangleBorder( const SizedBox(width: 4),
borderRadius: BorderRadius.circular(10)), 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: 30),
const SizedBox(height: 24),
// Testimonials list header // Reviews header with sort
Row( Row(
children: [ children: [
const Text( const Text(
'Received Testimonials', 'Reviews',
style: TextStyle( style: TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 15, fontSize: 14,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w500,
color: AppColors.primaryDark, color: Colors.black,
), ),
), ),
const Spacer(), const Spacer(),
// Sort toggle
GestureDetector( GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
@@ -1759,25 +1877,26 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
}, },
child: Row( child: Row(
children: [ children: [
Icon(Icons.sort,
size: 16,
color: AppColors.primaryDark.withValues(alpha: 0.6)),
const SizedBox(width: 4),
Text( Text(
_testimonialSort == 'latest' ? 'Latest' : 'Oldest', 'Sort By : ${_testimonialSort == 'latest' ? 'Latest' : 'Oldest'}',
style: TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'Fractul',
fontSize: 13, fontSize: 14,
color: AppColors.primaryDark.withValues(alpha: 0.6), 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) if (_isLoadingTestimonials)
const Center( const Center(
child: Padding( child: Padding(
@@ -1786,7 +1905,11 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
)) ))
else if (_testimonials.isEmpty) else if (_testimonials.isEmpty)
Container( 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: Center(
child: Column( child: Column(
children: [ children: [
@@ -1818,8 +1941,18 @@ class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
), ),
) )
else else
..._testimonials Container(
.map((t) => _buildTestimonialCard(t)), 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), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(7),
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,
),
],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Stars + date // Stars
Row( Row(
children: [ children: List.generate(
...List.generate( 5,
5, (i) => Icon(
(i) => Icon( i < rating ? Icons.star : Icons.star_border,
i < rating ? Icons.star : Icons.star_border, size: 16,
size: 16, color: i < rating
color: i < rating ? AppColors.accentOrange
? AppColors.accentOrange : AppColors.primaryDark.withValues(alpha: 0.3),
: 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), const SizedBox(height: 10),
// Text // Review text
Text( Text(
text, text,
style: const TextStyle( style: const TextStyle(
fontFamily: 'SourceSerif4', fontFamily: 'SourceSerif4',
fontSize: 14, fontSize: 14,
color: AppColors.primaryDark, color: Colors.black,
height: 1.5, height: 1.5,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Author // Author line: "Name , Role · time ago"
Text( RichText(
authorName, text: TextSpan(
style: const TextStyle( children: [
fontFamily: 'Fractul', TextSpan(
fontSize: 14, text: authorName,
fontWeight: FontWeight.w600, style: const TextStyle(
color: AppColors.primaryDark, 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; final IconData icon;
const _TabItem({required this.label, required this.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;
}