This commit is contained in:
2025-09-28 00:20:44 +07:00
parent 74d0e3d44c
commit cb53f5585b
10 changed files with 1098 additions and 160 deletions

View File

@@ -1,3 +1,4 @@
// Barrel export file for constants
export 'app_constants.dart';
export 'environment_config.dart';
export 'storage_constants.dart';

View File

@@ -0,0 +1,140 @@
/// Environment configuration for API endpoints and settings
enum Environment {
development,
staging,
production,
}
/// Environment-specific configuration
class EnvironmentConfig {
// Private constructor to prevent instantiation
const EnvironmentConfig._();
/// Current environment - Change this to switch environments
static const Environment currentEnvironment = Environment.development;
/// Get base URL for current environment
static String get baseUrl {
switch (currentEnvironment) {
case Environment.development:
return 'http://localhost:3000';
case Environment.staging:
return 'https://api-staging.example.com';
case Environment.production:
return 'https://api.example.com';
}
}
/// Get API path for current environment
static String get apiPath {
switch (currentEnvironment) {
case Environment.development:
// No API prefix for local development - endpoints are directly at /auth/
return '';
case Environment.staging:
case Environment.production:
return '/api/v1';
}
}
/// Check if current environment is development
static bool get isDevelopment => currentEnvironment == Environment.development;
/// Check if current environment is staging
static bool get isStaging => currentEnvironment == Environment.staging;
/// Check if current environment is production
static bool get isProduction => currentEnvironment == Environment.production;
/// Get timeout configurations based on environment
static int get connectTimeout {
switch (currentEnvironment) {
case Environment.development:
return 10000; // 10 seconds for local development
case Environment.staging:
return 20000; // 20 seconds for staging
case Environment.production:
return 30000; // 30 seconds for production
}
}
static int get receiveTimeout {
switch (currentEnvironment) {
case Environment.development:
return 15000; // 15 seconds for local development
case Environment.staging:
return 25000; // 25 seconds for staging
case Environment.production:
return 30000; // 30 seconds for production
}
}
static int get sendTimeout {
switch (currentEnvironment) {
case Environment.development:
return 15000; // 15 seconds for local development
case Environment.staging:
return 25000; // 25 seconds for staging
case Environment.production:
return 30000; // 30 seconds for production
}
}
/// Get retry configurations based on environment
static int get maxRetries {
switch (currentEnvironment) {
case Environment.development:
return 2; // Fewer retries for local development
case Environment.staging:
return 3; // Standard retries for staging
case Environment.production:
return 3; // Standard retries for production
}
}
static Duration get retryDelay {
switch (currentEnvironment) {
case Environment.development:
return const Duration(milliseconds: 500); // Faster retry for local
case Environment.staging:
return const Duration(seconds: 1); // Standard retry delay
case Environment.production:
return const Duration(seconds: 1); // Standard retry delay
}
}
/// Enable/disable features based on environment
static bool get enableLogging => !isProduction;
static bool get enableDetailedLogging => isDevelopment;
static bool get enableCertificatePinning => isProduction;
/// Authentication endpoints (consistent across environments)
static const String authEndpoint = '/auth';
static const String loginEndpoint = '$authEndpoint/login';
static const String registerEndpoint = '$authEndpoint/register';
static const String refreshEndpoint = '$authEndpoint/refresh';
static const String logoutEndpoint = '$authEndpoint/logout';
/// Full API URLs
static String get fullBaseUrl => baseUrl + apiPath;
static String get loginUrl => baseUrl + loginEndpoint;
static String get registerUrl => baseUrl + registerEndpoint;
static String get refreshUrl => baseUrl + refreshEndpoint;
static String get logoutUrl => baseUrl + logoutEndpoint;
/// Debug information
static Map<String, dynamic> get debugInfo => {
'environment': currentEnvironment.name,
'baseUrl': baseUrl,
'apiPath': apiPath,
'fullBaseUrl': fullBaseUrl,
'connectTimeout': connectTimeout,
'receiveTimeout': receiveTimeout,
'sendTimeout': sendTimeout,
'maxRetries': maxRetries,
'retryDelay': retryDelay.inMilliseconds,
'enableLogging': enableLogging,
'enableDetailedLogging': enableDetailedLogging,
'enableCertificatePinning': enableCertificatePinning,
};
}

View File

