diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart index 39860ed675f1..f928c78847a8 100644 --- a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart @@ -15,10 +15,9 @@ import 'package:catalyst_voices/pages/workspace/submission_closing_warning_dialo import 'package:catalyst_voices/routes/routes.dart'; import 'package:catalyst_voices/routes/routing/proposal_builder_route.dart'; import 'package:catalyst_voices/widgets/modals/comment/submit_comment_error_dialog.dart'; +import 'package:catalyst_voices/widgets/modals/proposals/proposal_error_dialog.dart'; import 'package:catalyst_voices/widgets/modals/proposals/proposal_limit_reached_dialog.dart'; -import 'package:catalyst_voices/widgets/modals/proposals/publish_proposal_error_dialog.dart'; import 'package:catalyst_voices/widgets/modals/proposals/publish_proposal_iteration_dialog.dart'; -import 'package:catalyst_voices/widgets/modals/proposals/submit_proposal_error_dialog.dart'; import 'package:catalyst_voices/widgets/modals/proposals/submit_proposal_for_review_dialog.dart'; import 'package:catalyst_voices/widgets/modals/proposals/unlock_edit_proposal.dart'; import 'package:catalyst_voices/widgets/snackbar/common_snackbars.dart'; @@ -142,8 +141,14 @@ class _ProposalBuilderBodyState extends State<_ProposalBuilderBody> unawaited(_showPublishException(error)); case ProposalBuilderSubmitException(): unawaited(_showSubmitException(error)); + case ProposalBuilderLimitReachedException(): + unawaited(_showLimitReachedException(error)); + case ProposalBuilderDocumentSignException(): + unawaited(_showDocumentSignException(error)); case LocalizedUnknownPublishCommentException(): unawaited(_showCommentException(error)); + case LocalizedException(): + unawaited(_showGenericException(error)); default: super.handleError(error); } @@ -277,6 +282,14 @@ class _ProposalBuilderBodyState extends State<_ProposalBuilderBody> ); } + Future _showDocumentSignException(ProposalBuilderDocumentSignException error) { + return ProposalErrorDialog.show( + context: context, + title: error.title(context), + message: error.message(context), + ); + } + Future _showEmailNotVerifiedDialog() async { final openAccount = await EmailNotVerifiedDialog.show(context); @@ -317,6 +330,22 @@ class _ProposalBuilderBodyState extends State<_ProposalBuilderBody> }); } + Future _showGenericException(LocalizedException error) { + return ProposalErrorDialog.show( + context: context, + title: context.l10n.somethingWentWrong, + message: error.message(context), + ); + } + + Future _showLimitReachedException(ProposalBuilderLimitReachedException error) { + return ProposalErrorDialog.show( + context: context, + title: error.title(context), + message: error.message(context), + ); + } + Future _showProposalLimitReachedDialog( MaxProposalsLimitReachedSignal signal, ) { @@ -344,9 +373,10 @@ class _ProposalBuilderBodyState extends State<_ProposalBuilderBody> } Future _showPublishException(ProposalBuilderPublishException error) { - return PublishProposalErrorDialog.show( + return ProposalErrorDialog.show( context: context, - exception: error, + title: error.title(context), + message: error.message(context), ); } @@ -380,9 +410,10 @@ class _ProposalBuilderBodyState extends State<_ProposalBuilderBody> } Future _showSubmitException(ProposalBuilderSubmitException error) { - return SubmitProposalErrorDialog.show( + return ProposalErrorDialog.show( context: context, - exception: error, + title: error.title(context), + message: error.message(context), ); } diff --git a/catalyst_voices/apps/voices/lib/widgets/modals/proposals/submit_proposal_error_dialog.dart b/catalyst_voices/apps/voices/lib/widgets/modals/proposals/proposal_error_dialog.dart similarity index 73% rename from catalyst_voices/apps/voices/lib/widgets/modals/proposals/submit_proposal_error_dialog.dart rename to catalyst_voices/apps/voices/lib/widgets/modals/proposals/proposal_error_dialog.dart index 742951b75ee3..6c782b09db77 100644 --- a/catalyst_voices/apps/voices/lib/widgets/modals/proposals/submit_proposal_error_dialog.dart +++ b/catalyst_voices/apps/voices/lib/widgets/modals/proposals/proposal_error_dialog.dart @@ -4,27 +4,27 @@ import 'package:catalyst_voices/widgets/modals/voices_info_dialog.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; -/// Error dialog when submitting proposal for review fails. -class SubmitProposalErrorDialog { +/// Generic error dialog related to proposal errors. +class ProposalErrorDialog { static Future show({ required BuildContext context, - required ProposalBuilderSubmitException exception, + required String title, + required String message, }) { return VoicesDialog.show( context: context, routeSettings: const RouteSettings( - name: '/proposal-builder/submit-error', + name: '/proposal-builder/error', ), builder: (context) { return VoicesInfoDialog( icon: VoicesAssets.icons.exclamation.buildIcon( color: Theme.of(context).colors.iconsWarning, ), - title: Text(exception.title(context)), - message: Text(exception.message(context)), + title: Text(title), + message: Text(message), action: VoicesFilledButton( onTap: () => Navigator.of(context).pop(), child: Text(context.l10n.okay), diff --git a/catalyst_voices/apps/voices/lib/widgets/modals/proposals/publish_proposal_error_dialog.dart b/catalyst_voices/apps/voices/lib/widgets/modals/proposals/publish_proposal_error_dialog.dart deleted file mode 100644 index 58d58a957f4c..000000000000 --- a/catalyst_voices/apps/voices/lib/widgets/modals/proposals/publish_proposal_error_dialog.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart'; -import 'package:catalyst_voices/widgets/modals/voices_dialog.dart'; -import 'package:catalyst_voices/widgets/modals/voices_info_dialog.dart'; -import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; -import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; -import 'package:flutter/material.dart'; - -/// Error dialog when publishing proposal to server fails. -class PublishProposalErrorDialog { - static Future show({ - required BuildContext context, - required ProposalBuilderPublishException exception, - }) { - return VoicesDialog.show( - context: context, - routeSettings: const RouteSettings( - name: '/proposal-builder/publish-error', - ), - builder: (context) { - return VoicesInfoDialog( - icon: VoicesAssets.icons.exclamation.buildIcon( - color: Theme.of(context).colors.iconsWarning, - ), - title: Text(exception.title(context)), - message: Text(exception.message(context)), - action: VoicesFilledButton( - onTap: () => Navigator.of(context).pop(), - child: Text(context.l10n.okay), - ), - ); - }, - ); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart index 57bfba8e6689..81a557ff9839 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart @@ -790,9 +790,21 @@ final class ProposalBuilderBloc extends Bloc const ProposalBuilderPublishException(), + ), + ); } finally { emit(state.copyWith(isChanging: false)); } @@ -1092,9 +1104,21 @@ final class ProposalBuilderBloc extends Bloc const ProposalBuilderSubmitException(), + ), + ); } finally { emit(state.copyWith(isChanging: false)); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb index 02e241169ee1..a0b09726041e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -2194,6 +2194,22 @@ "@proposalEditorDeleteDialogTitle": { "description": "A title for the dialog to confirm proposal deletion." }, + "proposalEditorDocumentSignErrorMessage": "We couldn't sign the proposal, please try again later.", + "@proposalEditorDocumentSignErrorMessage": { + "description": "Dialog message in proposal editor when user couldn't sign the proposal." + }, + "proposalEditorDocumentSignErrorTitle": "Unable to Sign the Proposal", + "@proposalEditorDocumentSignErrorTitle": { + "description": "Dialog title in proposal editor when user couldn't sign the proposal." + }, + "proposalEditorLimitReachedErrorMessage": "You have reached the maximum number of proposals allowed.", + "@proposalEditorLimitReachedErrorMessage": { + "description": "Dialog message in proposal editor when user has reached the max proposals limit." + }, + "proposalEditorLimitReachedErrorTitle": "Unable to Publish Proposal", + "@proposalEditorLimitReachedErrorTitle": { + "description": "Dialog title in proposal editor when user has reached the max proposals limit." + }, "proposalEditorNotAnswered": "Not Answered", "@proposalEditorNotAnswered": { "description": "Placeholder text when a property has been not filled." diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart index 098533ef6c57..b7f6c05258b0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/catalyst_voices_models.dart @@ -51,6 +51,7 @@ export 'document/enums/document_content_media_type.dart'; export 'document/enums/document_property_format.dart'; export 'document/enums/document_property_type.dart'; export 'document/exception/document_import_invalid_data_exception.dart'; +export 'document/exception/document_sign_exception.dart'; export 'document/schema/document_schema.dart'; export 'document/schema/property/document_property_schema.dart'; export 'document/specialized/comment_document.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/exception/document_sign_exception.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/exception/document_sign_exception.dart new file mode 100644 index 000000000000..695913337851 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/exception/document_sign_exception.dart @@ -0,0 +1,11 @@ +import 'package:equatable/equatable.dart'; + +/// An exception denoting a problem with signed document creation. +class DocumentSignException extends Equatable implements Exception { + final String message; + + const DocumentSignException(this.message); + + @override + List get props => [message]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/signed_document/signed_document_manager_impl.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/signed_document/signed_document_manager_impl.dart index 36c0b0ca2af0..23004a97bbaa 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/signed_document/signed_document_manager_impl.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/signed_document/signed_document_manager_impl.dart @@ -48,21 +48,25 @@ final class SignedDocumentManagerImpl implements SignedDocumentManager { required CatalystId catalystId, required CatalystPrivateKey privateKey, }) async { - final compressedPayload = await _brotliCompressPayload(document.toBytes()); - - final coseSign = await CoseSign.sign( - protectedHeaders: metadata.asCoseProtectedHeaders, - unprotectedHeaders: metadata.asCoseUnprotectedHeaders, - payload: compressedPayload, - signers: [_CatalystSigner(catalystId, privateKey)], - ); - - return _CoseSignedDocument( - coseSign: coseSign, - payload: document, - metadata: metadata, - signers: [catalystId], - ); + try { + final compressedPayload = await _brotliCompressPayload(document.toBytes()); + + final coseSign = await CoseSign.sign( + protectedHeaders: metadata.asCoseProtectedHeaders, + unprotectedHeaders: metadata.asCoseUnprotectedHeaders, + payload: compressedPayload, + signers: [_CatalystSigner(catalystId, privateKey)], + ); + + return _CoseSignedDocument( + coseSign: coseSign, + payload: document, + metadata: metadata, + signers: [catalystId], + ); + } on CoseSignException catch (error) { + throw DocumentSignException('Failed to create a signed document!\nSource: ${error.source}'); + } } Future _brotliCompressPayload(Uint8List payload) async { diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal_builder/exception/proposal_builder_exception.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal_builder/exception/proposal_builder_exception.dart index 8f278915de33..1347b1df2fed 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal_builder/exception/proposal_builder_exception.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal_builder/exception/proposal_builder_exception.dart @@ -2,6 +2,35 @@ import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:catalyst_voices_view_models/src/exception/localized_exception.dart'; import 'package:flutter/material.dart'; +/// An exception thrown when the app cannot sign the document on the user's behalf. +final class ProposalBuilderDocumentSignException extends LocalizedException { + const ProposalBuilderDocumentSignException(); + + @override + String message(BuildContext context) { + return context.l10n.proposalEditorDocumentSignErrorMessage; + } + + String title(BuildContext context) { + return context.l10n.proposalEditorDocumentSignErrorTitle; + } +} + +/// An exception thrown attempting to create / import a new +/// proposal when the user has reached the limit. +final class ProposalBuilderLimitReachedException extends LocalizedException { + const ProposalBuilderLimitReachedException(); + + @override + String message(BuildContext context) { + return context.l10n.proposalEditorLimitReachedErrorMessage; + } + + String title(BuildContext context) { + return context.l10n.proposalEditorLimitReachedErrorTitle; + } +} + /// Localized exception thrown when a proposal builder fails to publish a proposal. final class ProposalBuilderPublishException extends LocalizedException { const ProposalBuilderPublishException(); diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/catalyst_cose.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/catalyst_cose.dart index 5b124e4669eb..eb1772eb084e 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/catalyst_cose.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/catalyst_cose.dart @@ -1,6 +1,7 @@ export 'src/cose_constants.dart'; export 'src/cose_sign.dart'; export 'src/cose_sign1.dart'; +export 'src/exception/cose_exception.dart'; export 'src/types/cose_headers.dart'; export 'src/types/string_or_int.dart'; export 'src/types/uuid.dart'; diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart index 108e7b660c04..a5e7e9d79da3 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:catalyst_cose/src/cose_constants.dart'; +import 'package:catalyst_cose/src/exception/cose_exception.dart'; import 'package:catalyst_cose/src/types/cose_headers.dart'; import 'package:cbor/cbor.dart'; import 'package:collection/collection.dart'; @@ -133,40 +134,47 @@ final class CoseSign extends Equatable { required Uint8List payload, required List signers, }) async { - // TODO(dt-iohk): remove when server stops - // requiring alg header in body protected headers. - protectedHeaders = protectedHeaders.copyWith( - alg: () => signers.firstOrNull?.alg, - ); - - final signatures = []; - for (final signer in signers) { - final signatureProtectedHeaders = CoseHeaders.protected( - alg: signer.alg, - kid: await signer.kid, + try { + // TODO(dt-iohk): remove when server stops + // requiring alg header in body protected headers. + protectedHeaders = protectedHeaders.copyWith( + alg: () => signers.firstOrNull?.alg, ); - final toBeSigned = _createCoseSignSigStructureBytes( - bodyProtectedHeaders: protectedHeaders, - signatureProtectedHeaders: signatureProtectedHeaders, + final signatures = []; + for (final signer in signers) { + final signatureProtectedHeaders = CoseHeaders.protected( + alg: signer.alg, + kid: await signer.kid, + ); + + final toBeSigned = _createCoseSignSigStructureBytes( + bodyProtectedHeaders: protectedHeaders, + signatureProtectedHeaders: signatureProtectedHeaders, + payload: payload, + ); + + final signature = CoseSignature( + protectedHeaders: signatureProtectedHeaders, + unprotectedHeaders: const CoseHeaders.unprotected(), + signature: await signer.sign(toBeSigned), + ); + + signatures.add(signature); + } + + return CoseSign( + protectedHeaders: protectedHeaders, + unprotectedHeaders: unprotectedHeaders, payload: payload, + signatures: signatures, ); - - final signature = CoseSignature( - protectedHeaders: signatureProtectedHeaders, - unprotectedHeaders: const CoseHeaders.unprotected(), - signature: await signer.sign(toBeSigned), + } catch (error) { + throw CoseSignException( + message: 'Failed to create a CoseSign instance', + source: error, ); - - signatures.add(signature); } - - return CoseSign( - protectedHeaders: protectedHeaders, - unprotectedHeaders: unprotectedHeaders, - payload: payload, - signatures: signatures, - ); } static CborList _createCoseSignSigStructure({ diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart index 29de1d158198..2484e650b613 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:catalyst_cose/src/cose_constants.dart'; +import 'package:catalyst_cose/src/exception/cose_exception.dart'; import 'package:catalyst_cose/src/types/cose_headers.dart'; import 'package:cbor/cbor.dart'; import 'package:equatable/equatable.dart'; @@ -114,32 +115,39 @@ final class CoseSign1 extends Equatable { required Uint8List payload, required CatalystCoseSigner signer, }) async { - final kid = await signer.kid; + try { + final kid = await signer.kid; - protectedHeaders = protectedHeaders.copyWith( - alg: () => signer.alg, - kid: () => kid, - ); + protectedHeaders = protectedHeaders.copyWith( + alg: () => signer.alg, + kid: () => kid, + ); - final sigStructure = _createCoseSign1SigStructure( - protectedHeader: protectedHeaders.toCbor(), - payload: CborBytes(payload), - ); + final sigStructure = _createCoseSign1SigStructure( + protectedHeader: protectedHeaders.toCbor(), + payload: CborBytes(payload), + ); - final toBeSigned = Uint8List.fromList( - cbor.encode( - CborBytes( - cbor.encode(sigStructure), + final toBeSigned = Uint8List.fromList( + cbor.encode( + CborBytes( + cbor.encode(sigStructure), + ), ), - ), - ); + ); - return CoseSign1( - protectedHeaders: protectedHeaders, - unprotectedHeaders: unprotectedHeaders, - payload: payload, - signature: await signer.sign(toBeSigned), - ); + return CoseSign1( + protectedHeaders: protectedHeaders, + unprotectedHeaders: unprotectedHeaders, + payload: payload, + signature: await signer.sign(toBeSigned), + ); + } catch (error) { + throw CoseSignException( + message: 'Failed to create a CoseSign1 instance', + source: error, + ); + } } static CborValue _createCoseSign1SigStructure({ diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/exception/cose_exception.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/exception/cose_exception.dart new file mode 100644 index 000000000000..91c76727b277 --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/exception/cose_exception.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; + +/// A generalized COSE sign exception. +final class CoseSignException extends Equatable implements Exception { + /// A message describing the exception. + final String message; + + /// The source of the exception, if any. + final Object? source; + + /// The default constructor for the [CoseSignException]. + const CoseSignException({ + required this.message, + this.source, + }); + + @override + List get props => [message, source]; +}