refactor: Implement custom DioClient with AuthInterceptor and strongly typed API service responses.
This commit is contained in:
@@ -11,6 +11,7 @@ export 'src/presentation/observers/core_bloc_observer.dart';
|
|||||||
export 'src/config/app_config.dart';
|
export 'src/config/app_config.dart';
|
||||||
export 'src/routing/routing.dart';
|
export 'src/routing/routing.dart';
|
||||||
export 'src/services/api_service/api_service.dart';
|
export 'src/services/api_service/api_service.dart';
|
||||||
|
export 'src/services/api_service/dio_client.dart';
|
||||||
|
|
||||||
// Core API Services
|
// Core API Services
|
||||||
export 'src/services/api_service/core_api_services/core_api_endpoints.dart';
|
export 'src/services/api_service/core_api_services/core_api_endpoints.dart';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class CoreModule extends Module {
|
|||||||
@override
|
@override
|
||||||
void exportedBinds(Injector i) {
|
void exportedBinds(Injector i) {
|
||||||
// 1. Register the base HTTP client
|
// 1. Register the base HTTP client
|
||||||
i.addSingleton<Dio>(() => Dio());
|
i.addSingleton<Dio>(() => DioClient());
|
||||||
|
|
||||||
// 2. Register the base API service
|
// 2. Register the base API service
|
||||||
i.addSingleton<BaseApiService>(() => ApiService(i.get<Dio>()));
|
i.addSingleton<BaseApiService>(() => ApiService(i.get<Dio>()));
|
||||||
@@ -31,7 +31,7 @@ class CoreModule extends Module {
|
|||||||
i.addSingleton<LlmService>(() => LlmService(i.get<BaseApiService>()));
|
i.addSingleton<LlmService>(() => LlmService(i.get<BaseApiService>()));
|
||||||
|
|
||||||
// 4. Register Device dependency
|
// 4. Register Device dependency
|
||||||
i.addSingleton<ImagePicker>(ImagePicker.new);
|
i.addSingleton<ImagePicker>(() => ImagePicker());
|
||||||
|
|
||||||
// 5. Register Device Services
|
// 5. Register Device Services
|
||||||
i.addSingleton<CameraService>(() => CameraService(i.get<ImagePicker>()));
|
i.addSingleton<CameraService>(() => CameraService(i.get<ImagePicker>()));
|
||||||
|
|||||||
@@ -88,21 +88,9 @@ class ApiService implements BaseApiService {
|
|||||||
|
|
||||||
/// Extracts [ApiResponse] from a successful [Response].
|
/// Extracts [ApiResponse] from a successful [Response].
|
||||||
ApiResponse _handleResponse(Response<dynamic> response) {
|
ApiResponse _handleResponse(Response<dynamic> response) {
|
||||||
if (response.data is Map<String, dynamic>) {
|
|
||||||
final Map<String, dynamic> body = response.data as Map<String, dynamic>;
|
|
||||||
return ApiResponse(
|
|
||||||
code:
|
|
||||||
body['code']?.toString() ??
|
|
||||||
response.statusCode?.toString() ??
|
|
||||||
'unknown',
|
|
||||||
message: body['message']?.toString() ?? 'Success',
|
|
||||||
data: body['data'],
|
|
||||||
errors: _parseErrors(body['errors']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ApiResponse(
|
return ApiResponse(
|
||||||
code: response.statusCode?.toString() ?? '200',
|
code: response.statusCode?.toString() ?? '200',
|
||||||
message: 'Success',
|
message: response.data['message']?.toString() ?? 'Success',
|
||||||
data: response.data,
|
data: response.data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../core_api_endpoints.dart';
|
import '../core_api_endpoints.dart';
|
||||||
|
import 'file_upload_response.dart';
|
||||||
|
|
||||||
/// Service for uploading files to the Core API.
|
/// Service for uploading files to the Core API.
|
||||||
class FileUploadService extends BaseCoreService {
|
class FileUploadService extends BaseCoreService {
|
||||||
@@ -12,13 +13,13 @@ class FileUploadService extends BaseCoreService {
|
|||||||
/// [filePath] is the local path to the file.
|
/// [filePath] is the local path to the file.
|
||||||
/// [visibility] can be [FileVisibility.public] or [FileVisibility.private].
|
/// [visibility] can be [FileVisibility.public] or [FileVisibility.private].
|
||||||
/// [category] is an optional metadata field.
|
/// [category] is an optional metadata field.
|
||||||
Future<ApiResponse> uploadFile({
|
Future<FileUploadResponse> uploadFile({
|
||||||
required String filePath,
|
required String filePath,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
FileVisibility visibility = FileVisibility.private,
|
FileVisibility visibility = FileVisibility.private,
|
||||||
String? category,
|
String? category,
|
||||||
}) async {
|
}) async {
|
||||||
return action(() async {
|
final ApiResponse res = await action(() async {
|
||||||
final FormData formData = FormData.fromMap(<String, dynamic>{
|
final FormData formData = FormData.fromMap(<String, dynamic>{
|
||||||
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
'file': await MultipartFile.fromFile(filePath, filename: fileName),
|
||||||
'visibility': visibility.value,
|
'visibility': visibility.value,
|
||||||
@@ -27,5 +28,11 @@ class FileUploadService extends BaseCoreService {
|
|||||||
|
|
||||||
return api.post(CoreApiEndpoints.uploadFile, data: formData);
|
return api.post(CoreApiEndpoints.uploadFile, data: formData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return FileUploadResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../core_api_endpoints.dart';
|
import '../core_api_endpoints.dart';
|
||||||
|
import 'llm_response.dart';
|
||||||
|
|
||||||
/// Service for invoking Large Language Models (LLM).
|
/// Service for invoking Large Language Models (LLM).
|
||||||
class LlmService extends BaseCoreService {
|
class LlmService extends BaseCoreService {
|
||||||
@@ -11,12 +12,12 @@ class LlmService extends BaseCoreService {
|
|||||||
/// [prompt] is the text instruction for the model.
|
/// [prompt] is the text instruction for the model.
|
||||||
/// [responseJsonSchema] is an optional JSON schema to enforce structure.
|
/// [responseJsonSchema] is an optional JSON schema to enforce structure.
|
||||||
/// [fileUrls] are optional URLs of files (images/PDFs) to include in context.
|
/// [fileUrls] are optional URLs of files (images/PDFs) to include in context.
|
||||||
Future<ApiResponse> invokeLlm({
|
Future<LlmResponse> invokeLlm({
|
||||||
required String prompt,
|
required String prompt,
|
||||||
Map<String, dynamic>? responseJsonSchema,
|
Map<String, dynamic>? responseJsonSchema,
|
||||||
List<String>? fileUrls,
|
List<String>? fileUrls,
|
||||||
}) async {
|
}) async {
|
||||||
return action(() async {
|
final ApiResponse res = await action(() async {
|
||||||
return api.post(
|
return api.post(
|
||||||
CoreApiEndpoints.invokeLlm,
|
CoreApiEndpoints.invokeLlm,
|
||||||
data: <String, dynamic>{
|
data: <String, dynamic>{
|
||||||
@@ -27,5 +28,11 @@ class LlmService extends BaseCoreService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return LlmResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../core_api_endpoints.dart';
|
import '../core_api_endpoints.dart';
|
||||||
|
import 'signed_url_response.dart';
|
||||||
|
|
||||||
/// Service for creating signed URLs for Cloud Storage objects.
|
/// Service for creating signed URLs for Cloud Storage objects.
|
||||||
class SignedUrlService extends BaseCoreService {
|
class SignedUrlService extends BaseCoreService {
|
||||||
@@ -10,11 +11,11 @@ class SignedUrlService extends BaseCoreService {
|
|||||||
///
|
///
|
||||||
/// [fileUri] should be in gs:// format.
|
/// [fileUri] should be in gs:// format.
|
||||||
/// [expiresInSeconds] must be <= 900.
|
/// [expiresInSeconds] must be <= 900.
|
||||||
Future<ApiResponse> createSignedUrl({
|
Future<SignedUrlResponse> createSignedUrl({
|
||||||
required String fileUri,
|
required String fileUri,
|
||||||
int expiresInSeconds = 300,
|
int expiresInSeconds = 300,
|
||||||
}) async {
|
}) async {
|
||||||
return action(() async {
|
final ApiResponse res = await action(() async {
|
||||||
return api.post(
|
return api.post(
|
||||||
CoreApiEndpoints.createSignedUrl,
|
CoreApiEndpoints.createSignedUrl,
|
||||||
data: <String, dynamic>{
|
data: <String, dynamic>{
|
||||||
@@ -23,5 +24,11 @@ class SignedUrlService extends BaseCoreService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return SignedUrlResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../core_api_endpoints.dart';
|
import '../core_api_endpoints.dart';
|
||||||
|
import 'verification_response.dart';
|
||||||
|
|
||||||
/// Service for handling async verification jobs.
|
/// Service for handling async verification jobs.
|
||||||
class VerificationService extends BaseCoreService {
|
class VerificationService extends BaseCoreService {
|
||||||
@@ -11,14 +12,14 @@ class VerificationService extends BaseCoreService {
|
|||||||
/// [type] can be 'attire', 'government_id', etc.
|
/// [type] can be 'attire', 'government_id', etc.
|
||||||
/// [subjectType] is usually 'worker'.
|
/// [subjectType] is usually 'worker'.
|
||||||
/// [fileUri] is the gs:// path of the uploaded file.
|
/// [fileUri] is the gs:// path of the uploaded file.
|
||||||
Future<ApiResponse> createVerification({
|
Future<VerificationResponse> createVerification({
|
||||||
required String type,
|
required String type,
|
||||||
required String subjectType,
|
required String subjectType,
|
||||||
required String subjectId,
|
required String subjectId,
|
||||||
required String fileUri,
|
required String fileUri,
|
||||||
Map<String, dynamic>? rules,
|
Map<String, dynamic>? rules,
|
||||||
}) async {
|
}) async {
|
||||||
return action(() async {
|
final ApiResponse res = await action(() async {
|
||||||
return api.post(
|
return api.post(
|
||||||
CoreApiEndpoints.verifications,
|
CoreApiEndpoints.verifications,
|
||||||
data: <String, dynamic>{
|
data: <String, dynamic>{
|
||||||
@@ -30,25 +31,37 @@ class VerificationService extends BaseCoreService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return VerificationResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Polls the status of a specific verification.
|
/// Polls the status of a specific verification.
|
||||||
Future<ApiResponse> getStatus(String verificationId) async {
|
Future<VerificationResponse> getStatus(String verificationId) async {
|
||||||
return action(() async {
|
final ApiResponse res = await action(() async {
|
||||||
return api.get(CoreApiEndpoints.verificationStatus(verificationId));
|
return api.get(CoreApiEndpoints.verificationStatus(verificationId));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return VerificationResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Submits a manual review decision.
|
/// Submits a manual review decision.
|
||||||
///
|
///
|
||||||
/// [decision] should be 'APPROVED' or 'REJECTED'.
|
/// [decision] should be 'APPROVED' or 'REJECTED'.
|
||||||
Future<ApiResponse> reviewVerification({
|
Future<VerificationResponse> reviewVerification({
|
||||||
required String verificationId,
|
required String verificationId,
|
||||||
required String decision,
|
required String decision,
|
||||||
String? note,
|
String? note,
|
||||||
String? reasonCode,
|
String? reasonCode,
|
||||||
}) async {
|
}) async {
|
||||||
return action(() async {
|
final ApiResponse res = await action(() async {
|
||||||
return api.post(
|
return api.post(
|
||||||
CoreApiEndpoints.verificationReview(verificationId),
|
CoreApiEndpoints.verificationReview(verificationId),
|
||||||
data: <String, dynamic>{
|
data: <String, dynamic>{
|
||||||
@@ -58,12 +71,24 @@ class VerificationService extends BaseCoreService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return VerificationResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retries a verification job that failed or needs re-processing.
|
/// Retries a verification job that failed or needs re-processing.
|
||||||
Future<ApiResponse> retryVerification(String verificationId) async {
|
Future<VerificationResponse> retryVerification(String verificationId) async {
|
||||||
return action(() async {
|
final ApiResponse res = await action(() async {
|
||||||
return api.post(CoreApiEndpoints.verificationRetry(verificationId));
|
return api.post(CoreApiEndpoints.verificationRetry(verificationId));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (res.code.startsWith('2')) {
|
||||||
|
return VerificationResponse.fromJson(res.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(res.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:krow_core/src/services/api_service/inspectors/auth_interceptor.dart';
|
||||||
|
|
||||||
|
/// A custom Dio client for the Krow project that includes basic configuration
|
||||||
|
/// and an [AuthInterceptor].
|
||||||
|
class DioClient extends DioMixin implements Dio {
|
||||||
|
DioClient([BaseOptions? baseOptions]) {
|
||||||
|
options =
|
||||||
|
baseOptions ??
|
||||||
|
BaseOptions(
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the default adapter
|
||||||
|
httpClientAdapter = HttpClientAdapter();
|
||||||
|
|
||||||
|
// Add interceptors
|
||||||
|
interceptors.addAll(<Interceptor>[
|
||||||
|
AuthInterceptor(),
|
||||||
|
LogInterceptor(
|
||||||
|
requestBody: true,
|
||||||
|
responseBody: true,
|
||||||
|
), // Added for better debugging
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
|
/// An interceptor that adds the Firebase Auth ID token to the Authorization header.
|
||||||
|
class AuthInterceptor extends Interceptor {
|
||||||
|
@override
|
||||||
|
Future<void> onRequest(
|
||||||
|
RequestOptions options,
|
||||||
|
RequestInterceptorHandler handler,
|
||||||
|
) async {
|
||||||
|
final User? user = FirebaseAuth.instance.currentUser;
|
||||||
|
if (user != null) {
|
||||||
|
try {
|
||||||
|
final String? token = await user.getIdToken();
|
||||||
|
if (token != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handler.next(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import '../camera/camera_service.dart';
|
import '../camera/camera_service.dart';
|
||||||
import '../gallery/gallery_service.dart';
|
import '../gallery/gallery_service.dart';
|
||||||
import '../../api_service/core_api_services/file_upload/file_upload_service.dart';
|
import '../../api_service/core_api_services/file_upload/file_upload_service.dart';
|
||||||
|
import '../../api_service/core_api_services/file_upload/file_upload_response.dart';
|
||||||
|
|
||||||
/// Orchestrator service that combines device picking and network uploading.
|
/// Orchestrator service that combines device picking and network uploading.
|
||||||
///
|
///
|
||||||
@@ -20,7 +21,7 @@ class DeviceFileUploadService extends BaseDeviceService {
|
|||||||
final FileUploadService apiUploadService;
|
final FileUploadService apiUploadService;
|
||||||
|
|
||||||
/// Captures a photo from the camera and uploads it immediately.
|
/// Captures a photo from the camera and uploads it immediately.
|
||||||
Future<ApiResponse?> uploadFromCamera({
|
Future<FileUploadResponse?> uploadFromCamera({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
FileVisibility visibility = FileVisibility.private,
|
FileVisibility visibility = FileVisibility.private,
|
||||||
String? category,
|
String? category,
|
||||||
@@ -39,7 +40,7 @@ class DeviceFileUploadService extends BaseDeviceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Picks an image from the gallery and uploads it immediately.
|
/// Picks an image from the gallery and uploads it immediately.
|
||||||
Future<ApiResponse?> uploadFromGallery({
|
Future<FileUploadResponse?> uploadFromGallery({
|
||||||
required String fileName,
|
required String fileName,
|
||||||
FileVisibility visibility = FileVisibility.private,
|
FileVisibility visibility = FileVisibility.private,
|
||||||
String? category,
|
String? category,
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ dependencies:
|
|||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
file_picker: ^8.1.7
|
file_picker: ^8.1.7
|
||||||
|
firebase_auth: ^6.1.4
|
||||||
|
|||||||
@@ -37,23 +37,18 @@ class AttireRepositoryImpl implements AttireRepository {
|
|||||||
Future<AttireItem> uploadPhoto(String itemId, String filePath) async {
|
Future<AttireItem> uploadPhoto(String itemId, String filePath) async {
|
||||||
// 1. Upload file to Core API
|
// 1. Upload file to Core API
|
||||||
final FileUploadService uploadService = Modular.get<FileUploadService>();
|
final FileUploadService uploadService = Modular.get<FileUploadService>();
|
||||||
final ApiResponse uploadRes = await uploadService.uploadFile(
|
final FileUploadResponse uploadRes = await uploadService.uploadFile(
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
fileName: filePath.split('/').last,
|
fileName: filePath.split('/').last,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!uploadRes.code.startsWith('2')) {
|
final String fileUri = uploadRes.fileUri;
|
||||||
throw Exception('Upload failed: ${uploadRes.message}');
|
|
||||||
}
|
|
||||||
|
|
||||||
final String fileUri = uploadRes.data?['fileUri'] as String;
|
|
||||||
|
|
||||||
// 2. Create signed URL for the uploaded file
|
// 2. Create signed URL for the uploaded file
|
||||||
final SignedUrlService signedUrlService = Modular.get<SignedUrlService>();
|
final SignedUrlService signedUrlService = Modular.get<SignedUrlService>();
|
||||||
final ApiResponse signedUrlRes = await signedUrlService.createSignedUrl(
|
final SignedUrlResponse signedUrlRes = await signedUrlService
|
||||||
fileUri: fileUri,
|
.createSignedUrl(fileUri: fileUri);
|
||||||
);
|
final String photoUrl = signedUrlRes.signedUrl;
|
||||||
final String photoUrl = signedUrlRes.data?['signedUrl'] as String;
|
|
||||||
|
|
||||||
// 3. Initiate verification job
|
// 3. Initiate verification job
|
||||||
final VerificationService verificationService =
|
final VerificationService verificationService =
|
||||||
@@ -68,14 +63,15 @@ class AttireRepositoryImpl implements AttireRepository {
|
|||||||
final String dressCode =
|
final String dressCode =
|
||||||
'${targetItem.description ?? ''} ${targetItem.label}'.trim();
|
'${targetItem.description ?? ''} ${targetItem.label}'.trim();
|
||||||
|
|
||||||
final ApiResponse verifyRes = await verificationService.createVerification(
|
final VerificationResponse verifyRes = await verificationService
|
||||||
|
.createVerification(
|
||||||
type: 'attire',
|
type: 'attire',
|
||||||
subjectType: 'worker',
|
subjectType: 'worker',
|
||||||
subjectId: staff.id,
|
subjectId: staff.id,
|
||||||
fileUri: fileUri,
|
fileUri: fileUri,
|
||||||
rules: <String, dynamic>{'dressCode': dressCode},
|
rules: <String, dynamic>{'dressCode': dressCode},
|
||||||
);
|
);
|
||||||
final String verificationId = verifyRes.data?['verificationId'] as String;
|
final String verificationId = verifyRes.verificationId;
|
||||||
|
|
||||||
// 4. Poll for status until it's finished or timeout (max 10 seconds)
|
// 4. Poll for status until it's finished or timeout (max 10 seconds)
|
||||||
try {
|
try {
|
||||||
@@ -83,11 +79,10 @@ class AttireRepositoryImpl implements AttireRepository {
|
|||||||
bool isFinished = false;
|
bool isFinished = false;
|
||||||
while (!isFinished && attempts < 5) {
|
while (!isFinished && attempts < 5) {
|
||||||
await Future<void>.delayed(const Duration(seconds: 2));
|
await Future<void>.delayed(const Duration(seconds: 2));
|
||||||
final ApiResponse statusRes = await verificationService.getStatus(
|
final VerificationResponse statusRes = await verificationService
|
||||||
verificationId,
|
.getStatus(verificationId);
|
||||||
);
|
final String status = statusRes.status;
|
||||||
final String? status = statusRes.data?['status'] as String?;
|
if (status != 'PENDING' && status != 'QUEUED') {
|
||||||
if (status != null && status != 'PENDING' && status != 'QUEUED') {
|
|
||||||
isFinished = true;
|
isFinished = true;
|
||||||
}
|
}
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|||||||
Reference in New Issue
Block a user