@@ -0,0 +1,377 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constants/environment_config.dart';
import '../providers/app_providers.dart';
import '../providers/network_providers.dart';
/// Debug page to verify API configuration and test connectivity
/// This page helps verify that the localhost:3000 configuration is working correctly
class ApiDebugPage extends ConsumerWidget {
const ApiDebugPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final environmentInfo = ref.watch(environmentInfoProvider);
final environmentDebug = ref.watch(environmentDebugInfoProvider);
final apiConnectivityTest = ref.watch(apiConnectivityTestProvider);
return Scaffold(
appBar: AppBar(
title: const Text('API Configuration Debug'),
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildEnvironmentCard(context, environmentInfo),
const SizedBox(height: 16),
_buildEndpointsCard(context, environmentDebug),
const SizedBox(height: 16),
_buildConnectivityTestCard(context, apiConnectivityTest, ref),
const SizedBox(height: 16),
_buildQuickActionsCard(context, ref),
],
),
),
);
}
Widget _buildEnvironmentCard(BuildContext context, Map<String, dynamic> info) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.settings,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'Environment Configuration',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
_buildInfoRow('Environment', info['environment']),
_buildInfoRow('Base URL', info['baseUrl']),
_buildInfoRow('API Path', info['apiPath'].isEmpty ? 'None (Direct)' : info['apiPath']),
_buildInfoRow('Full Base URL', info['fullBaseUrl']),
const Divider(),
Text(
'Timeout Settings',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
_buildInfoRow('Connect Timeout', '${info['timeouts']['connect']}ms'),
_buildInfoRow('Receive Timeout', '${info['timeouts']['receive']}ms'),
_buildInfoRow('Send Timeout', '${info['timeouts']['send']}ms'),
],
),
),
);
}
Widget _buildEndpointsCard(BuildContext context, Map<String, dynamic> info) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.api,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'API Endpoints',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
_buildEndpointRow(context, 'Login', '${info['baseUrl']}/auth/login'),
_buildEndpointRow(context, 'Register', '${info['baseUrl']}/auth/register'),
_buildEndpointRow(context, 'Refresh', '${info['baseUrl']}/auth/refresh'),
_buildEndpointRow(context, 'Logout', '${info['baseUrl']}/auth/logout'),
],
),
),
);
}
Widget _buildConnectivityTestCard(
BuildContext context,
AsyncValue<Map<String, dynamic>> testResult,
WidgetRef ref,
) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.network_check,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'Connectivity Test',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () {
ref.read(apiConnectivityTestProvider.notifier).retry();
},
icon: const Icon(Icons.refresh),
tooltip: 'Retry Test',
),
],
),
const SizedBox(height: 16),
testResult.when(
data: (data) => _buildTestResult(context, data),
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => _buildErrorResult(context, error.toString()),
),
],
),
),
);
}
Widget _buildQuickActionsCard(BuildContext context, WidgetRef ref) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.flash_on,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'Quick Actions',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
children: [
ElevatedButton.icon(
onPressed: () => _testLogin(context, ref),
icon: const Icon(Icons.login),
label: const Text('Test Login'),
),
ElevatedButton.icon(
onPressed: () => _testRegister(context, ref),
icon: const Icon(Icons.person_add),
label: const Text('Test Register'),
),
ElevatedButton.icon(
onPressed: () => _copyConfiguration(context),
icon: const Icon(Icons.copy),
label: const Text('Copy Config'),
),
],
),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontFamily: 'monospace'),
),
),
],
),
);
}
Widget _buildEndpointRow(BuildContext context, String name, String url) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
SizedBox(
width: 80,
child: Text(
name,
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(
child: Text(
url,
style: const TextStyle(fontFamily: 'monospace'),
),
),
IconButton(
onPressed: () => _copyToClipboard(context, url),
icon: const Icon(Icons.copy, size: 16),
tooltip: 'Copy URL',
),
],
),
);
}
Widget _buildTestResult(BuildContext context, Map<String, dynamic> data) {
final isSuccess = data['status'] == 'success';
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSuccess
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isSuccess ? Icons.check_circle : Icons.error,
color: isSuccess
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Text(
data['message'] ?? (isSuccess ? 'Test passed' : 'Test failed'),
style: TextStyle(
fontWeight: FontWeight.bold,
color: isSuccess
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.error,
),
),
],
),
if (data['timestamp'] != null) ...[
const SizedBox(height: 8),
Text(
'Tested at: ${data['timestamp']}',
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
);
}
Widget _buildErrorResult(BuildContext context, String error) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Error: $error',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
);
}
void _testLogin(BuildContext context, WidgetRef ref) {
// This would be implemented based on your auth service
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login test would be implemented here'),
),
);
}
void _testRegister(BuildContext context, WidgetRef ref) {
// This would be implemented based on your auth service
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Register test would be implemented here'),
),
);
}
void _copyConfiguration(BuildContext context) {
final config = EnvironmentConfig.debugInfo;
final configText = config.entries
.map((e) => '${e.key}: ${e.value}')
.join('\n');
_copyToClipboard(context, configText);
}
void _copyToClipboard(BuildContext context, String text) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Copied to clipboard'),
duration: Duration(seconds: 2),
),
);
}
}

