diff --git a/.gitignore b/.gitignore index 3820a95..ce633d1 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Signing +/android/keystores/ +/android/key.properties diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index dd59616..b766aaf 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,3 +1,6 @@ +import java.util.Properties +import java.io.FileInputStream + plugins { id("com.android.application") id("kotlin-android") @@ -6,6 +9,12 @@ plugins { id("com.google.gms.google-services") } +val keyProperties = Properties() +val keyPropertiesFile = rootProject.file("key.properties") +if (keyPropertiesFile.exists()) { + keyProperties.load(FileInputStream(keyPropertiesFile)) +} + android { namespace = "com.requestn.real_estate_mobile" compileSdk = flutter.compileSdkVersion @@ -29,6 +38,21 @@ android { 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" productFlavors { @@ -41,11 +65,13 @@ android { dimension = "flavor-type" applicationIdSuffix = ".demo" resValue("string", "app_name", "RE-QuestN Demo") + signingConfig = signingConfigs.getByName("demoRelease") } create("dev") { dimension = "flavor-type" applicationIdSuffix = ".dev" resValue("string", "app_name", "RE-QuestN Dev") + signingConfig = signingConfigs.getByName("devRelease") } } diff --git a/assets/icons/diamond_icon.svg b/assets/icons/diamond_icon.svg new file mode 100644 index 0000000..a035434 --- /dev/null +++ b/assets/icons/diamond_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/feature_check_icon.svg b/assets/icons/feature_check_icon.svg new file mode 100644 index 0000000..e1ecff4 --- /dev/null +++ b/assets/icons/feature_check_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/need_help_illustration.jpg b/assets/icons/need_help_illustration.jpg new file mode 100644 index 0000000..8e3affb Binary files /dev/null and b/assets/icons/need_help_illustration.jpg differ diff --git a/assets/icons/wallet_illustration.jpg b/assets/icons/wallet_illustration.jpg new file mode 100644 index 0000000..47d9768 Binary files /dev/null and b/assets/icons/wallet_illustration.jpg differ diff --git a/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart b/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart index 95d563a..a4b025f 100644 --- a/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart +++ b/lib/features/agents/presentation/screens/agent_edit_profile_screen.dart @@ -343,11 +343,13 @@ class _AgentEditProfileScreenState return Scaffold( backgroundColor: const Color(0xFFF5F7FA), - body: Column( - children: [ - // Header with circular back button (SVG) matching Figma - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + 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( @@ -371,8 +373,9 @@ class _AgentEditProfileScreenState ], ), ), - Expanded(child: _buildBody(asyncData)), - ], + Expanded(child: _buildBody(asyncData)), + ], + ), ), ); } diff --git a/lib/features/profile/presentation/screens/profile_settings_screen.dart b/lib/features/profile/presentation/screens/profile_settings_screen.dart index 71182a6..dfef6ac 100644 --- a/lib/features/profile/presentation/screens/profile_settings_screen.dart +++ b/lib/features/profile/presentation/screens/profile_settings_screen.dart @@ -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 { 'Subscription & Payments', style: TextStyle( fontFamily: 'Fractul', - fontSize: 22, + fontSize: 25, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), @@ -1112,7 +1113,7 @@ class _ProfileSettingsScreenState extends ConsumerState { 'Current Plan', style: TextStyle( fontFamily: 'Fractul', - fontSize: 18, + fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), @@ -1265,132 +1266,206 @@ class _ProfileSettingsScreenState extends ConsumerState { '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 { // 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 { ), 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 { 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 { 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 { List _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 { 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 { }, 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 { )) 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 { ), ) 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 { 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; +}