Authentication
Auth Flow
┌─────────┐ Send phone ┌──────────────┐ OTP verified ┌──────────────┐
│ App │────────────────►│ POST │────────────────►│ Store JWT │
│ │ │ /login │ │ + PIN hash │
└─────────┘ └──────────────┘ └──────────────┘
┌─────────┐ Re-open app ┌──────────────┐ Biometric ┌──────────────┐
│ App │────────────────►│ Local auth │────────────────►│ Use stored │
│ │ │ (PIN/biometric)│ │ JWT │
└─────────┘ └──────────────┘ └──────────────┘
Steps
1. Send OTP
final response = await dioClient.post(
'/parent/user-auth/login',
data: {'phone': '+919876543210'},
);
2. Verify OTP
final response = await dioClient.post(
'/parent/user-auth/otp-verify',
data: {
'phone': '+919876543210',
'otp': '123456',
},
);
// Response
// {
// "accessToken": "...",
// "refreshToken": "...",
// "user": { "id": "...", "role": "PARENT" }
// }
// Store tokens
await storage.write('accessToken', accessToken);
await storage.write('refreshToken', refreshToken);
3. Set PIN (first login only)
await dioClient.post(
'/parent/user-auth/set-pin',
data: {'pin': '1234'},
options: Options(headers: {'Authorization': 'Bearer $accessToken'}),
);
// Store PIN hash locally
await storage.write('pinHash', hashPin('1234'));
4. Quick Re-Entry (subsequent opens)
// Biometrics (if enrolled)
final authenticated = await LocalAuthentication().authenticate(
localizedReason: 'Authenticate to access SyncAD',
);
if (authenticated) {
// Use stored access token directly
}
Token Management
// dio_client.dart — AuthInterceptor
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = storage.read('accessToken');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
}
// TokenRefreshInterceptor handles 401 and auto-refreshes
Biometric Enrollment
// After PIN is set, offer biometric enrollment
Future<void> enrollBiometrics() async {
final canCheck = await LocalAuthentication().canCheckBiometrics;
final isDeviceSupported = await LocalAuthentication().isDeviceSupported();
if (canCheck && isDeviceSupported) {
final result = await LocalAuthentication().authenticate(
localizedReason: 'Enable biometrics for quick login',
);
if (result) {
await storage.write('biometricsEnabled', 'true');
}
}
}
Logout
await storage.delete('accessToken');
await storage.delete('refreshToken');
await storage.delete('pinHash');
// Navigate to login screen