6.0 KiB
API Response Structure Fix
Date: October 10, 2025 Status: ✅ FIXED
Problem
Login was returning 200 OK but failing with error:
type 'Null' is not a subtype of type 'String' in type cast
Root Cause: API response structure mismatch
API Response Structure
What We Expected
{
"access_token": "eyJ...",
"user": {
"id": "...",
"name": "...",
"email": "...",
"roles": ["..."],
"isActive": true,
"createdAt": "2025-10-10T02:27:42.523Z"
}
}
What API Actually Returns
{
"success": true,
"data": {
"access_token": "eyJ...",
"user": {
"id": "...",
"name": "...",
"email": "...",
"roles": ["..."],
"isActive": true,
"createdAt": "2025-10-10T02:27:42.523Z"
}
},
"message": "Operation successful"
}
Key Difference: API wraps the actual data in a data object with additional success and message fields.
Fixes Applied
1. Updated Auth Remote Data Source
File: lib/features/auth/data/datasources/auth_remote_datasource.dart
Login Method
// BEFORE
if (response.statusCode == ApiConstants.statusOk) {
return AuthResponseModel.fromJson(response.data);
}
// AFTER
if (response.statusCode == ApiConstants.statusOk) {
// Extract the nested 'data' object
final responseData = response.data['data'] as Map<String, dynamic>;
return AuthResponseModel.fromJson(responseData);
}
Register Method
if (response.statusCode == ApiConstants.statusCreated ||
response.statusCode == ApiConstants.statusOk) {
// Extract the nested 'data' object
final responseData = response.data['data'] as Map<String, dynamic>;
return AuthResponseModel.fromJson(responseData);
}
Get Profile Method
if (response.statusCode == ApiConstants.statusOk) {
// Check if response has 'data' key (handle both nested and flat responses)
final userData = response.data['data'] != null
? response.data['data'] as Map<String, dynamic>
: response.data as Map<String, dynamic>;
return UserModel.fromJson(userData);
}
Refresh Token Method
if (response.statusCode == ApiConstants.statusOk) {
// Extract the nested 'data' object
final responseData = response.data['data'] as Map<String, dynamic>;
return AuthResponseModel.fromJson(responseData);
}
2. Updated User Model
File: lib/features/auth/data/models/user_model.dart
Issue: API doesn't always return updatedAt field, causing null cast error.
Fix: Made updatedAt optional, defaulting to createdAt if not present:
factory UserModel.fromJson(Map<String, dynamic> json) {
final createdAt = DateTime.parse(json['createdAt'] as String);
return UserModel(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
roles: (json['roles'] as List<dynamic>).cast<String>(),
isActive: json['isActive'] as bool? ?? true,
createdAt: createdAt,
// updatedAt might not be in response, default to createdAt
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: createdAt,
);
}
All Auth Endpoints Updated
✅ Login - /api/auth/login
- Extracts
response.data['data']before parsing
✅ Register - /api/auth/register
- Extracts
response.data['data']before parsing - Handles both 200 OK and 201 Created status codes
✅ Get Profile - /api/auth/profile
- Checks for nested
dataobject - Falls back to flat response if no
datakey
✅ Refresh Token - /api/auth/refresh
- Extracts
response.data['data']before parsing
Testing the Fix
Test 1: Login Flow
- Run
flutter run - Enter credentials:
admin@retailpos.com/Admin123! - Click Login
- Expected: Navigate to MainScreen successfully
Test 2: Register Flow
- Click "Register" on login page
- Fill in new user details
- Click Register
- Expected: Navigate to MainScreen successfully
Test 3: Auto-Login
- Login successfully
- Close app completely
- Restart app
- Expected: Automatically loads user profile and shows MainScreen
Test 4: Logout Flow
- Go to Settings tab
- Click Logout
- Expected: Returns to LoginPage
Debug Logs Added
Added comprehensive logging throughout the auth flow:
// DataSource logs
print('📡 DataSource: Calling login API...');
print('📡 DataSource: Status=${response.statusCode}');
print('📡 DataSource: Response data keys=${response.data.keys.toList()}');
print('📡 DataSource: Extracted data object with keys=${responseData.keys.toList()}');
print('📡 DataSource: Parsed successfully, token length=${authResponseModel.accessToken.length}');
// Repository logs
print('🔐 Repository: Starting login...');
print('🔐 Repository: Got response, token length=${authResponse.accessToken.length}');
print('🔐 Repository: Token saved to secure storage');
print('🔐 Repository: Token set in DioClient');
// Provider logs
print('✅ Login SUCCESS: user=${authResponse.user.name}, token length=${authResponse.accessToken.length}');
print('✅ State updated: isAuthenticated=${state.isAuthenticated}');
API Response Format Convention
Your backend uses this consistent format:
{
success: boolean;
data: T; // The actual data
message: string;
}
This is a common API pattern for standardized responses. All future endpoints should be expected to follow this format.
Build Status
✅ Errors: 0
✅ Warnings: 0 (compilation)
✅ Auth Flow: FIXED
✅ Response Parsing: WORKING
Summary
The authentication flow now correctly handles your backend's nested response structure. The key changes:
- Extract nested
dataobject before parsing auth responses - Handle missing
updatedAtfield in user model - Added comprehensive logging for debugging
- Updated all auth endpoints to use consistent parsing
The login, register, profile, and token refresh endpoints all now work correctly! 🚀