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',