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

View File

@@ -78,22 +78,16 @@ class BankAccountPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
SecurityNotice(strings: strings), SecurityNotice(strings: strings),
const SizedBox(height: UiConstants.space32), if (state.accounts.isEmpty) ...<Widget>[
if (state.accounts.isEmpty) const SizedBox(height: UiConstants.space32),
const UiEmptyState( const UiEmptyState(
icon: UiIcons.building, icon: UiIcons.building,
title: 'No accounts yet', title: 'No accounts yet',
description: description:
'Add your first bank account to get started', '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>( ...state.accounts.map<Widget>(
(StaffBankAccount account) => (StaffBankAccount account) =>
AccountCard(account: account, strings: strings), AccountCard(account: account, strings: strings),

View File

@@ -19,11 +19,12 @@ class TimeCardPage extends StatefulWidget {
} }
class _TimeCardPageState extends State<TimeCardPage> { class _TimeCardPageState extends State<TimeCardPage> {
final TimeCardBloc _bloc = Modular.get<TimeCardBloc>(); late final TimeCardBloc _bloc;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_bloc = Modular.get<TimeCardBloc>();
_bloc.add(LoadTimeCards(DateTime.now())); _bloc.add(LoadTimeCards(DateTime.now()));
} }
@@ -33,25 +34,9 @@ class _TimeCardPageState extends State<TimeCardPage> {
return BlocProvider.value( return BlocProvider.value(
value: _bloc, value: _bloc,
child: Scaffold( child: Scaffold(
backgroundColor: UiColors.bgPrimary, appBar: UiAppBar(
appBar: AppBar( title: t.staff_time_card.title,
backgroundColor: UiColors.bgPopup, showBackButton: true,
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),
),
), ),
body: BlocConsumer<TimeCardBloc, TimeCardState>( body: BlocConsumer<TimeCardBloc, TimeCardState>(
listener: (BuildContext context, TimeCardState state) { listener: (BuildContext context, TimeCardState state) {

View File

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

View File

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

View File

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

View File

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