2026-01-31 15:23:18 +05:30
// lib/core/api/api_client.dart
2026-04-19 21:40:17 +05:30
import ' dart:async ' ;
2026-01-31 15:23:18 +05:30
import ' dart:convert ' ;
2026-04-19 21:40:17 +05:30
import ' dart:io ' show SocketException ;
2026-01-31 15:23:18 +05:30
import ' package:flutter/foundation.dart ' ;
import ' package:http/http.dart ' as http ;
2026-04-08 21:17:55 +05:30
import ' package:http_parser/http_parser.dart ' ;
2026-01-31 15:23:18 +05:30
import ' ../storage/token_storage.dart ' ;
class ApiClient {
2026-04-19 21:40:17 +05:30
static const Duration _timeout = Duration ( seconds: 25 ) ;
static const Duration _retryDelay = Duration ( milliseconds: 600 ) ;
2026-03-14 08:57:25 +05:30
// Set to true to enable mock/offline development mode (useful when backend is unavailable)
2026-03-30 10:05:23 +05:30
static const bool _developmentMode = false ;
2026-01-31 15:23:18 +05:30
/// POST request
///
/// - `url` should be a fully qualified endpoint (ApiEndpoints.*)
/// - `body` is the JSON object to send (Map)
/// - when `requiresAuth == true` token & username are added to the request body
Future < Map < String , dynamic > > post (
String url , {
Map < String , dynamic > ? body ,
bool requiresAuth = true ,
} ) async {
final headers = < String , String > {
' Content-Type ' : ' application/json ' ,
} ;
final Map < String , dynamic > finalBody = await _buildAuthBody ( body , requiresAuth ) ;
late http . Response response ;
try {
2026-04-19 21:40:17 +05:30
response = await _postWithRetry ( url , headers , finalBody ) ;
2026-01-31 15:23:18 +05:30
} catch ( e ) {
if ( kDebugMode ) debugPrint ( ' ApiClient.post network error: $ e ' ) ;
2026-03-14 08:57:25 +05:30
// Development mode: return mock responses for common endpoints on network errors
if ( _developmentMode ) {
if ( url . contains ( ' /user/login/ ' ) ) {
if ( kDebugMode ) debugPrint ( ' Development mode: returning mock login response ' ) ;
final email = finalBody [ ' username ' ] ? ? ' test@example.com ' ;
return {
' token ' : ' mock_token_ ${ DateTime . now ( ) . millisecondsSinceEpoch } ' ,
' username ' : email ,
' email ' : email ,
' phone_number ' : ' +1234567890 ' ,
} ;
} else if ( url . contains ( ' /user/register/ ' ) ) {
if ( kDebugMode ) debugPrint ( ' Development mode: returning mock register response ' ) ;
final email = finalBody [ ' email ' ] ? ? ' test@example.com ' ;
return {
' token ' : ' mock_token_ ${ DateTime . now ( ) . millisecondsSinceEpoch } ' ,
' username ' : email ,
' email ' : email ,
' phone_number ' : finalBody [ ' phone_number ' ] ? ? ' +1234567890 ' ,
} ;
2026-03-30 10:05:23 +05:30
} else if ( url . contains ( ' /events/type-list/ ' ) ) {
if ( kDebugMode ) debugPrint ( ' Development mode: returning mock event types ' ) ;
return {
' event_types ' : [
{ ' id ' : 1 , ' event_type ' : ' Concert ' , ' event_type_icon ' : null } ,
{ ' id ' : 2 , ' event_type ' : ' Workshop ' , ' event_type_icon ' : null } ,
{ ' id ' : 3 , ' event_type ' : ' Festival ' , ' event_type_icon ' : null } ,
{ ' id ' : 4 , ' event_type ' : ' Sports ' , ' event_type_icon ' : null } ,
{ ' id ' : 5 , ' event_type ' : ' Conference ' , ' event_type_icon ' : null } ,
{ ' id ' : 6 , ' event_type ' : ' Exhibition ' , ' event_type_icon ' : null } ,
] ,
} ;
} else if ( url . contains ( ' /events/pincode-events/ ' ) ) {
if ( kDebugMode ) debugPrint ( ' Development mode: returning mock events ' ) ;
return { ' events ' : _mockEvents } ;
} else if ( url . contains ( ' /events/event-details/ ' ) ) {
if ( kDebugMode ) debugPrint ( ' Development mode: returning mock event detail ' ) ;
final eventId = finalBody [ ' event_id ' ] ? ? 1 ;
final match = _mockEvents . where ( ( e ) = > e [ ' id ' ] = = eventId ) ;
return match . isNotEmpty
? Map < String , dynamic > . from ( match . first )
: Map < String , dynamic > . from ( _mockEvents . first ) ;
} else if ( url . contains ( ' /events/events-by-month-year/ ' ) ) {
if ( kDebugMode ) debugPrint ( ' Development mode: returning mock calendar ' ) ;
return {
' total_number_of_events ' : 3 ,
' dates ' : [ ' 2026-04-05 ' , ' 2026-04-12 ' , ' 2026-04-20 ' ] ,
' date_events ' : [
{ ' date ' : ' 2026-04-05 ' , ' count ' : 1 } ,
{ ' date ' : ' 2026-04-12 ' , ' count ' : 2 } ,
{ ' date ' : ' 2026-04-20 ' , ' count ' : 1 } ,
] ,
} ;
2026-03-14 08:57:25 +05:30
}
}
2026-01-31 15:23:18 +05:30
throw Exception ( ' Network error: $ e ' ) ;
}
return _handleResponse ( url , response , finalBody ) ;
}
2026-04-19 21:40:17 +05:30
/// POST with one retry on transient network errors.
/// Retries on SocketException / TimeoutException only.
Future < http . Response > _postWithRetry (
String url ,
Map < String , String > headers ,
Map < String , dynamic > body ,
) async {
try {
return await http
. post ( Uri . parse ( url ) , headers: headers , body: jsonEncode ( body ) )
. timeout ( _timeout ) ;
} on SocketException {
if ( kDebugMode ) debugPrint ( ' ApiClient.post retry after SocketException ' ) ;
await Future . delayed ( _retryDelay ) ;
return await http
. post ( Uri . parse ( url ) , headers: headers , body: jsonEncode ( body ) )
. timeout ( _timeout ) ;
} on TimeoutException {
if ( kDebugMode ) debugPrint ( ' ApiClient.post retry after TimeoutException ' ) ;
await Future . delayed ( _retryDelay ) ;
return await http
. post ( Uri . parse ( url ) , headers: headers , body: jsonEncode ( body ) )
. timeout ( _timeout ) ;
}
}
2026-04-08 21:12:49 +05:30
/// Upload a single file as multipart/form-data.
///
/// Returns the `file` object from the server response:
/// `{ fileId, url, name, type, mimeType, size, backend }`
Future < Map < String , dynamic > > uploadFile ( String url , String filePath ) async {
final request = http . MultipartRequest ( ' POST ' , Uri . parse ( url ) ) ;
2026-04-08 21:17:55 +05:30
const _mimeMap = < String , List < String > > {
' jpg ' : [ ' image ' , ' jpeg ' ] ,
' jpeg ' : [ ' image ' , ' jpeg ' ] ,
' png ' : [ ' image ' , ' png ' ] ,
' webp ' : [ ' image ' , ' webp ' ] ,
' mp4 ' : [ ' video ' , ' mp4 ' ] ,
' mov ' : [ ' video ' , ' quicktime ' ] ,
} ;
final ext = filePath . split ( ' . ' ) . last . toLowerCase ( ) ;
final parts = _mimeMap [ ext ] ? ? [ ' image ' , ' jpeg ' ] ;
request . files . add ( await http . MultipartFile . fromPath (
' file ' ,
filePath ,
contentType: MediaType ( parts [ 0 ] , parts [ 1 ] ) ,
) ) ;
2026-04-08 21:12:49 +05:30
late http . StreamedResponse streamed ;
try {
streamed = await request . send ( ) . timeout ( const Duration ( seconds: 60 ) ) ;
} catch ( e ) {
throw Exception ( ' Upload network error: $ e ' ) ;
}
final body = await streamed . stream . bytesToString ( ) ;
dynamic decoded ;
try {
decoded = jsonDecode ( body ) ;
} catch ( _ ) {
throw Exception ( ' Upload response parse error ' ) ;
}
if ( streamed . statusCode > = 200 & & streamed . statusCode < 300 ) {
if ( decoded is Map < String , dynamic > & & decoded [ ' file ' ] is Map ) {
return Map < String , dynamic > . from ( decoded [ ' file ' ] as Map ) ;
}
return decoded is Map < String , dynamic > ? decoded : { } ;
}
final msg = ( decoded is Map & & decoded [ ' message ' ] is String )
? decoded [ ' message ' ] as String
: ' Upload failed ( ${ streamed . statusCode } ) ' ;
throw Exception ( msg ) ;
}
2026-01-31 15:23:18 +05:30
/// GET request
///
/// - If requiresAuth==true, token & username will be attached as query parameters.
/// - `params` will be appended as query parameters.
Future < Map < String , dynamic > > get (
String url , {
Map < String , dynamic > ? params ,
bool requiresAuth = true ,
} ) async {
// build final query params including auth if needed
2026-04-08 19:18:33 +05:30
final originalUri = Uri . parse ( url ) ;
final queryParams = < String , String > { . . . originalUri . queryParameters } ;
2026-01-31 15:23:18 +05:30
if ( requiresAuth ) {
final token = await TokenStorage . getToken ( ) ;
final username = await TokenStorage . getUsername ( ) ;
2026-03-20 22:40:50 +05:30
if ( token ! = null & & username ! = null ) {
2026-04-08 19:18:33 +05:30
queryParams [ ' token ' ] = token ;
queryParams [ ' username ' ] = username ;
2026-01-31 15:23:18 +05:30
}
2026-03-20 22:40:50 +05:30
// Guest mode: proceed without token — let backend decide
2026-01-31 15:23:18 +05:30
}
2026-04-08 19:18:33 +05:30
if ( params ! = null ) {
queryParams . addAll ( params . map ( ( k , v ) = > MapEntry ( k , v ? . toString ( ) ? ? ' ' ) ) ) ;
}
2026-01-31 15:23:18 +05:30
2026-04-08 19:18:33 +05:30
final uri = originalUri . replace ( queryParameters: queryParams ) ;
2026-01-31 15:23:18 +05:30
late http . Response response ;
try {
response = await http . get ( uri ) . timeout ( _timeout ) ;
} catch ( e ) {
if ( kDebugMode ) debugPrint ( ' ApiClient.get network error: $ e ' ) ;
throw Exception ( ' Network error: $ e ' ) ;
}
2026-04-08 19:18:33 +05:30
return _handleResponse ( url , response , queryParams ) ;
2026-01-31 15:23:18 +05:30
}
2026-03-30 10:05:23 +05:30
// ---------------------------------------------------------------------------
// Mock event data for development / offline mode
// ---------------------------------------------------------------------------
static final List < Map < String , dynamic > > _mockEvents = [
{
' id ' : 1 ,
' name ' : ' Tech Innovation Summit 2026 ' ,
' title ' : ' Tech Innovation Summit ' ,
' description ' :
' Join industry leaders for a two-day summit exploring the latest breakthroughs in AI, cloud computing, and sustainable technology. Featuring keynote speakers, hands-on workshops, and networking sessions. ' ,
' start_date ' : ' 2026-04-15 ' ,
' end_date ' : ' 2026-04-16 ' ,
' start_time ' : ' 09:00 ' ,
' end_time ' : ' 18:00 ' ,
2026-04-07 20:49:40 +05:30
' pincode ' : ' 680001 ' ,
' place ' : ' Thekkinkadu Maidanam ' ,
2026-03-30 10:05:23 +05:30
' is_bookable ' : true ,
' event_type ' : 5 ,
' thumb_img ' : ' https://picsum.photos/seed/event1/600/400 ' ,
' images ' : [
{ ' is_primary ' : true , ' image ' : ' https://picsum.photos/seed/event1a/800/500 ' } ,
{ ' is_primary ' : false , ' image ' : ' https://picsum.photos/seed/event1b/800/500 ' } ,
] ,
' important_information ' : ' Please carry a valid photo ID for entry. ' ,
2026-04-07 20:49:40 +05:30
' venue_name ' : ' Maidanam Grounds ' ,
2026-03-30 10:05:23 +05:30
' event_status ' : ' active ' ,
2026-04-07 20:49:40 +05:30
' latitude ' : 10.5276 ,
' longitude ' : 76.2144 ,
' location_name ' : ' Thrissur ' ,
2026-03-30 10:05:23 +05:30
' important_info ' : [
{ ' title ' : ' Entry ' , ' value ' : ' Free with registration ' } ,
{ ' title ' : ' Parking ' , ' value ' : ' Available on-site ' } ,
] ,
} ,
{
' id ' : 2 ,
' name ' : ' Sunset Music Festival ' ,
' title ' : ' Sunset Music Festival ' ,
' description ' :
' An open-air music festival featuring live performances from top artists across genres. Enjoy food stalls, art installations, and an unforgettable sunset experience. ' ,
' start_date ' : ' 2026-04-20 ' ,
' end_date ' : ' 2026-04-20 ' ,
' start_time ' : ' 16:00 ' ,
' end_time ' : ' 23:00 ' ,
' pincode ' : ' 400001 ' ,
' place ' : ' Marine Drive Amphitheatre ' ,
' is_bookable ' : true ,
' event_type ' : 1 ,
' thumb_img ' : ' https://picsum.photos/seed/event2/600/400 ' ,
' images ' : [
{ ' is_primary ' : true , ' image ' : ' https://picsum.photos/seed/event2a/800/500 ' } ,
] ,
' venue_name ' : ' Marine Drive Amphitheatre ' ,
' event_status ' : ' active ' ,
' latitude ' : 18.9432 ,
' longitude ' : 72.8235 ,
' location_name ' : ' Mumbai ' ,
' important_info ' : [
{ ' title ' : ' Age Limit ' , ' value ' : ' 16+ ' } ,
] ,
} ,
{
' id ' : 3 ,
' name ' : ' Creative Design Workshop ' ,
' title ' : ' Hands-on Design Workshop ' ,
' description ' :
' A full-day workshop on UI/UX design principles, prototyping in Figma, and building design systems. Perfect for beginners and intermediate designers. ' ,
' start_date ' : ' 2026-05-03 ' ,
' end_date ' : ' 2026-05-03 ' ,
' start_time ' : ' 10:00 ' ,
' end_time ' : ' 17:00 ' ,
' pincode ' : ' 110001 ' ,
' place ' : ' Design Hub Co-working ' ,
' is_bookable ' : true ,
' event_type ' : 2 ,
' thumb_img ' : ' https://picsum.photos/seed/event3/600/400 ' ,
' images ' : [
{ ' is_primary ' : true , ' image ' : ' https://picsum.photos/seed/event3a/800/500 ' } ,
] ,
' venue_name ' : ' Design Hub ' ,
' event_status ' : ' active ' ,
' latitude ' : 28.6139 ,
' longitude ' : 77.2090 ,
' location_name ' : ' New Delhi ' ,
' important_info ' : [
{ ' title ' : ' Bring ' , ' value ' : ' Laptop with Figma installed ' } ,
{ ' title ' : ' Seats ' , ' value ' : ' 30 max ' } ,
] ,
} ,
{
' id ' : 4 ,
' name ' : ' Marathon for a Cause ' ,
' title ' : ' City Marathon 2026 ' ,
' description ' :
' Run for fitness, run for charity! Choose from 5K, 10K, or full marathon routes through the city. All proceeds support local education initiatives. ' ,
' start_date ' : ' 2026-04-12 ' ,
' end_date ' : ' 2026-04-12 ' ,
' start_time ' : ' 05:30 ' ,
' end_time ' : ' 12:00 ' ,
' pincode ' : ' 600001 ' ,
' place ' : ' Marina Beach Road ' ,
' is_bookable ' : true ,
' event_type ' : 4 ,
' thumb_img ' : ' https://picsum.photos/seed/event4/600/400 ' ,
' images ' : [
{ ' is_primary ' : true , ' image ' : ' https://picsum.photos/seed/event4a/800/500 ' } ,
] ,
' venue_name ' : ' Marina Beach ' ,
' event_status ' : ' active ' ,
' latitude ' : 13.0500 ,
' longitude ' : 80.2824 ,
' location_name ' : ' Chennai ' ,
' important_info ' : [
{ ' title ' : ' Registration ' , ' value ' : ' Closes April 10 ' } ,
] ,
} ,
{
' id ' : 5 ,
' name ' : ' Art & Culture Exhibition ' ,
' title ' : ' Contemporary Art Exhibition ' ,
' description ' :
' Explore contemporary artworks from emerging and established artists. The exhibition features paintings, sculptures, and digital art installations. ' ,
' start_date ' : ' 2026-05-10 ' ,
' end_date ' : ' 2026-05-15 ' ,
' start_time ' : ' 11:00 ' ,
' end_time ' : ' 20:00 ' ,
' pincode ' : ' 500001 ' ,
' place ' : ' Salar Jung Museum Grounds ' ,
' is_bookable ' : true ,
' event_type ' : 6 ,
' thumb_img ' : ' https://picsum.photos/seed/event5/600/400 ' ,
' images ' : [
{ ' is_primary ' : true , ' image ' : ' https://picsum.photos/seed/event5a/800/500 ' } ,
{ ' is_primary ' : false , ' image ' : ' https://picsum.photos/seed/event5b/800/500 ' } ,
] ,
' venue_name ' : ' Salar Jung Museum ' ,
' event_status ' : ' active ' ,
' latitude ' : 17.3713 ,
' longitude ' : 78.4804 ,
' location_name ' : ' Hyderabad ' ,
' important_info ' : [
{ ' title ' : ' Entry Fee ' , ' value ' : ' ₹200 ' } ,
{ ' title ' : ' Photography ' , ' value ' : ' Allowed without flash ' } ,
] ,
} ,
] ;
2026-03-20 22:40:50 +05:30
/// Build request body and attach token + username if available
2026-01-31 15:23:18 +05:30
Future < Map < String , dynamic > > _buildAuthBody ( Map < String , dynamic > ? body , bool requiresAuth ) async {
final Map < String , dynamic > finalBody = { } ;
if ( requiresAuth ) {
final token = await TokenStorage . getToken ( ) ;
final username = await TokenStorage . getUsername ( ) ;
2026-03-20 22:40:50 +05:30
if ( token ! = null & & username ! = null ) {
finalBody [ ' token ' ] = token ;
finalBody [ ' username ' ] = username ;
2026-01-31 15:23:18 +05:30
}
2026-03-20 22:40:50 +05:30
// Guest mode: proceed without token — let backend decide
2026-01-31 15:23:18 +05:30
}
if ( body ! = null ) finalBody . addAll ( body ) ;
return finalBody ;
}
/// Centralized response handling and error parsing
Map < String , dynamic > _handleResponse ( String url , http . Response response , Map < String , dynamic > requestBody ) {
dynamic decoded ;
try {
decoded = jsonDecode ( response . body ) ;
} catch ( e ) {
decoded = response . body ;
}
if ( kDebugMode ) {
debugPrint ( ' API -> $ url ' ) ;
debugPrint ( ' Status: ${ response . statusCode } ' ) ;
debugPrint ( ' Request body: ${ jsonEncode ( requestBody ) } ' ) ;
debugPrint ( ' Response body: ${ response . body } ' ) ;
}
if ( response . statusCode > = 200 & & response . statusCode < 300 ) {
if ( decoded is Map < String , dynamic > ) return decoded ;
return { ' data ' : decoded } ;
}
// Build human-friendly message from common server patterns
String errorMessage = ' Request failed (status ${ response . statusCode } ) ' ;
if ( decoded is Map ) {
// 1) If there's an explicit top-level 'message' (string), prefer it
if ( decoded . containsKey ( ' message ' ) & & decoded [ ' message ' ] is String ) {
errorMessage = decoded [ ' message ' ] as String ;
}
// 2) If 'errors' exists and is a map, collect inner messages
else if ( decoded . containsKey ( ' errors ' ) ) {
final errs = decoded [ ' errors ' ] ;
final messages = < String > [ ] ;
if ( errs is String ) {
messages . add ( errs ) ;
} else if ( errs is List ) {
for ( final e in errs ) messages . add ( e . toString ( ) ) ;
} else if ( errs is Map ) {
// collect first-level messages (prefer the text, not the key)
errs . forEach ( ( k , v ) {
if ( v is List & & v . isNotEmpty ) {
messages . add ( v . first . toString ( ) ) ;
} else if ( v is String ) {
messages . add ( v ) ;
} else {
messages . add ( v . toString ( ) ) ;
}
} ) ;
} else {
messages . add ( errs . toString ( ) ) ;
}
if ( messages . isNotEmpty ) {
errorMessage = messages . join ( ' | ' ) ;
}
}
// 3) If '__all__' present (DRF default), show it
else if ( decoded . containsKey ( ' __all__ ' ) ) {
final all = decoded [ ' __all__ ' ] ;
if ( all is List ) {
errorMessage = all . join ( ' | ' ) ;
} else {
errorMessage = all . toString ( ) ;
}
}
// 4) fallback - join map values' messages (prefer strings inside lists)
else {
final messages = < String > [ ] ;
decoded . forEach ( ( k , v ) {
if ( v is List & & v . isNotEmpty ) {
messages . add ( v . first . toString ( ) ) ;
} else {
messages . add ( v . toString ( ) ) ;
}
} ) ;
errorMessage = messages . isNotEmpty ? messages . join ( ' | ' ) : decoded . toString ( ) ;
}
} else if ( decoded is String ) {
errorMessage = decoded ;
}
throw Exception ( errorMessage ) ;
}
}