feat: Enhance certificate upload process with file change verification and signed URL generation

This commit is contained in:
Achintha Isuru
2026-03-01 21:17:45 -05:00
parent 49ecede35f
commit 8e95589551
7 changed files with 56 additions and 76 deletions

View File

@@ -35,46 +35,63 @@ class CertificatesRepositoryImpl implements CertificatesRepository {
String? certificateNumber,
}) async {
return _service.run(() async {
// 1. Upload the file to cloud storage
final FileUploadResponse uploadRes = await _uploadService.uploadFile(
filePath: filePath,
fileName:
'staff_cert_${certificationType.name}_${DateTime.now().millisecondsSinceEpoch}.pdf',
visibility: domain.FileVisibility.private,
);
// Get existing certificate to check if file has changed
final List<domain.StaffCertificate> existingCerts = await getCertificates();
domain.StaffCertificate? existingCert;
try {
existingCert = existingCerts.firstWhere(
(domain.StaffCertificate c) => c.certificationType == certificationType,
);
} catch (e) {
// Certificate doesn't exist yet
}
// 2. Generate a signed URL for verification service to access the file
// Wait, verification service might need this or just the URI.
// Following DocumentRepository behavior:
final SignedUrlResponse signedUrlRes = await _signedUrlService.createSignedUrl(fileUri: uploadRes.fileUri);
String? signedUrl = existingCert?.certificateUrl;
String? verificationId = existingCert?.verificationId;
final bool fileChanged = existingCert == null || existingCert.certificateUrl != filePath;
// 3. Initiate verification
final String staffId = await _service.getStaffId();
final VerificationResponse verificationRes = await _verificationService
.createVerification(
fileUri: uploadRes.fileUri,
type: 'certification',
subjectType: 'worker',
subjectId: staffId,
rules: <String, dynamic>{
'certificateName': name,
'certificateIssuer': issuer,
'certificateNumber': certificateNumber,
},
);
// Only upload and verify if file path has changed
if (fileChanged) {
// 1. Upload the file to cloud storage
final FileUploadResponse uploadRes = await _uploadService.uploadFile(
filePath: filePath,
fileName:
'staff_cert_${certificationType.name}_${DateTime.now().millisecondsSinceEpoch}.pdf',
visibility: domain.FileVisibility.private,
);
// 2. Generate a signed URL for verification service to access the file
final SignedUrlResponse signedUrlRes = await _signedUrlService.createSignedUrl(fileUri: uploadRes.fileUri);
signedUrl = signedUrlRes.signedUrl;
// 3. Initiate verification
final String staffId = await _service.getStaffId();
final VerificationResponse verificationRes = await _verificationService
.createVerification(
fileUri: uploadRes.fileUri,
type: 'certification',
subjectType: 'worker',
subjectId: staffId,
rules: <String, dynamic>{
'certificateName': name,
'certificateIssuer': issuer,
'certificateNumber': certificateNumber,
},
);
verificationId = verificationRes.verificationId;
}
// 4. Update/Create Certificate in Data Connect
await _service.getStaffRepository().upsertStaffCertificate(
certificationType: certificationType,
name: name,
status: domain.StaffCertificateStatus.pending,
fileUrl: signedUrlRes.signedUrl,
status: existingCert?.status ?? domain.StaffCertificateStatus.pending,
fileUrl: signedUrl,
expiry: expiryDate,
issuer: issuer,
certificateNumber: certificateNumber,
validationStatus:
domain.StaffCertificateValidationStatus.pendingExpertReview,
verificationId: verificationRes.verificationId,
validationStatus: existingCert?.validationStatus ?? domain.StaffCertificateValidationStatus.pendingExpertReview,
verificationId: verificationId,
);
// 5. Return updated list or the specific certificate

View File

@@ -78,22 +78,16 @@ class BankAccountPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SecurityNotice(strings: strings),
const SizedBox(height: UiConstants.space32),
if (state.accounts.isEmpty)
if (state.accounts.isEmpty) ...<Widget>[
const SizedBox(height: UiConstants.space32),
const UiEmptyState(
icon: UiIcons.building,
title: 'No accounts yet',
description:
'Add your first bank account to get started',
)
else ...<Widget>[
Text(
strings.linked_accounts,
style: UiTypography.headline4m.copyWith(
color: UiColors.textPrimary,
),
),
const SizedBox(height: UiConstants.space3),
] else ...<Widget>[
const SizedBox(height: UiConstants.space4),
...state.accounts.map<Widget>(
(StaffBankAccount account) =>
AccountCard(account: account, strings: strings),

View File

@@ -19,11 +19,12 @@ class TimeCardPage extends StatefulWidget {
}
class _TimeCardPageState extends State<TimeCardPage> {
final TimeCardBloc _bloc = Modular.get<TimeCardBloc>();
late final TimeCardBloc _bloc;
@override
void initState() {
super.initState();
_bloc = Modular.get<TimeCardBloc>();
_bloc.add(LoadTimeCards(DateTime.now()));
}
@@ -33,25 +34,9 @@ class _TimeCardPageState extends State<TimeCardPage> {
return BlocProvider.value(
value: _bloc,
child: Scaffold(
backgroundColor: UiColors.bgPrimary,
appBar: AppBar(
backgroundColor: UiColors.bgPopup,
elevation: 0,
leading: IconButton(
icon: const Icon(
UiIcons.chevronLeft,
color: UiColors.iconSecondary,
),
onPressed: () => Modular.to.popSafe(),
),
title: Text(
t.staff_time_card.title,
style: UiTypography.headline4m.textPrimary,
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(color: UiColors.border, height: 1.0),
),
appBar: UiAppBar(
title: t.staff_time_card.title,
showBackButton: true,
),
body: BlocConsumer<TimeCardBloc, TimeCardState>(
listener: (BuildContext context, TimeCardState state) {

View File

@@ -17,10 +17,6 @@ class FaqsPage extends StatelessWidget {
appBar: UiAppBar(
title: t.staff_faqs.title,
showBackButton: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1),
),
),
body: BlocProvider<FaqsBloc>(
create: (BuildContext context) =>

View File

@@ -18,10 +18,6 @@ class PrivacyPolicyPage extends StatelessWidget {
appBar: UiAppBar(
title: t.staff_privacy_security.privacy_policy.title,
showBackButton: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1),
),
),
body: BlocProvider<PrivacyPolicyCubit>(
create: (BuildContext context) => Modular.get<PrivacyPolicyCubit>()..fetchPrivacyPolicy(),

View File

@@ -18,10 +18,6 @@ class TermsOfServicePage extends StatelessWidget {
appBar: UiAppBar(
title: t.staff_privacy_security.terms_of_service.title,
showBackButton: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1),
),
),
body: BlocProvider<TermsCubit>(
create: (BuildContext context) => Modular.get<TermsCubit>()..fetchTerms(),

View File

@@ -18,10 +18,6 @@ class PrivacySecurityPage extends StatelessWidget {
appBar: UiAppBar(
title: t.staff_privacy_security.title,
showBackButton: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1),
child: Container(color: UiColors.border, height: 1),
),
),
body: BlocProvider<PrivacySecurityBloc>.value(
value: Modular.get<PrivacySecurityBloc>()