feat: Enhance certificate upload process with file change verification and signed URL generation
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
Reference in New Issue
Block a user