diff --git a/assets/icons/arrow_right_white.svg b/assets/icons/arrow_right_white.svg new file mode 100644 index 0000000..c14ae09 --- /dev/null +++ b/assets/icons/arrow_right_white.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/onboarding_1_bg.svg b/assets/images/onboarding_1_bg.svg new file mode 100644 index 0000000..61ceef6 --- /dev/null +++ b/assets/images/onboarding_1_bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/onboarding_1_icon.svg b/assets/images/onboarding_1_icon.svg new file mode 100644 index 0000000..56bc505 --- /dev/null +++ b/assets/images/onboarding_1_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/onboarding_2_bg.svg b/assets/images/onboarding_2_bg.svg new file mode 100644 index 0000000..0572deb --- /dev/null +++ b/assets/images/onboarding_2_bg.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/onboarding_2_icon.svg b/assets/images/onboarding_2_icon.svg new file mode 100644 index 0000000..3075ca3 --- /dev/null +++ b/assets/images/onboarding_2_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/onboarding_small_icon_1.svg b/assets/images/onboarding_small_icon_1.svg new file mode 100644 index 0000000..3ef3714 --- /dev/null +++ b/assets/images/onboarding_small_icon_1.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/onboarding_small_icon_2.svg b/assets/images/onboarding_small_icon_2.svg new file mode 100644 index 0000000..66546f2 --- /dev/null +++ b/assets/images/onboarding_small_icon_2.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/onboarding_small_icon_3.svg b/assets/images/onboarding_small_icon_3.svg new file mode 100644 index 0000000..d11ed9f --- /dev/null +++ b/assets/images/onboarding_small_icon_3.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/features/onboarding/presentation/screens/onboarding_screen.dart b/lib/features/onboarding/presentation/screens/onboarding_screen.dart new file mode 100644 index 0000000..33befdd --- /dev/null +++ b/lib/features/onboarding/presentation/screens/onboarding_screen.dart @@ -0,0 +1,375 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:real_estate_mobile/core/constants/app_colors.dart'; + +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({super.key}); + + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State { + final PageController _pageController = PageController(); + int _currentPage = 0; + + static const _pages = [ + _OnboardingData( + bgAsset: 'assets/images/onboarding_1_bg.svg', + iconAsset: 'assets/images/onboarding_1_icon.svg', + iconWidth: 110, + iconHeight: 100, + title: 'Connect with Top\nProfessionals', + subtitle: + 'Agents can list properties and find qualified\nbuyers faster than ever.', + ), + _OnboardingData( + bgAsset: 'assets/images/onboarding_2_bg.svg', + iconAsset: 'assets/images/onboarding_2_icon.svg', + iconWidth: 83, + iconHeight: 75, + title: 'Real help. Real agents.\nInstantly.', + subtitle: + 'Chat with agents and get answers about\nproperties instantly.', + ), + ]; + + Future _completeOnboarding() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('onboarding_completed', true); + if (mounted) { + context.go('/home'); + } + } + + void _nextPage() { + if (_currentPage < _pages.length - 1) { + _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } else { + _completeOnboarding(); + } + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.gradientStart, // #C4D9D4 + AppColors.gradientEnd, // #F0F5FC + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + // Skip button - top right + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: _completeOnboarding, + child: const Padding( + padding: EdgeInsets.only(right: 24, top: 16), + child: Text( + 'Skip', + style: TextStyle( + fontFamily: 'Fractul', + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.accentOrange, + letterSpacing: 0.24, + ), + ), + ), + ), + ), + + // Main content - PageView + Expanded( + child: PageView.builder( + controller: _pageController, + itemCount: _pages.length, + onPageChanged: (index) { + setState(() => _currentPage = index); + }, + itemBuilder: (context, index) { + return _OnboardingPage(data: _pages[index]); + }, + ), + ), + + // Pagination dots + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_pages.length, (index) { + final isActive = index == _currentPage; + return Container( + margin: const EdgeInsets.symmetric(horizontal: 6), + width: isActive ? 32 : 8, + height: 8, + decoration: BoxDecoration( + color: isActive + ? AppColors.accentOrange + : const Color(0xFFBCBCBC), + borderRadius: BorderRadius.circular(9999), + ), + ); + }), + ), + + const SizedBox(height: 40), + + // Next / Get Started button + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 48), + child: SizedBox( + width: double.infinity, + height: 56, + child: DecoratedBox( + decoration: BoxDecoration( + color: AppColors.accentOrange, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppColors.accentOrange.withValues(alpha: 0.2), + blurRadius: 15, + offset: const Offset(0, 10), + spreadRadius: -3, + ), + BoxShadow( + color: AppColors.accentOrange.withValues(alpha: 0.2), + blurRadius: 6, + offset: const Offset(0, 4), + spreadRadius: -4, + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: _nextPage, + borderRadius: BorderRadius.circular(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _currentPage < _pages.length - 1 + ? 'Next' + : 'Get Started', + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + const SizedBox(width: 8), + SvgPicture.asset( + 'assets/icons/arrow_right_white.svg', + width: 16, + height: 16, + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _OnboardingPage extends StatelessWidget { + final _OnboardingData data; + + const _OnboardingPage({required this.data}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Illustration section - takes upper portion + Expanded( + flex: 5, + child: Padding( + padding: const EdgeInsets.only(left: 0, top: 8), + child: Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: MediaQuery.of(context).size.width - 24, + child: Stack( + alignment: Alignment.center, + children: [ + // Background oval/pill shape with orange gradient + SvgPicture.asset( + data.bgAsset, + width: 382, + height: 346, + fit: BoxFit.contain, + ), + // Blurred decoration - bottom right + Positioned( + right: 20, + bottom: 20, + child: Container( + width: 160, + height: 160, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.accentOrange.withValues(alpha: 0.2), + boxShadow: [ + BoxShadow( + color: AppColors.accentOrange + .withValues(alpha: 0.15), + blurRadius: 40, + spreadRadius: 0, + ), + ], + ), + ), + ), + // Blurred decoration - top left + Positioned( + left: 0, + top: 0, + child: Container( + width: 160, + height: 160, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.accentOrange.withValues(alpha: 0.1), + boxShadow: [ + BoxShadow( + color: AppColors.accentOrange + .withValues(alpha: 0.08), + blurRadius: 40, + spreadRadius: 0, + ), + ], + ), + ), + ), + // Main icon + small icons + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + data.iconAsset, + width: data.iconWidth, + height: data.iconHeight, + ), + const SizedBox(height: 16), + // Small property icons row + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + 'assets/images/onboarding_small_icon_1.svg', + width: 27, + height: 27, + ), + const SizedBox(width: 8), + SvgPicture.asset( + 'assets/images/onboarding_small_icon_2.svg', + width: 31.5, + height: 31.5, + ), + const SizedBox(width: 8), + SvgPicture.asset( + 'assets/images/onboarding_small_icon_3.svg', + width: 24, + height: 27, + ), + ], + ), + ], + ), + ], + ), + ), + ), + ), + ), + + // Text content section + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const SizedBox(height: 8), + // Title + Text( + data.title, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'Fractul', + fontSize: 32, + fontWeight: FontWeight.w700, + color: Color(0xFF0F172A), + height: 1.25, + letterSpacing: -0.8, + ), + ), + const SizedBox(height: 16), + // Subtitle + Text( + data.subtitle, + textAlign: TextAlign.center, + style: const TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 16, + fontWeight: FontWeight.w400, + color: Color(0xFF475569), + height: 1.625, + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _OnboardingData { + final String bgAsset; + final String iconAsset; + final double iconWidth; + final double iconHeight; + final String title; + final String subtitle; + + const _OnboardingData({ + required this.bgAsset, + required this.iconAsset, + required this.iconWidth, + required this.iconHeight, + required this.title, + required this.subtitle, + }); +} diff --git a/lib/features/splash/presentation/screens/splash_screen.dart b/lib/features/splash/presentation/screens/splash_screen.dart index dbe958e..c613e25 100644 --- a/lib/features/splash/presentation/screens/splash_screen.dart +++ b/lib/features/splash/presentation/screens/splash_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; class SplashScreen extends StatefulWidget { @@ -33,11 +34,20 @@ class _SplashScreenState extends State _controller.forward(); - _navigationTimer = Timer(const Duration(seconds: 3), () { - if (mounted) { - context.go('/home'); - } - }); + _navigationTimer = Timer(const Duration(seconds: 3), _navigate); + } + + Future _navigate() async { + if (!mounted) return; + final prefs = await SharedPreferences.getInstance(); + final onboardingCompleted = prefs.getBool('onboarding_completed') ?? false; + + if (!mounted) return; + if (onboardingCompleted) { + context.go('/home'); + } else { + context.go('/onboarding'); + } } @override diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart index 4d14f77..e89469e 100644 --- a/lib/routing/app_router.dart +++ b/lib/routing/app_router.dart @@ -21,6 +21,7 @@ import 'package:real_estate_mobile/features/notifications/presentation/screens/n import 'package:real_estate_mobile/features/agents/presentation/screens/agent_edit_profile_screen.dart'; import 'package:real_estate_mobile/features/profile/presentation/screens/payment_success_screen.dart'; import 'package:real_estate_mobile/features/splash/presentation/screens/splash_screen.dart'; +import 'package:real_estate_mobile/features/onboarding/presentation/screens/onboarding_screen.dart'; final routerProvider = Provider((ref) { final authRefreshNotifier = _AuthRefreshNotifier(); @@ -51,8 +52,8 @@ final routerProvider = Provider((ref) { (route) => location == route || location.startsWith('$route/'), ); - // Never redirect away from splash — it handles its own navigation - if (location == '/splash') return null; + // Never redirect away from splash or onboarding — they handle their own navigation + if (location == '/splash' || location == '/onboarding') return null; // While checking auth status, don't redirect if (isLoading) return null; @@ -75,6 +76,12 @@ final routerProvider = Provider((ref) { builder: (context, state) => const SplashScreen(), ), + // Onboarding (shown only once on first install) + GoRoute( + path: '/onboarding', + builder: (context, state) => const OnboardingScreen(), + ), + // Auth routes (no shell — full screen) GoRoute( path: '/login',