View File

@@ -1,29 +1,22 @@
import '../constants/environment_config.dart';
/// API constants for network configuration
class ApiConstants {
// Private constructor to prevent instantiation
const ApiConstants._();
// Base URLs for different environments
static const String baseUrlDev = 'https://api-dev.example.com';
static const String baseUrlStaging = 'https://api-staging.example.com';
static const String baseUrlProd = 'https://api.example.com';
// Environment-based configuration
static String get baseUrl => EnvironmentConfig.baseUrl;
static String get apiPath => EnvironmentConfig.apiPath;
// Current environment base URL
// In a real app, this would be determined by build configuration
static const String baseUrl = baseUrlDev;
// Timeout configurations (environment-specific)
static int get connectTimeout => EnvironmentConfig.connectTimeout;
static int get receiveTimeout => EnvironmentConfig.receiveTimeout;
static int get sendTimeout => EnvironmentConfig.sendTimeout;
// API versioning
static const String apiVersion = 'v1';
static const String apiPath = '/api/$apiVersion';
// Timeout configurations (in milliseconds)
static const int connectTimeout = 30000; // 30 seconds
static const int receiveTimeout = 30000; // 30 seconds
static const int sendTimeout = 30000; // 30 seconds
// Retry configurations
static const int maxRetries = 3;
static const Duration retryDelay = Duration(seconds: 1);
// Retry configurations (environment-specific)
static int get maxRetries => EnvironmentConfig.maxRetries;
static Duration get retryDelay => EnvironmentConfig.retryDelay;
// Headers
static const String contentType = 'application/json';
@@ -35,11 +28,12 @@ class ApiConstants {
static const String bearerPrefix = 'Bearer';
static const String apiKeyHeaderKey = 'X-API-Key';
// Common API endpoints
static const String authEndpoint = '/auth';
static const String loginEndpoint = '$authEndpoint/login';
static const String refreshEndpoint = '$authEndpoint/refresh';
static const String logoutEndpoint = '$authEndpoint/logout';
// Authentication endpoints (from environment config)
static String get authEndpoint => EnvironmentConfig.authEndpoint;
static String get loginEndpoint => EnvironmentConfig.loginEndpoint;
static String get registerEndpoint => EnvironmentConfig.registerEndpoint;
static String get refreshEndpoint => EnvironmentConfig.refreshEndpoint;
static String get logoutEndpoint => EnvironmentConfig.logoutEndpoint;
static const String userEndpoint = '/user';
static const String profileEndpoint = '$userEndpoint/profile';
@@ -68,9 +62,10 @@ class ApiConstants {
// Example: 'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
];
// Development flags
static const bool enableLogging = true;
static const bool enableCertificatePinning = false; // Disabled for development
// Development flags (environment-specific)
static bool get enableLogging => EnvironmentConfig.enableLogging;
static bool get enableCertificatePinning => EnvironmentConfig.enableCertificatePinning;
static bool get enableDetailedLogging => EnvironmentConfig.enableDetailedLogging;
// API rate limiting
static const int maxRequestsPerMinute = 100;

View File

@@ -0,0 +1,242 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../constants/environment_config.dart';
import 'api_constants.dart';
import 'dio_client.dart';
/// Example authentication service demonstrating the localhost:3000 configuration
/// This service shows how to use the updated API configuration with the backend
class AuthServiceExample {
final DioClient _dioClient;
AuthServiceExample(this._dioClient);
/// Test connection to the backend
Future<Map<String, dynamic>> testConnection() async {
try {
debugPrint('🔍 Testing connection to ${EnvironmentConfig.baseUrl}...');
final response = await _dioClient.get('/');
return {
'status': 'success',
'message': 'Connected to backend successfully',
'baseUrl': EnvironmentConfig.baseUrl,
'statusCode': response.statusCode,
'timestamp': DateTime.now().toIso8601String(),
};
} catch (error) {
debugPrint('❌ Connection test failed: $error');
return {
'status': 'error',
'message': 'Failed to connect to backend',
'error': error.toString(),
'baseUrl': EnvironmentConfig.baseUrl,
'timestamp': DateTime.now().toIso8601String(),
};
}
}
/// Login with email and password
/// Calls POST localhost:3000/auth/login
Future<Map<String, dynamic>> login({
required String email,
required String password,
}) async {
try {
debugPrint('🔐 Attempting login to ${EnvironmentConfig.loginUrl}...');
final response = await _dioClient.post(
ApiConstants.loginEndpoint, // This resolves to /auth/login
data: {
'email': email,
'password': password,
},
);
debugPrint('✅ Login successful');
return {
'status': 'success',
'data': response.data,
'endpoint': EnvironmentConfig.loginUrl,
'timestamp': DateTime.now().toIso8601String(),
};
} on DioException catch (dioError) {
debugPrint('❌ Login failed with DioException: ${dioError.message}');
return {
'status': 'error',
'error': dioError.message ?? 'Unknown Dio error',
'statusCode': dioError.response?.statusCode,
'endpoint': EnvironmentConfig.loginUrl,
'timestamp': DateTime.now().toIso8601String(),
};
} catch (error) {
debugPrint('❌ Login failed: $error');
return {
'status': 'error',
'error': error.toString(),
'endpoint': EnvironmentConfig.loginUrl,
'timestamp': DateTime.now().toIso8601String(),
};
}
}
/// Register new user
/// Calls POST localhost:3000/auth/register
Future<Map<String, dynamic>> register({
required String email,
required String password,
required String name,
}) async {
try {
debugPrint('📝 Attempting registration to ${EnvironmentConfig.registerUrl}...');
final response = await _dioClient.post(
ApiConstants.registerEndpoint, // This resolves to /auth/register
data: {
'email': email,
'password': password,
'name': name,
},
);
debugPrint('✅ Registration successful');
return {
'status': 'success',
'data': response.data,
'endpoint': EnvironmentConfig.registerUrl,
'timestamp': DateTime.now().toIso8601String(),
};
} on DioException catch (dioError) {
debugPrint('❌ Registration failed with DioException: ${dioError.message}');
return {
'status': 'error',
'error': dioError.message ?? 'Unknown Dio error',
'statusCode': dioError.response?.statusCode,
'endpoint': EnvironmentConfig.registerUrl,
'timestamp': DateTime.now().toIso8601String(),
};
} catch (error) {
debugPrint('❌ Registration failed: $error');
return {
'status': 'error',
'error': error.toString(),
'endpoint': EnvironmentConfig.registerUrl,
'timestamp': DateTime.now().toIso8601String(),
};
}
}
/// Refresh authentication token
/// Calls POST localhost:3000/auth/refresh
Future<Map<String, dynamic>> refreshToken(String refreshToken) async {
try {
debugPrint('🔄 Attempting token refresh to ${EnvironmentConfig.refreshUrl}...');
final response = await _dioClient.post(
ApiConstants.refreshEndpoint, // This resolves to /auth/refresh
data: {
'refreshToken': refreshToken,
},
);
debugPrint('✅ Token refresh successful');
return {
'status': 'success',
'data': response.data,
'endpoint': EnvironmentConfig.refreshUrl,
'timestamp': DateTime.now().toIso8601String(),
};
} on DioException catch (dioError) {
debugPrint('❌ Token refresh failed with DioException: ${dioError.message}');
return {
'status': 'error',
'error': dioError.message ?? 'Unknown Dio error',
'statusCode': dioError.response?.statusCode,
'endpoint': EnvironmentConfig.refreshUrl,
'timestamp': DateTime.now().toIso8601String(),
};
} catch (error) {
debugPrint('❌ Token refresh failed: $error');
return {
'status': 'error',
'error': error.toString(),
'endpoint': EnvironmentConfig.refreshUrl,
'timestamp': DateTime.now().toIso8601String(),
};
}
}
/// Logout user
/// Calls POST localhost:3000/auth/logout
Future<Map<String, dynamic>> logout(String accessToken) async {
try {
debugPrint('🚪 Attempting logout to ${EnvironmentConfig.logoutUrl}...');
final response = await _dioClient.post(
ApiConstants.logoutEndpoint, // This resolves to /auth/logout
options: Options(
headers: {
ApiConstants.authHeaderKey: '${ApiConstants.bearerPrefix} $accessToken',
},
),
);
debugPrint('✅ Logout successful');
return {
'status': 'success',
'data': response.data,
'endpoint': EnvironmentConfig.logoutUrl,
'timestamp': DateTime.now().toIso8601String(),
};
} on DioException catch (dioError) {
debugPrint('❌ Logout failed with DioException: ${dioError.message}');
return {
'status': 'error',
'error': dioError.message ?? 'Unknown Dio error',
'statusCode': dioError.response?.statusCode,
'endpoint': EnvironmentConfig.logoutUrl,
'timestamp': DateTime.now().toIso8601String(),
};
} catch (error) {
debugPrint('❌ Logout failed: $error');
return {
'status': 'error',
'error': error.toString(),
'endpoint': EnvironmentConfig.logoutUrl,
'timestamp': DateTime.now().toIso8601String(),
};
}
}
/// Get current environment configuration for debugging
Map<String, dynamic> getEnvironmentInfo() {
return {
'environment': EnvironmentConfig.currentEnvironment.name,
'baseUrl': EnvironmentConfig.baseUrl,
'apiPath': EnvironmentConfig.apiPath,
'fullBaseUrl': EnvironmentConfig.fullBaseUrl,
'endpoints': {
'login': EnvironmentConfig.loginUrl,
'register': EnvironmentConfig.registerUrl,
'refresh': EnvironmentConfig.refreshUrl,
'logout': EnvironmentConfig.logoutUrl,
},
'timeouts': {
'connect': EnvironmentConfig.connectTimeout,
'receive': EnvironmentConfig.receiveTimeout,
'send': EnvironmentConfig.sendTimeout,
},
'retry': {
'maxRetries': EnvironmentConfig.maxRetries,
'retryDelay': EnvironmentConfig.retryDelay.inMilliseconds,
},
'flags': {
'enableLogging': EnvironmentConfig.enableLogging,
'enableDetailedLogging': EnvironmentConfig.enableDetailedLogging,
'enableCertificatePinning': EnvironmentConfig.enableCertificatePinning,
},
};
}
}

View File

@@ -0,0 +1,74 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import '../constants/environment_config.dart';
/// Simple utility to test API connection
class ApiConnectionTest {
static Future<void> testConnection(BuildContext context) async {
final dio = Dio();
try {
debugPrint('🔍 Testing API connection...');
debugPrint('Base URL: ${EnvironmentConfig.baseUrl}');
debugPrint('Auth endpoint: ${EnvironmentConfig.authEndpoint}');
// Test basic connectivity to the auth endpoint
final response = await dio.get(
'${EnvironmentConfig.baseUrl}${EnvironmentConfig.authEndpoint}',
options: Options(
validateStatus: (status) => true, // Accept any status code
receiveTimeout: const Duration(seconds: 5),
),
);
debugPrint('✅ Connection successful!');
debugPrint('Status code: ${response.statusCode}');
debugPrint('Response: ${response.data}');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('API Connected! Status: ${response.statusCode}'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
debugPrint('❌ Connection failed: $e');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Connection failed: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
/// Get current environment info as formatted string
static String getEnvironmentInfo() {
final info = EnvironmentConfig.debugInfo;
final buffer = StringBuffer();
buffer.writeln('📊 Environment Configuration:');
buffer.writeln('================================');
info.forEach((key, value) {
buffer.writeln('$key: $value');
});
buffer.writeln('================================');
buffer.writeln('\n📍 Auth Endpoints:');
buffer.writeln('Login: ${EnvironmentConfig.loginUrl}');
buffer.writeln('Register: ${EnvironmentConfig.registerUrl}');
buffer.writeln('Refresh: ${EnvironmentConfig.refreshUrl}');
buffer.writeln('Logout: ${EnvironmentConfig.logoutUrl}');
return buffer.toString();
}
/// Print environment info to console
static void printEnvironmentInfo() {
debugPrint(getEnvironmentInfo());
}
}

View File

@@ -1,6 +1,7 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter/foundation.dart';
import '../constants/environment_config.dart';
import '../database/hive_service.dart';
import '../database/models/app_settings.dart';
import '../database/providers/database_providers.dart';
@@ -267,15 +268,33 @@ class AppConfiguration extends _$AppConfiguration {
final buildMode = ref.watch(appBuildModeProvider);
return {
'apiTimeout': 30000, // 30 seconds
// Environment-specific API configuration
'environment': EnvironmentConfig.currentEnvironment.name,
'baseUrl': EnvironmentConfig.baseUrl,
'apiPath': EnvironmentConfig.apiPath,
'fullBaseUrl': EnvironmentConfig.fullBaseUrl,
'connectTimeout': EnvironmentConfig.connectTimeout,
'receiveTimeout': EnvironmentConfig.receiveTimeout,
'sendTimeout': EnvironmentConfig.sendTimeout,
'maxRetries': EnvironmentConfig.maxRetries,
'retryDelay': EnvironmentConfig.retryDelay.inMilliseconds,
// Environment-specific logging
'enableLogging': EnvironmentConfig.enableLogging,
'enableDetailedLogging': EnvironmentConfig.enableDetailedLogging,
'enableCertificatePinning': EnvironmentConfig.enableCertificatePinning,
'logLevel': EnvironmentConfig.enableDetailedLogging ? 'verbose' : 'error',
// General configuration
'cacheTimeout': 3600000, // 1 hour in milliseconds
'maxRetries': 3,
'retryDelay': 1000, // 1 second
'enableLogging': buildMode == 'debug',
'logLevel': buildMode == 'debug' ? 'verbose' : 'error',
'maxCacheSize': 100 * 1024 * 1024, // 100MB
'imageQuality': 85,
'compressionEnabled': true,
// Environment flags
'isDevelopment': EnvironmentConfig.isDevelopment,
'isStaging': EnvironmentConfig.isStaging,
'isProduction': EnvironmentConfig.isProduction,
};
}
@@ -346,3 +365,68 @@ class ErrorTracker extends _$ErrorTracker {
return state.reversed.take(count).toList();
}
}
/// Environment debug information provider
@riverpod
Map<String, dynamic> environmentDebugInfo(EnvironmentDebugInfoRef ref) {
return EnvironmentConfig.debugInfo;
}
/// API connectivity test provider
@riverpod
class ApiConnectivityTest extends _$ApiConnectivityTest {
@override
Future<Map<String, dynamic>> build() async {
return _testConnectivity();
}
Future<Map<String, dynamic>> _testConnectivity() async {
try {
debugPrint('🔍 Testing API connectivity to ${EnvironmentConfig.baseUrl}...');
final result = {
'baseUrl': EnvironmentConfig.baseUrl,
'fullBaseUrl': EnvironmentConfig.fullBaseUrl,
'loginUrl': EnvironmentConfig.loginUrl,
'environment': EnvironmentConfig.currentEnvironment.name,
'timestamp': DateTime.now().toIso8601String(),
'status': 'success',
'message': 'Configuration loaded successfully',
'endpoints': {
'login': EnvironmentConfig.loginUrl,
'register': EnvironmentConfig.registerUrl,
'refresh': EnvironmentConfig.refreshUrl,
'logout': EnvironmentConfig.logoutUrl,
},
'settings': {
'connectTimeout': EnvironmentConfig.connectTimeout,
'receiveTimeout': EnvironmentConfig.receiveTimeout,
'sendTimeout': EnvironmentConfig.sendTimeout,
'maxRetries': EnvironmentConfig.maxRetries,
'retryDelay': EnvironmentConfig.retryDelay.inMilliseconds,
'enableLogging': EnvironmentConfig.enableLogging,
'enableDetailedLogging': EnvironmentConfig.enableDetailedLogging,
}
};
debugPrint('✅ API connectivity test completed successfully');
return result;
} catch (error, stackTrace) {
debugPrint('❌ API connectivity test failed: $error');
debugPrint('Stack trace: $stackTrace');
return {
'status': 'error',
'error': error.toString(),
'timestamp': DateTime.now().toIso8601String(),
'baseUrl': EnvironmentConfig.baseUrl,
};
}
}
/// Retry connectivity test
Future<void> retry() async {
state = const AsyncValue.loading();
state = AsyncValue.data(await _testConnectivity());
}
}

View File

@@ -89,6 +89,25 @@ final isAppReadyProvider = AutoDisposeProvider<bool>.internal(
);
typedef IsAppReadyRef = AutoDisposeProviderRef<bool>;
String _$environmentDebugInfoHash() =>
r'8a936abed2173bd1539eec64231e8d970ed7382a';
/// Environment debug information provider
///
/// Copied from [environmentDebugInfo].
@ProviderFor(environmentDebugInfo)
final environmentDebugInfoProvider =
AutoDisposeProvider<Map<String, dynamic>>.internal(
environmentDebugInfo,
name: r'environmentDebugInfoProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$environmentDebugInfoHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef EnvironmentDebugInfoRef = AutoDisposeProviderRef<Map<String, dynamic>>;
String _$appInitializationHash() => r'cdf86e2d6985c6dcee80f618bc032edf81011fc9';
/// App initialization provider
@@ -142,7 +161,7 @@ final featureFlagsProvider =
);
typedef _$FeatureFlags = AutoDisposeNotifier<Map<String, bool>>;
String _$appConfigurationHash() => r'115fff1ac67a37ff620bbd15ea142a7211e9dc9c';
String _$appConfigurationHash() => r'7699bbd57d15b91cd520a876454368e5b97342bd';
/// App configuration provider
///
@@ -196,5 +215,24 @@ final errorTrackerProvider = AutoDisposeNotifierProvider<ErrorTracker,
);
typedef _$ErrorTracker = AutoDisposeNotifier<List<Map<String, dynamic>>>;
String _$apiConnectivityTestHash() =>
r'19c63d75d09ad8f95452afb1a409528fcdd5cbaa';
/// API connectivity test provider
///
/// Copied from [ApiConnectivityTest].
@ProviderFor(ApiConnectivityTest)
final apiConnectivityTestProvider = AutoDisposeAsyncNotifierProvider<
ApiConnectivityTest, Map<String, dynamic>>.internal(
ApiConnectivityTest.new,
name: r'apiConnectivityTestProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$apiConnectivityTestHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ApiConnectivityTest = AutoDisposeAsyncNotifier<Map<String, dynamic>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View File

@@ -2,6 +2,7 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../network/auth_service_example.dart';
import '../network/dio_client.dart';
import '../network/network_info.dart';
@@ -49,3 +50,15 @@ final networkConnectionDetailsProvider = FutureProvider((ref) async {
final networkInfo = ref.watch(networkInfoProvider) as NetworkInfoImpl;
return await networkInfo.getConnectionDetails();
});
/// Provider for AuthServiceExample - demonstrates localhost:3000 API usage
final authServiceExampleProvider = Provider<AuthServiceExample>((ref) {
final dioClient = ref.watch(dioClientProvider);
return AuthServiceExample(dioClient);
});
/// Provider for environment information debugging
final environmentInfoProvider = Provider<Map<String, dynamic>>((ref) {
final authService = ref.watch(authServiceExampleProvider);
return authService.getEnvironmentInfo();
});

View File

@@ -1,6 +1,8 @@
import 'package:base_flutter/core/utils/utils.dart';
import 'package:dio/dio.dart';
import '../../../../core/errors/exceptions.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/network/api_constants.dart';
import '../../../../core/services/api_service.dart';
import '../models/user_model.dart';
abstract class AuthRemoteDataSource {
@@ -34,58 +36,43 @@ abstract class AuthRemoteDataSource {
});
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
final DioClient dioClient;
AuthRemoteDataSourceImpl({required this.dioClient});
class AuthRemoteDataSourceImpl extends BaseApiService implements AuthRemoteDataSource {
AuthRemoteDataSourceImpl({required DioClient dioClient}) : super(dioClient);
@override
Future<UserModel> login({
required String email,
required String password,
}) async {
try {
// Using JSONPlaceholder as a mock API
// In real app, this would be your actual auth endpoint
final response = await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/posts',
return executeRequest(
() => dioClient.post(
ApiConstants.loginEndpoint,
data: {
'email': email,
'password': password,
},
),
(data) {
final responseData = data as DataMap;
// If the backend returns user data in a 'user' field
if (responseData.containsKey('user')) {
final userData = DataMap.from(responseData['user']);
// Add token if it's returned separately
if (responseData.containsKey('token')) {
userData['token'] = responseData['token'];
}
if (responseData.containsKey('tokenExpiry')) {
userData['tokenExpiry'] = responseData['tokenExpiry'];
}
return UserModel.fromJson(userData);
}
// If the backend returns everything in the root
else {
return UserModel.fromJson(responseData);
}
},
);
// Mock validation - accept any email/password for demo
// In real app, the server would validate credentials
if (email.isEmpty || password.isEmpty) {
throw const ServerException('Invalid credentials');
}
// Mock response for demonstration
// In real app, parse actual API response
final mockUser = {
'id': '1',
'email': email,
'name': email.split('@').first,
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
};
return UserModel.fromJson(mockUser);
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw const ServerException('Invalid credentials');
} else if (e.response?.statusCode == 404) {
throw const ServerException('User not found');
} else {
throw ServerException(e.message ?? 'Login failed');
}
} catch (e) {
if (e.toString().contains('Invalid credentials')) {
rethrow;
}
throw ServerException(e.toString());
}
}
@override
@@ -94,73 +81,76 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
required String password,
required String name,
}) async {
try {
// Mock API call
final response = await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/users',
return executeRequest(
() => dioClient.post(
ApiConstants.registerEndpoint,
data: {
'email': email,
'password': password,
'name': name,
},
),
(data) {
final responseData = data as Map<String, dynamic>;
// If the backend returns user data in a 'user' field
if (responseData.containsKey('user')) {
final userData = Map<String, dynamic>.from(responseData['user']);
// Add token if it's returned separately
if (responseData.containsKey('token')) {
userData['token'] = responseData['token'];
}
if (responseData.containsKey('tokenExpiry')) {
userData['tokenExpiry'] = responseData['tokenExpiry'];
}
return UserModel.fromJson(userData);
}
// If the backend returns everything in the root
else {
return UserModel.fromJson(responseData);
}
},
);
// Mock response
final mockUser = {
'id': DateTime.now().millisecondsSinceEpoch.toString(),
'email': email,
'name': name,
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
};
return UserModel.fromJson(mockUser);
} on DioException catch (e) {
if (e.response?.statusCode == 409) {
throw const ServerException('Email already exists');
} else {
throw ServerException(e.message ?? 'Registration failed');
}
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<void> logout() async {
try {
// Mock API call
await dioClient.dio.post('https://jsonplaceholder.typicode.com/posts');
// In real app, you might call a logout endpoint to invalidate token
} catch (e) {
throw ServerException(e.toString());
}
await executeRequest(
() => dioClient.post(ApiConstants.logoutEndpoint),
(_) {}, // No return value needed for logout
);
}
@override
Future<UserModel> refreshToken(String token) async {
try {
// Mock API call
final response = await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/users',
return executeRequest(
() => dioClient.post(
ApiConstants.refreshEndpoint,
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
),
(data) {
final responseData = data as Map<String, dynamic>;
// Mock response
final mockUser = {
'id': '1',
'email': 'user@example.com',
'name': 'User',
'token': 'refreshed_token_${DateTime.now().millisecondsSinceEpoch}',
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
};
return UserModel.fromJson(mockUser);
} catch (e) {
throw ServerException(e.toString());
// If the backend returns user data in a 'user' field
if (responseData.containsKey('user')) {
final userData = Map<String, dynamic>.from(responseData['user']);
// Add new token if it's returned separately
if (responseData.containsKey('token')) {
userData['token'] = responseData['token'];
}
if (responseData.containsKey('tokenExpiry')) {
userData['tokenExpiry'] = responseData['tokenExpiry'];
}
return UserModel.fromJson(userData);
}
// If the backend returns everything in the root
else {
return UserModel.fromJson(responseData);
}
},
);
}
@override
@@ -168,30 +158,18 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
required String name,
String? avatarUrl,
}) async {
try {
// Mock API call
final response = await dioClient.dio.put(
'https://jsonplaceholder.typicode.com/users/1',
// For now, keeping this as mock since profile endpoints are not specified
// When you have the actual endpoint, update this to use executeRequest
return executeRequest(
() => dioClient.put(
'/user/profile', // Replace with actual endpoint when available
data: {
'name': name,
'avatarUrl': avatarUrl,
},
),
(data) => UserModel.fromJson(data as Map<String, dynamic>),
);
// Mock response
final mockUser = {
'id': '1',
'email': 'user@example.com',
'name': name,
'avatarUrl': avatarUrl,
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
};
return UserModel.fromJson(mockUser);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
@@ -199,34 +177,30 @@ class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
required String oldPassword,
required String newPassword,
}) async {
try {
// Mock API call
await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/posts',
await executeRequest(
() => dioClient.post(
'/auth/change-password', // Replace with actual endpoint when available
data: {
'oldPassword': oldPassword,
'newPassword': newPassword,
},
),
(_) {}, // No return value needed
);
} catch (e) {
throw ServerException(e.toString());
}
}
@override
Future<void> resetPassword({
required String email,
}) async {
try {
// Mock API call
await dioClient.dio.post(
'https://jsonplaceholder.typicode.com/posts',
await executeRequest(
() => dioClient.post(
'/auth/reset-password', // Replace with actual endpoint when available
data: {
'email': email,
},
),
(_) {}, // No return value needed
);
} catch (e) {
throw ServerException(e.toString());
}
}
}