feat: Implement initial authentication, multi-environment support, and core app infrastructure.

This commit is contained in:
pradeepkumar
2026-02-23 19:23:30 +05:30
parent 0f07a16132
commit 4f8ad7fe49
55 changed files with 3836 additions and 118 deletions

View File

@@ -0,0 +1,13 @@
class ApiConstants {
ApiConstants._();
static const String authRegister = '/auth/register';
static const String authLogin = '/auth/login';
static const String authForgotPassword = '/auth/forgot-password';
static const String authResetPassword = '/auth/reset-password';
static const String authVerifyEmail = '/auth/verify-email';
static const String authResendVerification = '/auth/resend-verification';
static const String authMe = '/auth/me';
static const String authRefresh = '/auth/refresh';
static const String authLogout = '/auth/logout';
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class AppColors {
AppColors._();
static const Color primaryDark = Color(0xFF00293D);
static const Color accentOrange = Color(0xFFE58625);
static const Color accentOrangeHover = Color(0xFFD47A1F);
static const Color gradientStart = Color(0xFFC4D9D4);
static const Color gradientEnd = Color(0xFFF0F5FC);
static const Color inputFill = Color(0xFFF0F5FC);
static const Color white = Color(0xFFFFFFFF);
static const Color error = Color(0xFFDC2626);
static const Color errorBg = Color(0xFFFEF2F2);
static const Color errorBorder = Color(0xFFFECACA);
static const Color success = Color(0xFF22C55E);
static const Color divider = Color(0x3300293D); // 20% opacity
static const Color hintText = Color(0x9900293D); // 60% opacity
static const Color subtleText = Color(0x7000293D); // ~44% opacity
}

View File

@@ -0,0 +1,92 @@
import 'package:dio/dio.dart';
import 'package:real_estate_mobile/config/app_config.dart';
import 'package:real_estate_mobile/core/network/api_exceptions.dart';
import 'package:real_estate_mobile/core/storage/secure_storage.dart';
class ApiClient {
static ApiClient? _instance;
late final Dio _dio;
ApiClient._() {
_dio = Dio(
BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
_dio.interceptors.addAll([
_authInterceptor(),
_errorInterceptor(),
]);
}
static ApiClient get instance {
_instance ??= ApiClient._();
return _instance!;
}
Dio get dio => _dio;
InterceptorsWrapper _authInterceptor() {
return InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await SecureStorage.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
);
}
InterceptorsWrapper _errorInterceptor() {
return InterceptorsWrapper(
onError: (error, handler) {
final response = error.response;
if (response != null) {
final data = response.data;
if (data is Map<String, dynamic>) {
final message = data['message'] is List
? (data['message'] as List).join(', ')
: data['message']?.toString() ?? 'An error occurred';
final errors = (data['errors'] as List<dynamic>?)
?.map((e) => FieldError.fromJson(e as Map<String, dynamic>))
.toList() ??
[];
handler.reject(
DioException(
requestOptions: error.requestOptions,
response: error.response,
error: ApiException(
message: message,
statusCode: response.statusCode,
fieldErrors: errors,
),
),
);
return;
}
}
handler.reject(
DioException(
requestOptions: error.requestOptions,
response: error.response,
error: ApiException(
message: error.message ?? 'Network error occurred',
statusCode: response?.statusCode,
),
),
);
},
);
}
}

View File

@@ -0,0 +1,31 @@
class ApiException implements Exception {
final String message;
final int? statusCode;
final List<FieldError> fieldErrors;
const ApiException({
required this.message,
this.statusCode,
this.fieldErrors = const [],
});
@override
String toString() => 'ApiException: $message (status: $statusCode)';
}
class FieldError {
final String field;
final List<String> errors;
const FieldError({required this.field, required this.errors});
factory FieldError.fromJson(Map<String, dynamic> json) {
return FieldError(
field: json['field'] as String? ?? '',
errors: (json['errors'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
);
}
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorage {
static const _storage = FlutterSecureStorage();
static const _accessTokenKey = 'access_token';
static const _refreshTokenKey = 'refresh_token';
static Future<void> saveTokens({
required String accessToken,
required String refreshToken,
}) async {
await Future.wait([
_storage.write(key: _accessTokenKey, value: accessToken),
_storage.write(key: _refreshTokenKey, value: refreshToken),
]);
}
static Future<String?> getAccessToken() async {
return _storage.read(key: _accessTokenKey);
}
static Future<String?> getRefreshToken() async {
return _storage.read(key: _refreshTokenKey);
}
static Future<void> clearTokens() async {
await Future.wait([
_storage.delete(key: _accessTokenKey),
_storage.delete(key: _refreshTokenKey),
]);
}
}

View File

@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
class AppTheme {
AppTheme._();
static ThemeData get light {
return ThemeData(
useMaterial3: true,
fontFamily: 'Fractul',
scaffoldBackgroundColor: AppColors.white,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.primaryDark,
primary: AppColors.primaryDark,
secondary: AppColors.accentOrange,
surface: AppColors.white,
error: AppColors.error,
),
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontFamily: 'Fractul',
fontSize: 30,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
headlineMedium: TextStyle(
fontFamily: 'Fractul',
fontSize: 25,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
titleMedium: TextStyle(
fontFamily: 'Fractul',
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
bodyMedium: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w300,
color: AppColors.primaryDark,
),
bodySmall: TextStyle(
fontFamily: 'Fractul',
fontSize: 12,
fontWeight: FontWeight.w300,
color: AppColors.hintText,
),
labelLarge: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.inputFill,
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 20,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(
color: AppColors.primaryDark,
width: 1.5,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(
color: AppColors.error,
width: 1.5,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(
color: AppColors.error,
width: 1.5,
),
),
hintStyle: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w300,
color: AppColors.primaryDark,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accentOrange,
foregroundColor: AppColors.primaryDark,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
textStyle: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
),
);
}
}