From 4343c76ffc65785885fa7c540767c076aadaba8c Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Wed, 4 Dec 2024 13:34:36 +0700 Subject: [PATCH 01/25] docs(cat-gateway): Proposal schema templates WIP --- .../proposal/proposal.F14.example.json | 105 +++++ .../proposal/proposal.F14.schema.json | 402 ++++++++++++++++++ .../proposal/proposalTemplate.F14.schema.json | 401 +++++++++++++++++ 3 files changed, 908 insertions(+) create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.example.json create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.schema.json create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.schema.json diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.example.json new file mode 100644 index 00000000000..27a616b7cc3 --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.example.json @@ -0,0 +1,105 @@ +{ + "$schema": "./proposalTemplate.F14.schema.json", + "template_version": { + "description": "The version of the schema, formatted as a ULID.", + "title": "Version", + "uid": "version_section", + "value": "01F8MECHZXK8F8D8F8D8F8D8F9" + }, + "category_segment": { + "description": "Details about the categories related to the proposal.", + "title": "Category Segment", + "uid": "category_segment_section", + "value": { + "additional_questions": { + "description": "Details about additional questions regarding the project.", + "title": "Additional Questions", + "uid": "additional_questions_section", + "value": { + "is_open_source": true, + "license_type": "anything i please", + "product_repository_url": "http://somwhere.test/a/project/url", + "project_documentation_url": "http://somwhere.test/a/project/url/for/documentation" + } + }, + "agreements": { + "description": "Terms and agreements for the proposal.", + "title": "Terms and Agreements", + "uid": "agreements_section", + "value": { + "agree_fund_rules": false, + "catalyst_terms_and_conditions": false, + "privacy_policy": true + } + }, + "milestones": { + "description": "Milestones to track project progress.", + "title": "Project Milestones", + "uid": "milestones_section", + "value": [ + { + "deliverables": [ + "something", + "something else" + ], + "description": "a multi lin\\nstring", + "title": "a single line string" + } + ] + }, + "resources": { + "description": "Information about resources and budget allocation.", + "title": "Resources and Budget", + "uid": "resources_section", + "value": { + "budget_breakdown": "multi\\nline\\nstring", + "project_team": [ + { + "experience": "multi\\nline\\nstring", + "role": "manager" + } + ] + } + } + } + }, + "category_template": { + "description": "The category to which this template belongs.", + "title": "Category", + "uid": "category_section", + "value": "can_be_any_value_without_whitespace" + }, + "proposal_setup_segment": { + "description": "Basic information about your proposal.", + "title": "Proposal Setup", + "uid": "proposal_setup_section", + "value": { + "proposal_title": { + "description": "The name of the proposal.", + "title": "Proposal Title", + "uid": "proposal_title_field_section", + "value": "a proposal title single line" + } + } + }, + "proposal_summary_segment": { + "description": "Detailed information about the proposal.", + "title": "Proposal Details", + "uid": "proposal_details_section", + "value": { + "proposal_problem": "a\\nmultiline\\nproblem", + "proposal_solution": "a\\nmultiline\\nsolution" + } + }, + "public_description_segment": { + "description": "Detailed public description information.", + "title": "Public Description", + "uid": "public_description_section", + "value": { + "topic_1": "topic 1", + "topic_2": "topic 2", + "topic_3": "topic 3", + "topic_4": "topic 4" + } + } +} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.schema.json new file mode 100644 index 00000000000..8c08aeea95f --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.schema.json @@ -0,0 +1,402 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Catalyst Fund 14 Base Proposal Template", + "description": "A structured template for creating Fund 14 proposals", + "type": "object", + "properties": { + "$schema": "./.schema.json", + "template_version": { + "type": "object", + "properties": { + "uid": { + "const": "version_section", + "description": "Unique identifier for the version." + }, + "title": { + "const": "Version", + "description": "Title of the version." + }, + "description": { + "const": "The version of the schema, formatted as a ULID.", + "description": "Description for the version." + }, + "value": { + "type": "string", + "description": "A unique identifier for the version, formatted as a ULID.", + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", + "default": "01F8MECHZXK8F8D8F8D8F8D8F8" + } + }, + "required": ["uid", "title", "description", "value"] + }, + "category_template": { + "type": "object", + "properties": { + "uid": { + "const": "category_section", + "description": "Unique identifier for the category." + }, + "title": { + "const": "Category", + "description": "Title of the category." + }, + "description": { + "const": "The category to which this template belongs.", + "description": "Description for the category." + }, + "value": { + "type": "string", + "description": "A unique identifier for the category.", + "pattern": "^[a-zA-Z0-9_-]+$", + "default": "cardano_open_category" + } + }, + "required": ["uid", "title", "description", "value"] + }, + "proposal_setup_segment": { + "type": "object", + "properties": { + "uid": { + "const": "proposal_setup_section", + "description": "Unique identifier for the Proposal Setup Segment." + }, + "title": { + "const": "Proposal Setup", + "description": "Title of the Proposal Setup Segment." + }, + "description": { + "const": "Basic information about your proposal.", + "description": "Description for the Proposal Setup Segment." + }, + "value": { + "type": "object", + "properties": { + "proposal_title": { + "type": "object", + "properties": { + "uid": { + "const": "proposal_title_field_section", + "description": "Unique identifier for the Proposal Title field." + }, + "title": { + "const": "Proposal Title", + "description": "Title of the Proposal Title field." + }, + "description": { + "const": "The name of the proposal.", + "description": "Description for the Proposal Title field." + }, + "value": { + "type": "string", + "maxLength": 60, + "description": "User-provided proposal title.", + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + } + }, + "required": ["uid", "title", "description", "value"] + } + } + } + }, + "required": ["uid", "title", "description", "value"] + }, + "proposal_summary_segment": { + "type": "object", + "properties": { + "uid": { + "const": "proposal_details_section", + "description": "Unique identifier for the Proposal Details." + }, + "title": { + "const": "Proposal Details", + "description": "Details about the proposal." + }, + "description": { + "const": "Detailed information about the proposal.", + "description": "Description for the Proposal Details." + }, + "value": { + "type": "object", + "properties": { + "proposal_problem": { + "type": "string", + "description": "Describe the problem you're addressing in the Cardano ecosystem.", + "maxLength": 1000, + "format": "string", + "pattern": "^[\\s\\S]*$" + }, + "proposal_solution": { + "type": "string", + "description": "Describe your solution and how it addresses the problem.", + "maxLength": 2000, + "format": "string", + "pattern": "^[\\s\\S]*$" + } + }, + "required": ["proposal_problem", "proposal_solution"] + } + }, + "required": ["uid", "title", "description", "value"] + }, + "public_description_segment": { + "type": "object", + "properties": { + "uid": { + "const": "public_description_section", + "description": "Unique identifier for the Public Description Segment." + }, + "title": { + "const": "Public Description", + "description": "Public description of the proposal." + }, + "description": { + "const": "Detailed public description information.", + "description": "Description for the Public Description Segment." + }, + "value": { + "type": "object", + "properties": { + "topic_1": { + "type": "string", + "description": "First topic of the public description.", + "maxLength": 500, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "topic_2": { + "type": "string", + "description": "Second topic of the public description.", + "maxLength": 500, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "topic_3": { + "type": "string", + "description": "Third topic of the public description.", + "maxLength": 500, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "topic_4": { + "type": "string", + "description": "Fourth topic of the public description.", + "maxLength": 500, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + } + }, + "required": ["topic_1", "topic_2", "topic_3", "topic_4"] + } + }, + "required": ["uid", "title", "description", "value"] + }, + "category_segment": { + "type": "object", + "properties": { + "uid": { + "const": "category_segment_section", + "description": "Unique identifier for the Category Segment." + }, + "title": { + "const": "Category Segment", + "description": "Segment for categorizing the proposal." + }, + "description": { + "const": "Details about the categories related to the proposal.", + "description": "Description for the Category Segment." + }, + "value": { + "type": "object", + "properties": { + "additional_questions": { + "type": "object", + "properties": { + "uid": { + "const": "additional_questions_section", + "description": "Unique identifier for Additional Questions." + }, + "title": { + "const": "Additional Questions", + "description": "Questions related to the project." + }, + "description": { + "const": "Details about additional questions regarding the project.", + "description": "Description for Additional Questions." + }, + "value": { + "type": "object", + "properties": { + "is_open_source": { + "type": "boolean", + "description": "Is the project open source?" + }, + "license_type": { + "type": "string", + "description": "Type of license used.", + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "product_repository_url": { + "type": "string", + "format": "uri", + "description": "URL to the product repository." + }, + "project_documentation_url": { + "type": "string", + "format": "uri", + "description": "URL to project documentation." + } + }, + "required": ["is_open_source", "license_type", "product_repository_url", "project_documentation_url"] + } + }, + "required": ["uid", "title", "description", "value"] + }, + "milestones": { + "type": "object", + "properties": { + "uid": { + "const": "milestones_section", + "description": "Unique identifier for the Project Milestones." + }, + "title": { + "const": "Project Milestones", + "description": "Milestones for the project." + }, + "description": { + "const": "Milestones to track project progress.", + "description": "Description for the Project Milestones." + }, + "value": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Milestone Title.", + "maxLength": 100, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "description": { + "type": "string", + "description": "Milestone Description.", + "maxLength": 500, + "format": "string", + "pattern": "^[\\s\\S]*$" + }, + "deliverables": { + "type": "array", + "items": { + "type": "string", + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + } + } + }, + "required": ["title", "description", "deliverables"] + } + } + }, + "required": ["uid", "title", "description", "value"] + }, + "resources": { + "type": "object", + "properties": { + "uid": { + "const": "resources_section", + "description": "Unique identifier for Resources and Budget." + }, + "title": { + "const": "Resources and Budget", + "description": "Details about resources and budget." + }, + "description": { + "const": "Information about resources and budget allocation.", + "description": "Description for Resources and Budget." + }, + "value": { + "type": "object", + "properties": { + "project_team": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "Role of the team member.", + "maxLength": 100, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "experience": { + "type": "string", + "description": "Relevant experience of the team member.", + "maxLength": 500, + "format": "string", + "pattern": "^[\\s\\S]*$" + } + }, + "required": ["role", "experience"] + } + }, + "budget_breakdown": { + "type": "string", + "description": "Detailed breakdown of how the funds will be used.", + "maxLength": 2000, + "format": "string", + "pattern": "^[\\s\\S]*$" + } + }, + "required": ["project_team", "budget_breakdown"] + } + }, + "required": ["uid", "title", "description", "value"] + }, + "agreements": { + "type": "object", + "properties": { + "uid": { + "const": "agreements_section", + "description": "Unique identifier for Terms and Agreements." + }, + "title": { + "const": "Terms and Agreements", + "description": "Agreements related to the proposal." + }, + "description": { + "const": "Terms and agreements for the proposal.", + "description": "Description for Terms and Agreements." + }, + "value": { + "type": "object", + "properties": { + "agree_fund_rules": { + "type": "boolean", + "description": "Agreement to fund rules." + }, + "catalyst_terms_and_conditions": { + "type": "boolean", + "description": "Agreement to terms and conditions." + }, + "privacy_policy": { + "type": "boolean", + "description": "Agreement to privacy policy." + } + }, + "required": ["agree_fund_rules", "catalyst_terms_and_conditions", "privacy_policy"] + } + }, + "required": ["uid", "title", "description", "value"] + } + }, + "required": ["additional_questions", "milestones", "resources", "agreements"] + } + }, + "required": ["uid", "title", "description", "value"] + } + } +} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.schema.json new file mode 100644 index 00000000000..8583da87505 --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.schema.json @@ -0,0 +1,401 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Catalyst Fund 14 Base Proposal Template", + "description": "A structured template for creating Fund 14 proposals", + "type": "object", + "properties": { + "template_version": { + "type": "object", + "properties": { + "uid": { + "const": "version_section", + "description": "Unique identifier for the version." + }, + "title": { + "const": "Version", + "description": "Title of the version." + }, + "description": { + "const": "The version of the schema, formatted as a ULID.", + "description": "Description for the version." + }, + "value": { + "type": "string", + "description": "A unique identifier for the version, formatted as a ULID.", + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", + "default": "01F8MECHZXK8F8D8F8D8F8D8F8" + } + }, + "required": ["uid", "title", "description", "value"] + }, + "category_template": { + "type": "object", + "properties": { + "uid": { + "const": "category_section", + "description": "Unique identifier for the category." + }, + "title": { + "const": "Category", + "description": "Title of the category." + }, + "description": { + "const": "The category to which this template belongs.", + "description": "Description for the category." + }, + "value": { + "type": "string", + "description": "A unique identifier for the category.", + "pattern": "^[a-zA-Z0-9_-]+$", + "default": "cardano_open_category" + } + }, + "required": ["uid", "title", "description", "value"] + }, + "proposal_setup_segment": { + "type": "object", + "properties": { + "uid": { + "const": "proposal_setup_section", + "description": "Unique identifier for the Proposal Setup Segment." + }, + "title": { + "const": "Proposal Setup", + "description": "Title of the Proposal Setup Segment." + }, + "description": { + "const": "Basic information about your proposal.", + "description": "Description for the Proposal Setup Segment." + }, + "value": { + "type": "object", + "properties": { + "proposal_title": { + "type": "object", + "properties": { + "uid": { + "const": "proposal_title_field_section", + "description": "Unique identifier for the Proposal Title field." + }, + "title": { + "const": "Proposal Title", + "description": "Title of the Proposal Title field." + }, + "description": { + "const": "The name of the proposal.", + "description": "Description for the Proposal Title field." + }, + "value": { + "type": "string", + "maxLength": 60, + "description": "User-provided proposal title.", + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + } + }, + "required": ["uid", "title", "description", "value"] + } + } + } + }, + "required": ["uid", "title", "description", "value"] + }, + "proposal_summary_segment": { + "type": "object", + "properties": { + "uid": { + "const": "proposal_details_section", + "description": "Unique identifier for the Proposal Details." + }, + "title": { + "const": "Proposal Details", + "description": "Details about the proposal." + }, + "description": { + "const": "Detailed information about the proposal.", + "description": "Description for the Proposal Details." + }, + "value": { + "type": "object", + "properties": { + "proposal_problem": { + "type": "string", + "description": "Describe the problem you're addressing in the Cardano ecosystem.", + "maxLength": 1000, + "format": "string", + "pattern": "^[\\s\\S]*$" + }, + "proposal_solution": { + "type": "string", + "description": "Describe your solution and how it addresses the problem.", + "maxLength": 2000, + "format": "string", + "pattern": "^[\\s\\S]*$" + } + }, + "required": ["proposal_problem", "proposal_solution"] + } + }, + "required": ["uid", "title", "description", "value"] + }, + "public_description_segment": { + "type": "object", + "properties": { + "uid": { + "const": "public_description_section", + "description": "Unique identifier for the Public Description Segment." + }, + "title": { + "const": "Public Description", + "description": "Public description of the proposal." + }, + "description": { + "const": "Detailed public description information.", + "description": "Description for the Public Description Segment." + }, + "value": { + "type": "object", + "properties": { + "topic_1": { + "type": "string", + "description": "First topic of the public description.", + "maxLength": 500, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "topic_2": { + "type": "string", + "description": "Second topic of the public description.", + "maxLength": 500, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "topic_3": { + "type": "string", + "description": "Third topic of the public description.", + "maxLength": 500, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "topic_4": { + "type": "string", + "description": "Fourth topic of the public description.", + "maxLength": 500, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + } + }, + "required": ["topic_1", "topic_2", "topic_3", "topic_4"] + } + }, + "required": ["uid", "title", "description", "value"] + }, + "category_segment": { + "type": "object", + "properties": { + "uid": { + "const": "category_segment_section", + "description": "Unique identifier for the Category Segment." + }, + "title": { + "const": "Category Segment", + "description": "Segment for categorizing the proposal." + }, + "description": { + "const": "Details about the categories related to the proposal.", + "description": "Description for the Category Segment." + }, + "value": { + "type": "object", + "properties": { + "additional_questions": { + "type": "object", + "properties": { + "uid": { + "const": "additional_questions_section", + "description": "Unique identifier for Additional Questions." + }, + "title": { + "const": "Additional Questions", + "description": "Questions related to the project." + }, + "description": { + "const": "Details about additional questions regarding the project.", + "description": "Description for Additional Questions." + }, + "value": { + "type": "object", + "properties": { + "is_open_source": { + "type": "boolean", + "description": "Is the project open source?" + }, + "license_type": { + "type": "string", + "description": "Type of license used.", + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "product_repository_url": { + "type": "string", + "format": "uri", + "description": "URL to the product repository." + }, + "project_documentation_url": { + "type": "string", + "format": "uri", + "description": "URL to project documentation." + } + }, + "required": ["is_open_source", "license_type", "product_repository_url", "project_documentation_url"] + } + }, + "required": ["uid", "title", "description", "value"] + }, + "milestones": { + "type": "object", + "properties": { + "uid": { + "const": "milestones_section", + "description": "Unique identifier for the Project Milestones." + }, + "title": { + "const": "Project Milestones", + "description": "Milestones for the project." + }, + "description": { + "const": "Milestones to track project progress.", + "description": "Description for the Project Milestones." + }, + "value": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Milestone Title.", + "maxLength": 100, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "description": { + "type": "string", + "description": "Milestone Description.", + "maxLength": 500, + "format": "string", + "pattern": "^[\\s\\S]*$" + }, + "deliverables": { + "type": "array", + "items": { + "type": "string", + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + } + } + }, + "required": ["title", "description", "deliverables"] + } + } + }, + "required": ["uid", "title", "description", "value"] + }, + "resources": { + "type": "object", + "properties": { + "uid": { + "const": "resources_section", + "description": "Unique identifier for Resources and Budget." + }, + "title": { + "const": "Resources and Budget", + "description": "Details about resources and budget." + }, + "description": { + "const": "Information about resources and budget allocation.", + "description": "Description for Resources and Budget." + }, + "value": { + "type": "object", + "properties": { + "project_team": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "Role of the team member.", + "maxLength": 100, + "format": "string", + "pattern": "^[a-zA-Z0-9 ]+$" + }, + "experience": { + "type": "string", + "description": "Relevant experience of the team member.", + "maxLength": 500, + "format": "string", + "pattern": "^[\\s\\S]*$" + } + }, + "required": ["role", "experience"] + } + }, + "budget_breakdown": { + "type": "string", + "description": "Detailed breakdown of how the funds will be used.", + "maxLength": 2000, + "format": "string", + "pattern": "^[\\s\\S]*$" + } + }, + "required": ["project_team", "budget_breakdown"] + } + }, + "required": ["uid", "title", "description", "value"] + }, + "agreements": { + "type": "object", + "properties": { + "uid": { + "const": "agreements_section", + "description": "Unique identifier for Terms and Agreements." + }, + "title": { + "const": "Terms and Agreements", + "description": "Agreements related to the proposal." + }, + "description": { + "const": "Terms and agreements for the proposal.", + "description": "Description for Terms and Agreements." + }, + "value": { + "type": "object", + "properties": { + "agree_fund_rules": { + "type": "boolean", + "description": "Agreement to fund rules." + }, + "catalyst_terms_and_conditions": { + "type": "boolean", + "description": "Agreement to terms and conditions." + }, + "privacy_policy": { + "type": "boolean", + "description": "Agreement to privacy policy." + } + }, + "required": ["agree_fund_rules", "catalyst_terms_and_conditions", "privacy_policy"] + } + }, + "required": ["uid", "title", "description", "value"] + } + }, + "required": ["additional_questions", "milestones", "resources", "agreements"] + } + }, + "required": ["uid", "title", "description", "value"] + } + } +} \ No newline at end of file From 0b447045aa4814f4780439df338bec173d7b33ec Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Wed, 4 Dec 2024 23:19:05 +0700 Subject: [PATCH 02/25] feat(cat-gateway): wip f14 templates --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 255 +++++++++++ .../F14-Generic/proposal.F14.example.json | 19 + .../proposal/proposal.F14.schema.json | 402 ------------------ ...json => proposalTemplate.F14.example.json} | 0 4 files changed, 274 insertions(+), 402 deletions(-) create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json delete mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.schema.json rename docs/src/architecture/08_concepts/document_templates/proposal/{proposal.F14.example.json => proposalTemplate.F14.example.json} (100%) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json new file mode 100644 index 00000000000..07bcf365c27 --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -0,0 +1,255 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Catalyst Fund 14 Base Proposal Template", + "description": "A structured template for creating Fund 14 proposals", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "format": "path", + "const": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", + "readOnly": true + }, + "general": { + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Proposal title", + "description": "

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.


The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "maxLength": 80, + "minLength": 0 + }, + { + "email": { + "type": "string", + "title": "Email", + "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", + "contentMediaType": "text/plain", + "format": "email", + "pattern": "^.*$", + "maxLength": 80, + "minLength": 0 + }, + + }, + "applicant": { + "type": "string", + "title": "Name and surname of main applicant", + "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "maxLength": 80, + "minLength": 0 + }, + "applicant_type": { + "type": "string", + "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", + "description": "

Please select from one of the following:

", + "contentMediaType": "text/plain", + "enum": [ + "Individual", + "Entity (Incorporated)", + "Entity (Not Incorporated)" + ], + "default": "Individual" + }, + "co-proposers": { + "type": "string", + "title": "Co-proposers and additional applicants", + "description": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals / accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers.


IMPORTANT - A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund13 rules for added detail.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 1024, + "minLength": 0 + }, + "requested_funds": { + "type": "integer", + "title": "Requested funds in ada", + "description": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:


Minimum Funding Amount per proposal:


Maximum Funding Amount per proposal:

", + "minimum": 1, + "maximum": 18446744073709551615, + "format": "cardano:ada" + }, + "duration": { + "type": "integer", + "title": "Please specify how many months you expect your project to last (from 2-12 months)", + "description": "

Minimum 2 months - Maximum 12 months.


The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months.


If you believe your project will take longer than 12 months, consider reducing the project’s scope so that it becomes achievable within 12 months.


If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", + "minimum": 2, + "maximum": 12, + "format": "datetime:months" + }, + "translated": { + "type": "boolean", + "title": "Please indicate if your proposal has been auto-translated into English from another language", + "description": "

YES/NO - Tick YES so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections.


You can either link a document with your proposal in its original language OR provide your response in your native language after the English language in each question if you wish.


Tick NO if your proposal has not been auto-translated into English from another language.

", + "format": "yes/no" + }, + "problem": { + "type": "string", + "title": "What is the problem you want to solve? (200-character limit including spaces)", + "description": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 200, + "minLength": 1 + }, + "solution": { + "type": "string", + "title": "Summarize your solution to the problem (200-character limit including spaces)", + "description": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a...'


Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how'.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 200, + "minLength": 1 + }, + "links": { + "type": "array", + "title": "Website / GitHub repository, White paper, Marketing or any other relevant link", + "description": "

Here, provide links to yours or your partner organization’s website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal.


Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere.


If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include ‘https’. Without these steps, the form will not be submittable and show errors.

", + "items": { + "type": "string", + "format": "uri", + "contentMediaType": "text/plain", + "maxLength": 1024 + }, + "uniqueItems": true, + "default": [], + "minItems": 0, + "maxItems": 3 + }, + "dependencies": { + "type": "string", + "title": "If you have any dependencies then, please describe what the dependency is and why you believe it is essential for your project’s delivery. If NO, please write “No dependencies.”", + "description": "

Here you should list any dependencies and prerequisites for your project’s success. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 1024, + "minLength": 0 + }, + "open_source": { + "type": "boolean", + "title": "Will your project’s output/s be fully open source?", + "description": "

Open source refers to something people can modify and share because its design is publicly accessible. 


Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software.

", + "format": "yes/no" + }, + "license_info": { + "type": "string", + "title": "[GENERAL] Please provide here more information on the open source status of your project outputs", + "description": "

If you answered YES to the above question:


If declaring the project is open source in the application form, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository.


Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs. 


If only certain elements of your code will be open source please clarify which elements will be open source here. 


If you answered NO to the above question, please give further details as to why your projects outputs will not be open source.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 1024, + "minLength": 0 + } + }, + "required": [ + "title", + "applicant", + "applicant_type", + "requested_funds", + "duration", + "translated", + "problem", + "solution", + "open_source", + "license_info" + ] + }, + "metadata": { + "title": "Horizons", + "description": "

Please choose the most relevant category group and tag related to the outcomes of your proposal. Can select only one group and one tag.

", + "format": "nested-tag-selector", + "oneOf": [ + { + "type": "object", + "properties": { + "group": { + "type": "string", + "const": "Governance" + }, + "tag": { + "type": "string", + "enum": [ + "Governance", + "DAO" + ] + } + } + }, + { + "type": "object", + "properties": { + "group": { + "type": "string", + "const": "Education" + }, + "tag": { + "type": "string", + "enum": [ + "Education", + "Learn to Earn", + "Training", + "Translation" + ] + } + } + }, + { + "group": { + "type": "string", + "const": "Community & Outreach" + }, + "tag": { + "type": "string", + "enum": [ + "Connected Community", + "Community", + "Community Outreach", + "Social Media" + ] + } + }, + { + "group": { + "type": "string", + "const": "Development & Tools" + }, + "tag": { + "type": "string", + "enum": [ + "Developer Tools", + "L2", + "Infrastructure", + "Analytics", + "AI", + "Research", + "UTXO", + "P2P" + ] + } + } + ] + }, + "agreements": { + "type": "object", + "properties": { + "fund_rules": { + "type": "string", + "title": "Fund Rules:", + "description": "

By submitting a proposal to Project Catalyst Fund13, I confirm that I have read and agree to be bound by the Fund Rules.

", + "contentMediaType": "text/plain", + "enum": [ + "Yes", + "No" + ], + "default": "No", + "pattern": "Yes", + "format": "checkbox" + } + } + } + } +} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json new file mode 100644 index 00000000000..d77c676cb9e --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json @@ -0,0 +1,19 @@ +{ + "$schema": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", + "general": { + "title": "Single line plain text ..............................", + "links": [ + "http://notauri", + "ftp://some_old_site", + "cardano://some-block" + ], + "applicant_type": "Individual", + }, + "metadata": { + "group": "Education", + "tag": "Learn to Earn" + }, + "agreements": { + "fund_rules": "Yes" + } +} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.schema.json deleted file mode 100644 index 8c08aeea95f..00000000000 --- a/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.schema.json +++ /dev/null @@ -1,402 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Catalyst Fund 14 Base Proposal Template", - "description": "A structured template for creating Fund 14 proposals", - "type": "object", - "properties": { - "$schema": "./.schema.json", - "template_version": { - "type": "object", - "properties": { - "uid": { - "const": "version_section", - "description": "Unique identifier for the version." - }, - "title": { - "const": "Version", - "description": "Title of the version." - }, - "description": { - "const": "The version of the schema, formatted as a ULID.", - "description": "Description for the version." - }, - "value": { - "type": "string", - "description": "A unique identifier for the version, formatted as a ULID.", - "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", - "default": "01F8MECHZXK8F8D8F8D8F8D8F8" - } - }, - "required": ["uid", "title", "description", "value"] - }, - "category_template": { - "type": "object", - "properties": { - "uid": { - "const": "category_section", - "description": "Unique identifier for the category." - }, - "title": { - "const": "Category", - "description": "Title of the category." - }, - "description": { - "const": "The category to which this template belongs.", - "description": "Description for the category." - }, - "value": { - "type": "string", - "description": "A unique identifier for the category.", - "pattern": "^[a-zA-Z0-9_-]+$", - "default": "cardano_open_category" - } - }, - "required": ["uid", "title", "description", "value"] - }, - "proposal_setup_segment": { - "type": "object", - "properties": { - "uid": { - "const": "proposal_setup_section", - "description": "Unique identifier for the Proposal Setup Segment." - }, - "title": { - "const": "Proposal Setup", - "description": "Title of the Proposal Setup Segment." - }, - "description": { - "const": "Basic information about your proposal.", - "description": "Description for the Proposal Setup Segment." - }, - "value": { - "type": "object", - "properties": { - "proposal_title": { - "type": "object", - "properties": { - "uid": { - "const": "proposal_title_field_section", - "description": "Unique identifier for the Proposal Title field." - }, - "title": { - "const": "Proposal Title", - "description": "Title of the Proposal Title field." - }, - "description": { - "const": "The name of the proposal.", - "description": "Description for the Proposal Title field." - }, - "value": { - "type": "string", - "maxLength": 60, - "description": "User-provided proposal title.", - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - } - }, - "required": ["uid", "title", "description", "value"] - } - } - } - }, - "required": ["uid", "title", "description", "value"] - }, - "proposal_summary_segment": { - "type": "object", - "properties": { - "uid": { - "const": "proposal_details_section", - "description": "Unique identifier for the Proposal Details." - }, - "title": { - "const": "Proposal Details", - "description": "Details about the proposal." - }, - "description": { - "const": "Detailed information about the proposal.", - "description": "Description for the Proposal Details." - }, - "value": { - "type": "object", - "properties": { - "proposal_problem": { - "type": "string", - "description": "Describe the problem you're addressing in the Cardano ecosystem.", - "maxLength": 1000, - "format": "string", - "pattern": "^[\\s\\S]*$" - }, - "proposal_solution": { - "type": "string", - "description": "Describe your solution and how it addresses the problem.", - "maxLength": 2000, - "format": "string", - "pattern": "^[\\s\\S]*$" - } - }, - "required": ["proposal_problem", "proposal_solution"] - } - }, - "required": ["uid", "title", "description", "value"] - }, - "public_description_segment": { - "type": "object", - "properties": { - "uid": { - "const": "public_description_section", - "description": "Unique identifier for the Public Description Segment." - }, - "title": { - "const": "Public Description", - "description": "Public description of the proposal." - }, - "description": { - "const": "Detailed public description information.", - "description": "Description for the Public Description Segment." - }, - "value": { - "type": "object", - "properties": { - "topic_1": { - "type": "string", - "description": "First topic of the public description.", - "maxLength": 500, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "topic_2": { - "type": "string", - "description": "Second topic of the public description.", - "maxLength": 500, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "topic_3": { - "type": "string", - "description": "Third topic of the public description.", - "maxLength": 500, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "topic_4": { - "type": "string", - "description": "Fourth topic of the public description.", - "maxLength": 500, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - } - }, - "required": ["topic_1", "topic_2", "topic_3", "topic_4"] - } - }, - "required": ["uid", "title", "description", "value"] - }, - "category_segment": { - "type": "object", - "properties": { - "uid": { - "const": "category_segment_section", - "description": "Unique identifier for the Category Segment." - }, - "title": { - "const": "Category Segment", - "description": "Segment for categorizing the proposal." - }, - "description": { - "const": "Details about the categories related to the proposal.", - "description": "Description for the Category Segment." - }, - "value": { - "type": "object", - "properties": { - "additional_questions": { - "type": "object", - "properties": { - "uid": { - "const": "additional_questions_section", - "description": "Unique identifier for Additional Questions." - }, - "title": { - "const": "Additional Questions", - "description": "Questions related to the project." - }, - "description": { - "const": "Details about additional questions regarding the project.", - "description": "Description for Additional Questions." - }, - "value": { - "type": "object", - "properties": { - "is_open_source": { - "type": "boolean", - "description": "Is the project open source?" - }, - "license_type": { - "type": "string", - "description": "Type of license used.", - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "product_repository_url": { - "type": "string", - "format": "uri", - "description": "URL to the product repository." - }, - "project_documentation_url": { - "type": "string", - "format": "uri", - "description": "URL to project documentation." - } - }, - "required": ["is_open_source", "license_type", "product_repository_url", "project_documentation_url"] - } - }, - "required": ["uid", "title", "description", "value"] - }, - "milestones": { - "type": "object", - "properties": { - "uid": { - "const": "milestones_section", - "description": "Unique identifier for the Project Milestones." - }, - "title": { - "const": "Project Milestones", - "description": "Milestones for the project." - }, - "description": { - "const": "Milestones to track project progress.", - "description": "Description for the Project Milestones." - }, - "value": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Milestone Title.", - "maxLength": 100, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "description": { - "type": "string", - "description": "Milestone Description.", - "maxLength": 500, - "format": "string", - "pattern": "^[\\s\\S]*$" - }, - "deliverables": { - "type": "array", - "items": { - "type": "string", - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - } - } - }, - "required": ["title", "description", "deliverables"] - } - } - }, - "required": ["uid", "title", "description", "value"] - }, - "resources": { - "type": "object", - "properties": { - "uid": { - "const": "resources_section", - "description": "Unique identifier for Resources and Budget." - }, - "title": { - "const": "Resources and Budget", - "description": "Details about resources and budget." - }, - "description": { - "const": "Information about resources and budget allocation.", - "description": "Description for Resources and Budget." - }, - "value": { - "type": "object", - "properties": { - "project_team": { - "type": "array", - "items": { - "type": "object", - "properties": { - "role": { - "type": "string", - "description": "Role of the team member.", - "maxLength": 100, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "experience": { - "type": "string", - "description": "Relevant experience of the team member.", - "maxLength": 500, - "format": "string", - "pattern": "^[\\s\\S]*$" - } - }, - "required": ["role", "experience"] - } - }, - "budget_breakdown": { - "type": "string", - "description": "Detailed breakdown of how the funds will be used.", - "maxLength": 2000, - "format": "string", - "pattern": "^[\\s\\S]*$" - } - }, - "required": ["project_team", "budget_breakdown"] - } - }, - "required": ["uid", "title", "description", "value"] - }, - "agreements": { - "type": "object", - "properties": { - "uid": { - "const": "agreements_section", - "description": "Unique identifier for Terms and Agreements." - }, - "title": { - "const": "Terms and Agreements", - "description": "Agreements related to the proposal." - }, - "description": { - "const": "Terms and agreements for the proposal.", - "description": "Description for Terms and Agreements." - }, - "value": { - "type": "object", - "properties": { - "agree_fund_rules": { - "type": "boolean", - "description": "Agreement to fund rules." - }, - "catalyst_terms_and_conditions": { - "type": "boolean", - "description": "Agreement to terms and conditions." - }, - "privacy_policy": { - "type": "boolean", - "description": "Agreement to privacy policy." - } - }, - "required": ["agree_fund_rules", "catalyst_terms_and_conditions", "privacy_policy"] - } - }, - "required": ["uid", "title", "description", "value"] - } - }, - "required": ["additional_questions", "milestones", "resources", "agreements"] - } - }, - "required": ["uid", "title", "description", "value"] - } - } -} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.example.json similarity index 100% rename from docs/src/architecture/08_concepts/document_templates/proposal/proposal.F14.example.json rename to docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.example.json From bb6d8a9cb5bb71c35335b4525cb7da4a88b4eaa8 Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Wed, 4 Dec 2024 21:59:47 +0300 Subject: [PATCH 03/25] feat(cat-gateway): initialization of f14 templates --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 508 +++--- .../proposal.F14.example.json | 0 .../proposalTemplate.F14.example.json | 0 .../proposalTemplate.F14.schema.json | 1514 +++++++++++++++++ .../proposalTemplate.F14.example.json | 105 -- .../proposal/proposalTemplate.F14.schema.json | 401 ----- 6 files changed, 1768 insertions(+), 760 deletions(-) rename docs/src/architecture/08_concepts/document_templates/proposal/{F14-Generic => F14-Generic-Steven}/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json (98%) rename docs/src/architecture/08_concepts/document_templates/proposal/{F14-Generic => F14-Generic-Steven}/proposal.F14.example.json (100%) create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.example.json create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json delete mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.example.json delete mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.schema.json diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json similarity index 98% rename from docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json rename to docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 07bcf365c27..033a6ba2be6 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -1,255 +1,255 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Catalyst Fund 14 Base Proposal Template", - "description": "A structured template for creating Fund 14 proposals", - "type": "object", - "properties": { - "$schema": { - "type": "string", - "format": "path", - "const": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", - "readOnly": true - }, - "general": { - "type": "object", - "properties": { - "title": { - "type": "string", - "title": "Proposal title", - "description": "

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.


The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

", - "contentMediaType": "text/plain", - "pattern": "^.*$", - "maxLength": 80, - "minLength": 0 - }, - { - "email": { - "type": "string", - "title": "Email", - "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", - "contentMediaType": "text/plain", - "format": "email", - "pattern": "^.*$", - "maxLength": 80, - "minLength": 0 - }, - - }, - "applicant": { - "type": "string", - "title": "Name and surname of main applicant", - "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", - "contentMediaType": "text/plain", - "pattern": "^.*$", - "maxLength": 80, - "minLength": 0 - }, - "applicant_type": { - "type": "string", - "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", - "description": "

Please select from one of the following:

", - "contentMediaType": "text/plain", - "enum": [ - "Individual", - "Entity (Incorporated)", - "Entity (Not Incorporated)" - ], - "default": "Individual" - }, - "co-proposers": { - "type": "string", - "title": "Co-proposers and additional applicants", - "description": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals / accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers.


IMPORTANT - A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund13 rules for added detail.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", - "maxLength": 1024, - "minLength": 0 - }, - "requested_funds": { - "type": "integer", - "title": "Requested funds in ada", - "description": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:


Minimum Funding Amount per proposal:

  • Cardano Open: ₳15,000
  • Cardano Uses Cases: ₳15,000
  • Cardano Partners: ₳500,000


Maximum Funding Amount per proposal:

  • Cardano Open: 
  • Developers (technical): ₳200,000
  • Ecosystem (non-technical): ₳100,000
  • Cardano Uses Cases:
  • Concept: ₳150,000
  • Product: ₳500,000
  • Cardano Partners:
  • Enterprise R&D: ₳2,000,000 
  • Growth & Acceleration: ₳2,000,000
", - "minimum": 1, - "maximum": 18446744073709551615, - "format": "cardano:ada" - }, - "duration": { - "type": "integer", - "title": "Please specify how many months you expect your project to last (from 2-12 months)", - "description": "

Minimum 2 months - Maximum 12 months.


The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months.


If you believe your project will take longer than 12 months, consider reducing the project’s scope so that it becomes achievable within 12 months.


If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", - "minimum": 2, - "maximum": 12, - "format": "datetime:months" - }, - "translated": { - "type": "boolean", - "title": "Please indicate if your proposal has been auto-translated into English from another language", - "description": "

YES/NO - Tick YES so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections.


You can either link a document with your proposal in its original language OR provide your response in your native language after the English language in each question if you wish.


Tick NO if your proposal has not been auto-translated into English from another language.

", - "format": "yes/no" - }, - "problem": { - "type": "string", - "title": "What is the problem you want to solve? (200-character limit including spaces)", - "description": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", - "maxLength": 200, - "minLength": 1 - }, - "solution": { - "type": "string", - "title": "Summarize your solution to the problem (200-character limit including spaces)", - "description": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a...'


Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how'.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", - "maxLength": 200, - "minLength": 1 - }, - "links": { - "type": "array", - "title": "Website / GitHub repository, White paper, Marketing or any other relevant link", - "description": "

Here, provide links to yours or your partner organization’s website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal.


Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere.


If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include ‘https’. Without these steps, the form will not be submittable and show errors.

", - "items": { - "type": "string", - "format": "uri", - "contentMediaType": "text/plain", - "maxLength": 1024 - }, - "uniqueItems": true, - "default": [], - "minItems": 0, - "maxItems": 3 - }, - "dependencies": { - "type": "string", - "title": "If you have any dependencies then, please describe what the dependency is and why you believe it is essential for your project’s delivery. If NO, please write “No dependencies.”", - "description": "

Here you should list any dependencies and prerequisites for your project’s success. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", - "maxLength": 1024, - "minLength": 0 - }, - "open_source": { - "type": "boolean", - "title": "Will your project’s output/s be fully open source?", - "description": "

Open source refers to something people can modify and share because its design is publicly accessible. 


Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software.

", - "format": "yes/no" - }, - "license_info": { - "type": "string", - "title": "[GENERAL] Please provide here more information on the open source status of your project outputs", - "description": "

If you answered YES to the above question:


If declaring the project is open source in the application form, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository.


Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs. 


If only certain elements of your code will be open source please clarify which elements will be open source here. 


If you answered NO to the above question, please give further details as to why your projects outputs will not be open source.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", - "maxLength": 1024, - "minLength": 0 - } - }, - "required": [ - "title", - "applicant", - "applicant_type", - "requested_funds", - "duration", - "translated", - "problem", - "solution", - "open_source", - "license_info" - ] - }, - "metadata": { - "title": "Horizons", - "description": "

Please choose the most relevant category group and tag related to the outcomes of your proposal. Can select only one group and one tag.

", - "format": "nested-tag-selector", - "oneOf": [ - { - "type": "object", - "properties": { - "group": { - "type": "string", - "const": "Governance" - }, - "tag": { - "type": "string", - "enum": [ - "Governance", - "DAO" - ] - } - } - }, - { - "type": "object", - "properties": { - "group": { - "type": "string", - "const": "Education" - }, - "tag": { - "type": "string", - "enum": [ - "Education", - "Learn to Earn", - "Training", - "Translation" - ] - } - } - }, - { - "group": { - "type": "string", - "const": "Community & Outreach" - }, - "tag": { - "type": "string", - "enum": [ - "Connected Community", - "Community", - "Community Outreach", - "Social Media" - ] - } - }, - { - "group": { - "type": "string", - "const": "Development & Tools" - }, - "tag": { - "type": "string", - "enum": [ - "Developer Tools", - "L2", - "Infrastructure", - "Analytics", - "AI", - "Research", - "UTXO", - "P2P" - ] - } - } - ] - }, - "agreements": { - "type": "object", - "properties": { - "fund_rules": { - "type": "string", - "title": "Fund Rules:", - "description": "

By submitting a proposal to Project Catalyst Fund13, I confirm that I have read and agree to be bound by the Fund Rules.

", - "contentMediaType": "text/plain", - "enum": [ - "Yes", - "No" - ], - "default": "No", - "pattern": "Yes", - "format": "checkbox" - } - } - } - } +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Catalyst Fund 14 Base Proposal Template", + "description": "A structured template for creating Fund 14 proposals", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "format": "path", + "const": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", + "readOnly": true + }, + "general": { + "type": "object", + "properties": { + "title": { + "type": "string", + "title": "Proposal title", + "description": "

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.


The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "maxLength": 80, + "minLength": 0 + }, + { + "email": { + "type": "string", + "title": "Email", + "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", + "contentMediaType": "text/plain", + "format": "email", + "pattern": "^.*$", + "maxLength": 80, + "minLength": 0 + }, + + }, + "applicant": { + "type": "string", + "title": "Name and surname of main applicant", + "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "maxLength": 80, + "minLength": 0 + }, + "applicant_type": { + "type": "string", + "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", + "description": "

Please select from one of the following:

", + "contentMediaType": "text/plain", + "enum": [ + "Individual", + "Entity (Incorporated)", + "Entity (Not Incorporated)" + ], + "default": "Individual" + }, + "co-proposers": { + "type": "string", + "title": "Co-proposers and additional applicants", + "description": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals / accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers.


IMPORTANT - A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund13 rules for added detail.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 1024, + "minLength": 0 + }, + "requested_funds": { + "type": "integer", + "title": "Requested funds in ada", + "description": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:


Minimum Funding Amount per proposal:

  • Cardano Open: ₳15,000
  • Cardano Uses Cases: ₳15,000
  • Cardano Partners: ₳500,000


Maximum Funding Amount per proposal:

  • Cardano Open: 
  • Developers (technical): ₳200,000
  • Ecosystem (non-technical): ₳100,000
  • Cardano Uses Cases:
  • Concept: ₳150,000
  • Product: ₳500,000
  • Cardano Partners:
  • Enterprise R&D: ₳2,000,000 
  • Growth & Acceleration: ₳2,000,000
", + "minimum": 1, + "maximum": 18446744073709551615, + "format": "cardano:ada" + }, + "duration": { + "type": "integer", + "title": "Please specify how many months you expect your project to last (from 2-12 months)", + "description": "

Minimum 2 months - Maximum 12 months.


The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months.


If you believe your project will take longer than 12 months, consider reducing the project’s scope so that it becomes achievable within 12 months.


If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", + "minimum": 2, + "maximum": 12, + "format": "datetime:months" + }, + "translated": { + "type": "boolean", + "title": "Please indicate if your proposal has been auto-translated into English from another language", + "description": "

YES/NO - Tick YES so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections.


You can either link a document with your proposal in its original language OR provide your response in your native language after the English language in each question if you wish.


Tick NO if your proposal has not been auto-translated into English from another language.

", + "format": "yes/no" + }, + "problem": { + "type": "string", + "title": "What is the problem you want to solve? (200-character limit including spaces)", + "description": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 200, + "minLength": 1 + }, + "solution": { + "type": "string", + "title": "Summarize your solution to the problem (200-character limit including spaces)", + "description": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a...'


Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how'.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 200, + "minLength": 1 + }, + "links": { + "type": "array", + "title": "Website / GitHub repository, White paper, Marketing or any other relevant link", + "description": "

Here, provide links to yours or your partner organization’s website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal.


Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere.


If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include ‘https’. Without these steps, the form will not be submittable and show errors.

", + "items": { + "type": "string", + "format": "uri", + "contentMediaType": "text/plain", + "maxLength": 1024 + }, + "uniqueItems": true, + "default": [], + "minItems": 0, + "maxItems": 3 + }, + "dependencies": { + "type": "string", + "title": "If you have any dependencies then, please describe what the dependency is and why you believe it is essential for your project’s delivery. If NO, please write “No dependencies.”", + "description": "

Here you should list any dependencies and prerequisites for your project’s success. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 1024, + "minLength": 0 + }, + "open_source": { + "type": "boolean", + "title": "Will your project’s output/s be fully open source?", + "description": "

Open source refers to something people can modify and share because its design is publicly accessible. 


Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software.

", + "format": "yes/no" + }, + "license_info": { + "type": "string", + "title": "[GENERAL] Please provide here more information on the open source status of your project outputs", + "description": "

If you answered YES to the above question:


If declaring the project is open source in the application form, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository.


Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs. 


If only certain elements of your code will be open source please clarify which elements will be open source here. 


If you answered NO to the above question, please give further details as to why your projects outputs will not be open source.

", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$", + "maxLength": 1024, + "minLength": 0 + } + }, + "required": [ + "title", + "applicant", + "applicant_type", + "requested_funds", + "duration", + "translated", + "problem", + "solution", + "open_source", + "license_info" + ] + }, + "metadata": { + "title": "Horizons", + "description": "

Please choose the most relevant category group and tag related to the outcomes of your proposal. Can select only one group and one tag.

", + "format": "nested-tag-selector", + "oneOf": [ + { + "type": "object", + "properties": { + "group": { + "type": "string", + "const": "Governance" + }, + "tag": { + "type": "string", + "enum": [ + "Governance", + "DAO" + ] + } + } + }, + { + "type": "object", + "properties": { + "group": { + "type": "string", + "const": "Education" + }, + "tag": { + "type": "string", + "enum": [ + "Education", + "Learn to Earn", + "Training", + "Translation" + ] + } + } + }, + { + "group": { + "type": "string", + "const": "Community & Outreach" + }, + "tag": { + "type": "string", + "enum": [ + "Connected Community", + "Community", + "Community Outreach", + "Social Media" + ] + } + }, + { + "group": { + "type": "string", + "const": "Development & Tools" + }, + "tag": { + "type": "string", + "enum": [ + "Developer Tools", + "L2", + "Infrastructure", + "Analytics", + "AI", + "Research", + "UTXO", + "P2P" + ] + } + } + ] + }, + "agreements": { + "type": "object", + "properties": { + "fund_rules": { + "type": "string", + "title": "Fund Rules:", + "description": "

By submitting a proposal to Project Catalyst Fund13, I confirm that I have read and agree to be bound by the Fund Rules.

", + "contentMediaType": "text/plain", + "enum": [ + "Yes", + "No" + ], + "default": "No", + "pattern": "Yes", + "format": "checkbox" + } + } + } + } } \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/proposal.F14.example.json similarity index 100% rename from docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json rename to docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/proposal.F14.example.json diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.example.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json new file mode 100644 index 00000000000..40176e7e832 --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json @@ -0,0 +1,1514 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cardano.org/schemas/catalyst/f14/proposal", + "version": "1.0.0", + "title": "F14 Submission Form", + "description": "Schema for the F14 Catalyst Proposal Submission Form", + "type": "object", + "properties": { + "$schema": { + "type": "string", + "format": "path", + "const": "./proposalTemplate.F14.schema.json", + "readOnly": true + }, + "setup": { + "type": "object", + "title": "proposal setup", + "description": "Proposal title", + "properties": { + "title": { + "type": "object", + "title": "proposal setup", + "description": "Proposal title", + "properties": { + "title": { + "type": "string", + "title": "Proposal Title", + "description": "A clear, unambiguous, and concise title for your proposal", + "minLength": 1, + "maxLength": 60, + "examples": ["DeFi Integration Platform for Cardano"] + } + }, + "additionalProperties": false, + "required": [ + "title" + ] + }, + "proposer": { + "type": "object", + "properties": { + "mainApplicant": { + "type": "string", + "title": "Name and surname of main applicant", + "description": "Name and surname of main applicant", + "$comment": "Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).", + "minLength": 2, + "maxLength": 100 + }, + "applicant_type": { + "type": "string", + "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", + "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", + "$comment": "Please select from one of the following: 1. Individual 2. Entity (Incorporated) 3. Entity (Not Incorporated)", + "enum": [ + "Individual", + "Entity (Incorporated)", + "Entity (Not Incorporated)" + ] + }, + "co-proposers": { + "type": "string", + "title": "Co-proposers and additional applicants", + "description": "Co-proposers and additional applicants", + "$comment": "List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 13 rules for added detail.", + "items": { + "type": "string" + }, + "maxItems": 5 + } + }, + "required": ["mainApplicant", "applicantType"] + } + }, + "required": ["title", "proposer"] + }, + "proposalSummary": { + "type": "object", + "title": "Proposal Summary", + "description": "Key information about your proposal", + "properties": { + "budget": { + "type": "object", + "title": "Budget Information", + "properties": { + "requestedFunds": { + "type": "number", + "title": "Requested funds in ADA", + "description": "The amount of funding requested for your proposal", + "minimum": 15000, + "maximum": 2000000, + "examples": ["There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category: Minimum Funding Amount per proposal: Cardano Open: A15,000 Cardano Uses Cases: A15,000 Cardano Partners: A500,000 Maximum Funding Amount per proposal: Cardano Open: Developers (technical): A200,000 • Ecosystem (non-technical): A100,000 Cardano Uses Cases: Concept A150,000 Product: A500,000 Cardano Partners: Enterprise R&D A2,000,000 Growth & Acceleration: A2,000,000"], + "format": "cardano:ada", + "errorMessage": { + "minimum": "Minimum funding amount is 15,000 ADA", + "maximum": "Maximum funding amount is 2,000,000 ADA" + } + } + } + }, + "Time": { + "type": "object", + "properties": { + "project_duration": { + "type": "integer", + "title": "Project Duration in Months", + "description": "Specify the expected duration of your project. Projects must be completable within 2-12 months.", + "minimum": 2, + "maximum": 12, + "examples": ["Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out."] + } + }, + "required": ["project_duration"] + }, + "Translation": { + "type": "object", + "title": "Translation Information", + "description": "Information about the proposal's language and translation status", + "properties": { + "isTranslated": { + "type": "boolean", + "title": "Auto-translated Status", + "description": "Indicate if your proposal has been auto-translated into English from another language", + "default": false + }, + "originalLanguage": { + "type": "string", + "title": "Original Language", + "description": "If auto-translated, specify the original language of your proposal", + "minLength": 2, + "maxLength": 50, + "examples": ["Spanish", "Japanese", "French"] + }, + "translationNotes": { + "type": "string", + "title": "Translation Notes", + "description": "Additional notes about the translation or original language content", + "maxLength": 500 + } + }, + "required": ["isTranslated"], + "dependencies": { + "originalLanguage": ["isTranslated"], + "translationNotes": ["isTranslated"] + }, + "if": { + "properties": { "isTranslated": { "const": true } } + }, + "then": { + "required": ["originalLanguage"] + } + }, + "Problem": { + "type": "object", + "title": "Problem Statement", + "description": "Define the problem your proposal aims to solve", + "properties": { + "statement": { + "type": "string", + "title": "Problem Description", + "description": "Clearly define the problem you aim to solve. This will be visible in the Catalyst voting app.", + "minLength": 10, + "maxLength": 200, + "examples": ["The Cardano ecosystem lacks standardized tools for cross-protocol communication, resulting in fragmented user experiences and inefficient resource utilization."] + }, + "impactArea": { + "type": "array", + "title": "Impact Areas", + "description": "Select the areas that will be most impacted by solving this problem", + "items": { + "type": "string", + "enum": [ + "Technical Infrastructure", + "User Experience", + "Developer Tooling", + "Community Growth", + "Economic Sustainability", + "Interoperability", + "Security", + "Scalability", + "Education", + "Adoption" + ] + }, + "minItems": 1, + "maxItems": 3, + "uniqueItems": true + } + }, + "required": ["statement", "impactArea"] + }, + "Solution": { + "type": "object", + "title": "Solution Overview", + "description": "Describe your proposed solution to the problem", + "properties": { + "summary": { + "type": "string", + "title": "Solution Summary", + "description": "Briefly describe your solution. Focus on what you will do or create to solve the problem.", + "minLength": 10, + "maxLength": 200, + "examples": ["Develop an open-source integration framework that standardizes protocol communication and provides a unified API layer for seamless DeFi interactions."] + }, + "approach": { + "type": "string", + "title": "Technical Approach", + "description": "Outline the technical approach or methodology you will use", + "maxLength": 500 + }, + "innovationAspects": { + "type": "array", + "title": "Innovation Aspects", + "description": "Key innovative aspects of your solution", + "items": { + "type": "string", + "maxLength": 100 + }, + "minItems": 1, + "maxItems": 5, + "uniqueItems": true + } + }, + "required": ["summary", "approach"] + }, + "SupportingLinks": { + "type": "object", + "title": "Supporting Documentation", + "description": "Additional resources and documentation for your proposal", + "properties": { + "links": { + "type": "array", + "title": "Resource Links", + "description": "Links to relevant documentation, code repositories, or marketing materials. All links must use HTTPS.", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://.*", + "title": "Resource URL", + "description": "URL must start with https://", + "examples": [ + "https://github.com/your-org/project", + "https://your-project-docs.com" + ] + }, + "type": { + "type": "string", + "enum": [ + "GitHub Repository", + "Documentation", + "Whitepaper", + "Website", + "Marketing Material", + "Technical Specification", + "Research Paper", + "Blog Post", + "Social Media", + "Other" + ], + "title": "Resource Type", + "description": "Type of resource being linked" + }, + "description": { + "type": "string", + "maxLength": 200, + "title": "Resource Description", + "description": "Brief description explaining what this resource contains and why it's relevant", + "examples": [ + "Project's main GitHub repository containing all source code", + "Technical whitepaper detailing the solution architecture" + ] + } + }, + "required": ["url", "type", "description"] + }, + "minItems": 0, + "maxItems": 10, + "uniqueItems": true + }, + "mainRepository": { + "type": "string", + "format": "uri", + "pattern": "^https://(github\\.com|gitlab\\.com|bitbucket\\.org)/.*", + "title": "Main Code Repository", + "description": "Primary repository where the project's code will be hosted" + }, + "documentation": { + "type": "string", + "format": "uri", + "pattern": "^https://.*", + "title": "Documentation URL", + "description": "Main documentation site or resource for the project" + } + } + }, + "Dependencies": { + "type": "object", + "title": "Project Dependencies", + "description": "External dependencies and requirements for project success", + "properties": { + "hasDependencies": { + "type": "boolean", + "title": "Has Dependencies", + "description": "Indicate if your project has any dependencies on other organizations or technologies", + "default": false + }, + "details": { + "type": "array", + "title": "Dependency Details", + "description": "List and describe each dependency", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Dependency Name", + "description": "Name of the organization, technology, or resource", + "maxLength": 100 + }, + "type": { + "type": "string", + "enum": [ + "Technical", + "Organizational", + "Legal", + "Financial", + "Other" + ], + "title": "Dependency Type", + "description": "Type of dependency" + }, + "description": { + "type": "string", + "title": "Description", + "description": "Explain why this dependency is essential and how it affects your project", + "maxLength": 500 + }, + "mitigationPlan": { + "type": "string", + "title": "Mitigation Plan", + "description": "How will you handle potential issues with this dependency", + "maxLength": 300 + } + }, + "required": ["name", "type", "description"] + }, + "minItems": 0, + "maxItems": 10 + } + }, + "required": ["hasDependencies"], + "dependencies": { + "details": ["hasDependencies"] + }, + "if": { + "properties": { "hasDependencies": { "const": true } } + }, + "then": { + "required": ["details"] + } + } + } + }, + "milestones": { + "type": "object", + "title": "Project Milestones", + "description": "Detailed project milestones and deliverables", + "properties": { + "milestonesConfig": { + "type": "object", + "title": "Milestones Configuration", + "description": "Configuration for number of milestones based on grant amount", + "properties": { + "grantAmount": { + "type": "number", + "title": "Grant Amount in ADA", + "description": "Total grant amount requested in ADA", + "minimum": 0, + "maximum": 1000000 + }, + "numberOfMilestones": { + "type": "integer", + "title": "Number of Milestones", + "description": "Total number of milestones including the final milestone", + "minimum": 3, + "maximum": 10 + } + }, + "required": ["grantAmount", "numberOfMilestones"] + }, + "milestonesList": { + "type": "array", + "title": "List of Milestones", + "description": "Detailed description of each project milestone", + "items": { + "type": "object", + "title": "Milestone", + "properties": { + "title": { + "type": "string", + "title": "Milestone Title", + "description": "Short, descriptive title for the milestone", + "minLength": 5, + "maxLength": 100 + }, + "description": { + "type": "string", + "title": "Milestone Description", + "description": "Detailed description of what this milestone entails", + "minLength": 50, + "maxLength": 1000 + }, + "deliverables": { + "type": "array", + "title": "Deliverables", + "description": "Specific outputs and deliverables for this milestone", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Deliverable Name", + "minLength": 5, + "maxLength": 100 + }, + "description": { + "type": "string", + "title": "Deliverable Description", + "minLength": 20, + "maxLength": 500 + }, + "type": { + "type": "string", + "title": "Deliverable Type", + "enum": [ + "Documentation", + "Software", + "Report", + "Presentation", + "Video", + "Other" + ] + } + }, + "required": ["name", "description", "type"] + }, + "minItems": 1, + "maxItems": 5 + }, + "acceptanceCriteria": { + "type": "array", + "title": "Acceptance Criteria", + "description": "Specific criteria that must be met to consider this milestone complete", + "items": { + "type": "string", + "minLength": 10, + "maxLength": 200 + }, + "minItems": 1, + "maxItems": 5 + }, + "evidenceOfCompletion": { + "type": "array", + "title": "Evidence of Completion", + "description": "How will you demonstrate that this milestone is complete?", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "title": "Evidence Type", + "enum": [ + "Code Repository", + "Documentation", + "Demo Video", + "Test Results", + "Metrics Report", + "User Feedback", + "Other" + ] + }, + "description": { + "type": "string", + "title": "Evidence Description", + "minLength": 20, + "maxLength": 300 + } + }, + "required": ["type", "description"] + }, + "minItems": 1, + "maxItems": 3 + }, + "timeline": { + "type": "object", + "title": "Timeline", + "properties": { + "startDate": { + "type": "string", + "title": "Start Date", + "format": "date" + }, + "endDate": { + "type": "string", + "title": "End Date", + "format": "date" + }, + "durationInWeeks": { + "type": "integer", + "title": "Duration in Weeks", + "minimum": 1, + "maximum": 52 + } + }, + "required": ["startDate", "endDate", "durationInWeeks"] + }, + "budget": { + "type": "object", + "title": "Milestone Budget", + "properties": { + "amount": { + "type": "number", + "title": "Amount in ADA", + "minimum": 0 + }, + "breakdown": { + "type": "array", + "title": "Budget Breakdown", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": [ + "Development", + "Design", + "Marketing", + "Operations", + "Other" + ] + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "description": { + "type": "string", + "minLength": 10, + "maxLength": 200 + } + }, + "required": ["category", "amount", "description"] + }, + "minItems": 1 + } + }, + "required": ["amount", "breakdown"] + } + }, + "required": [ + "title", + "description", + "deliverables", + "acceptanceCriteria", + "evidenceOfCompletion", + "timeline", + "budget" + ] + }, + "minItems": 3, + "maxItems": 10 + }, + "finalMilestone": { + "type": "object", + "title": "Final Milestone", + "description": "Project close-out milestone with final report and video", + "properties": { + "closeoutReport": { + "type": "object", + "title": "Project Close-out Report", + "properties": { + "summary": { + "type": "string", + "title": "Project Summary", + "description": "Overall summary of project achievements", + "minLength": 100, + "maxLength": 2000 + }, + "keyAchievements": { + "type": "array", + "title": "Key Achievements", + "items": { + "type": "string", + "minLength": 10, + "maxLength": 200 + }, + "minItems": 1, + "maxItems": 10 + }, + "lessonsLearned": { + "type": "array", + "title": "Lessons Learned", + "items": { + "type": "string", + "minLength": 20, + "maxLength": 500 + }, + "minItems": 1, + "maxItems": 5 + }, + "futureSteps": { + "type": "string", + "title": "Future Steps", + "description": "Plans for project continuation or future development", + "minLength": 50, + "maxLength": 1000 + } + }, + "required": ["summary", "keyAchievements", "lessonsLearned", "futureSteps"] + }, + "demoVideo": { + "type": "object", + "title": "Project Demo Video", + "properties": { + "url": { + "type": "string", + "title": "Video URL", + "format": "uri", + "pattern": "^https://" + }, + "duration": { + "type": "integer", + "title": "Duration in Minutes", + "minimum": 3, + "maximum": 15 + }, + "description": { + "type": "string", + "title": "Video Description", + "minLength": 50, + "maxLength": 500 + } + }, + "required": ["url", "duration", "description"] + } + }, + "required": ["closeoutReport", "demoVideo"] + } + }, + "required": ["milestonesConfig", "milestonesList", "finalMilestone"] + }, + "Horizons": { + "type": "object", + "title": "Project Horizons", + "description": "Long-term vision and categorization of your project", + "properties": { + "category": { + "type": "object", + "title": "Project Category", + "description": "Select the most relevant category and tags for your project", + "properties": { + "primaryCategory": { + "type": "string", + "title": "Primary Category", + "description": "Main category that best describes your project", + "enum": [ + "Governance", + "Education", + "Community & Outreach", + "Development & Tools", + "Identity & Security", + "DeFi", + "Real World Applications", + "Events & Marketing", + "Interoperability", + "Legal & Policy", + "Sustainability", + "Smart Contracts" + ] + }, + "subCategory": { + "type": "string", + "title": "Sub-category", + "description": "Specific area within the main category", + "enum": { + "Governance": [ + "DAO", + "Voting", + "Treasury Management" + ], + "Education": [ + "Learn to Earn", + "Training", + "Translation" + ], + "Community & Outreach": [ + "Connected Community", + "Social Media", + "Community Building" + ], + "Development & Tools": [ + "Developer Tools", + "L2 Infrastructure", + "Analytics", + "AI Research", + "UTXO", + "P2P" + ], + "Identity & Security": [ + "Identity & Verification", + "Cybersecurity", + "Authentication", + "Privacy" + ], + "DeFi": [ + "Payments", + "Stablecoin", + "Risk Management", + "Yield", + "Staking", + "Lending" + ], + "Real World Applications": [ + "Wallet", + "Marketplace", + "Manufacturing", + "IoT", + "Financial Services", + "E-commerce", + "Business Services", + "Supply Chain", + "Real Estate", + "Healthcare", + "Tourism", + "Entertainment", + "RWA", + "Music", + "Tokenization" + ], + "Events & Marketing": [ + "Events", + "Marketing", + "Hackathons", + "Accelerator", + "Incubator" + ], + "Interoperability": [ + "Cross-chain", + "Off-chain", + "Bridges" + ], + "Legal & Policy": [ + "Policy", + "Advocacy", + "Standards", + "Compliance" + ], + "Sustainability": [ + "Environment", + "Agriculture", + "Clean Energy" + ], + "Smart Contracts": [ + "Development", + "Security", + "Templates", + "Auditing" + ] + } + } + } + }, + "tags": { + "type": "array", + "title": "Project Tags", + "description": "Additional tags to help categorize your project", + "items": { + "type": "string", + "minLength": 2, + "maxLength": 30 + }, + "minItems": 1, + "maxItems": 5, + "uniqueItems": true + }, + "impact": { + "type": "object", + "title": "Project Impact", + "description": "Describe the expected impact of your project", + "properties": { + "timeframe": { + "type": "string", + "enum": [ + "Short-term (0-6 months)", + "Medium-term (6-18 months)", + "Long-term (18+ months)" + ], + "title": "Impact Timeframe", + "description": "Expected timeframe to see meaningful impact" + }, + "scale": { + "type": "string", + "enum": [ + "Local", + "Regional", + "Global" + ], + "title": "Impact Scale", + "description": "Geographic scale of impact" + }, + "metrics": { + "type": "array", + "title": "Impact Metrics", + "description": "Key metrics to measure project success", + "items": { + "type": "object", + "properties": { + "metric": { + "type": "string", + "title": "Metric Name", + "description": "Name of the metric", + "maxLength": 100 + }, + "target": { + "type": "string", + "title": "Target Value", + "description": "Target value or goal for this metric", + "maxLength": 100 + }, + "measurement": { + "type": "string", + "title": "Measurement Method", + "description": "How this metric will be measured", + "maxLength": 200 + } + }, + "required": ["metric", "target", "measurement"] + }, + "minItems": 1, + "maxItems": 5 + } + }, + "required": ["timeframe", "scale", "metrics"] + } + }, + "required": ["primaryCategory", "subCategory", "tags", "impact"] + }, + "proposalDetails": { + "type": "object", + "title": "Proposal Details", + "description": "Detailed information about your proposal's solution, impact, and feasibility", + "properties": { + "solution": { + "type": "object", + "title": "Solution Description", + "description": "Detailed description of your proposed solution", + "properties": { + "description": { + "type": "string", + "title": "Solution Description", + "description": "Provide a comprehensive description of your proposed solution", + "minLength": 100, + "maxLength": 2000, + "examples": [ + "Our solution involves developing a decentralized education platform that will..." + ] + }, + "uniqueValue": { + "type": "string", + "title": "Unique Value Proposition", + "description": "What makes your solution unique and innovative?", + "minLength": 50, + "maxLength": 500 + }, + "targetAudience": { + "type": "array", + "title": "Target Audience", + "description": "Who will benefit from your solution?", + "items": { + "type": "string", + "minLength": 5, + "maxLength": 100 + }, + "minItems": 1, + "maxItems": 5, + "uniqueItems": true + }, + "implementation": { + "type": "string", + "title": "Implementation Approach", + "description": "How will you implement your solution?", + "minLength": 100, + "maxLength": 1000 + } + }, + "required": ["description", "uniqueValue", "targetAudience", "implementation"] + }, + "impact": { + "type": "object", + "title": "Project Impact", + "description": "Define and measure the impact of your project", + "properties": { + "communityBenefit": { + "type": "string", + "title": "Community Benefit", + "description": "How will the Cardano community benefit from your project?", + "minLength": 100, + "maxLength": 1000 + }, + "metrics": { + "type": "array", + "title": "Impact Metrics", + "description": "Specific metrics to measure project success", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Metric Name", + "minLength": 5, + "maxLength": 100 + }, + "description": { + "type": "string", + "title": "Metric Description", + "minLength": 20, + "maxLength": 300 + }, + "target": { + "type": "string", + "title": "Target Value", + "minLength": 1, + "maxLength": 100 + }, + "measurement": { + "type": "string", + "title": "Measurement Method", + "minLength": 20, + "maxLength": 300 + } + }, + "required": ["name", "description", "target", "measurement"] + }, + "minItems": 2, + "maxItems": 5 + }, + "outputs": { + "type": "array", + "title": "Project Outputs", + "description": "Tangible outputs and deliverables from the project", + "items": { + "type": "string", + "minLength": 10, + "maxLength": 200 + }, + "minItems": 1, + "maxItems": 10, + "uniqueItems": true + } + }, + "required": ["communityBenefit", "metrics", "outputs"] + }, + "capability": { + "type": "object", + "title": "Capability & Feasibility", + "description": "Demonstrate your ability to deliver the project successfully", + "properties": { + "teamExperience": { + "type": "string", + "title": "Team Experience", + "description": "Describe your team's relevant experience and capabilities", + "minLength": 100, + "maxLength": 1000 + }, + "feasibilityApproach": { + "type": "string", + "title": "Feasibility Approach", + "description": "How will you validate the feasibility of your approach?", + "minLength": 100, + "maxLength": 1000 + }, + "riskMitigation": { + "type": "array", + "title": "Risk Mitigation", + "description": "Key risks and mitigation strategies", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "title": "Risk Description", + "minLength": 10, + "maxLength": 200 + }, + "impact": { + "type": "string", + "enum": ["Low", "Medium", "High"], + "title": "Risk Impact" + }, + "mitigation": { + "type": "string", + "title": "Mitigation Strategy", + "minLength": 20, + "maxLength": 300 + } + }, + "required": ["risk", "impact", "mitigation"] + }, + "minItems": 1, + "maxItems": 5 + }, + "fundManagement": { + "type": "string", + "title": "Fund Management", + "description": "How will you ensure proper management and accountability of funds?", + "minLength": 100, + "maxLength": 1000 + } + }, + "required": ["teamExperience", "feasibilityApproach", "riskMitigation", "fundManagement"] + } + }, + "required": ["solution", "impact", "capability"] + }, + "finalPitch": { + "type": "object", + "title": "Final Pitch", + "description": "Final project pitch including team, budget, and value proposition", + "properties": { + "team": { + "type": "object", + "title": "Team Information", + "description": "Details about the project team and their capabilities", + "properties": { + "members": { + "type": "array", + "title": "Team Members", + "description": "List of team members and their roles", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Full name of the team member", + "minLength": 2, + "maxLength": 100 + }, + "role": { + "type": "string", + "title": "Role", + "description": "Primary role in the project", + "minLength": 5, + "maxLength": 100 + }, + "expertise": { + "type": "array", + "title": "Areas of Expertise", + "items": { + "type": "string", + "minLength": 3, + "maxLength": 50 + }, + "minItems": 1, + "maxItems": 5, + "uniqueItems": true + }, + "experience": { + "type": "string", + "title": "Relevant Experience", + "description": "Brief description of relevant experience", + "minLength": 50, + "maxLength": 500 + }, + "links": { + "type": "array", + "title": "Professional Links", + "description": "Links to professional profiles or past work", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "LinkedIn", + "GitHub", + "Portfolio", + "Twitter", + "Website", + "Other" + ] + }, + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "description": { + "type": "string", + "maxLength": 100 + } + }, + "required": ["type", "url"] + }, + "maxItems": 5 + } + }, + "required": ["name", "role", "expertise", "experience"] + }, + "minItems": 1, + "maxItems": 10 + }, + "teamCapabilities": { + "type": "string", + "title": "Team Capabilities", + "description": "Overview of the team's collective capabilities and why they are best suited for this project", + "minLength": 100, + "maxLength": 1000 + }, + "previousWork": { + "type": "array", + "title": "Previous Work", + "description": "Examples of relevant previous work or projects", + "items": { + "type": "object", + "properties": { + "projectName": { + "type": "string", + "title": "Project Name", + "minLength": 3, + "maxLength": 100 + }, + "description": { + "type": "string", + "title": "Project Description", + "minLength": 50, + "maxLength": 500 + }, + "relevance": { + "type": "string", + "title": "Relevance to Current Proposal", + "minLength": 50, + "maxLength": 300 + }, + "url": { + "type": "string", + "title": "Project URL", + "format": "uri", + "pattern": "^https://" + } + }, + "required": ["projectName", "description", "relevance"] + }, + "maxItems": 5 + } + }, + "required": ["members", "teamCapabilities"] + }, + "budget": { + "type": "object", + "title": "Budget Details", + "description": "Detailed budget breakdown and justification", + "properties": { + "totalBudget": { + "type": "number", + "title": "Total Budget (ADA)", + "description": "Total amount requested in ADA", + "minimum": 0, + "maximum": 1000000 + }, + "categories": { + "type": "array", + "title": "Budget Categories", + "description": "Breakdown of budget by category", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": [ + "Development", + "Design", + "Marketing", + "Operations", + "Research", + "Community Management", + "Legal", + "Other" + ] + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "description": { + "type": "string", + "minLength": 20, + "maxLength": 300 + }, + "breakdown": { + "type": "array", + "items": { + "type": "object", + "properties": { + "item": { + "type": "string", + "minLength": 5, + "maxLength": 100 + }, + "cost": { + "type": "number", + "minimum": 0 + }, + "justification": { + "type": "string", + "minLength": 20, + "maxLength": 200 + } + }, + "required": ["item", "cost", "justification"] + }, + "minItems": 1 + } + }, + "required": ["category", "amount", "description", "breakdown"] + }, + "minItems": 1 + }, + "timeline": { + "type": "object", + "title": "Budget Timeline", + "properties": { + "distributionSchedule": { + "type": "array", + "items": { + "type": "object", + "properties": { + "milestone": { + "type": "string", + "minLength": 5, + "maxLength": 100 + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "percentage": { + "type": "number", + "minimum": 0, + "maximum": 100 + } + }, + "required": ["milestone", "amount", "percentage"] + }, + "minItems": 1 + } + }, + "required": ["distributionSchedule"] + } + }, + "required": ["totalBudget", "categories", "timeline"] + }, + "valueProposition": { + "type": "object", + "title": "Value Proposition", + "description": "Justification of the project's value for money", + "properties": { + "costBenefitAnalysis": { + "type": "string", + "title": "Cost-Benefit Analysis", + "description": "Analysis of the project's costs versus its benefits to the Cardano ecosystem", + "minLength": 100, + "maxLength": 1000 + }, + "impactMetrics": { + "type": "array", + "title": "Impact Metrics", + "description": "Specific metrics that demonstrate value for money", + "items": { + "type": "object", + "properties": { + "metric": { + "type": "string", + "title": "Metric Name", + "minLength": 5, + "maxLength": 100 + }, + "target": { + "type": "string", + "title": "Target Value", + "minLength": 1, + "maxLength": 100 + }, + "justification": { + "type": "string", + "title": "Value Justification", + "minLength": 50, + "maxLength": 300 + } + }, + "required": ["metric", "target", "justification"] + }, + "minItems": 2, + "maxItems": 5 + }, + "longTermValue": { + "type": "string", + "title": "Long-term Value", + "description": "Description of the long-term value and sustainability of the project", + "minLength": 100, + "maxLength": 1000 + }, + "communityBenefits": { + "type": "array", + "title": "Community Benefits", + "description": "Specific benefits to the Cardano community", + "items": { + "type": "string", + "minLength": 20, + "maxLength": 200 + }, + "minItems": 2, + "maxItems": 5 + } + }, + "required": ["costBenefitAnalysis", "impactMetrics", "longTermValue", "communityBenefits"] + } + }, + "required": ["team", "budget", "valueProposition"] + }, + "mandatoryAcknowledgments": { + "type": "object", + "title": "Mandatory Acknowledgments", + "description": "Required acknowledgments and agreements for proposal submission", + "properties": { + "fundRules": { + "type": "object", + "title": "Fund Rules Agreement", + "properties": { + "acknowledgment": { + "type": "boolean", + "title": "Fund Rules Acknowledgment", + "description": "I confirm that I have read and agree to be bound by the Fund Rules", + "const": true + }, + "version": { + "type": "string", + "title": "Fund Rules Version", + "description": "Version of the Fund Rules being acknowledged", + "pattern": "^F[0-9]+$" + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the rules were acknowledged", + "format": "date-time" + } + }, + "required": ["acknowledgment", "version", "timestamp"] + }, + "termsAndConditions": { + "type": "object", + "title": "Terms and Conditions Agreement", + "properties": { + "acknowledgment": { + "type": "boolean", + "title": "Terms and Conditions Acknowledgment", + "description": "I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions", + "const": true + }, + "version": { + "type": "string", + "title": "Terms Version", + "description": "Version of the Terms and Conditions being acknowledged", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the terms were acknowledged", + "format": "date-time" + } + }, + "required": ["acknowledgment", "version", "timestamp"] + }, + "privacyPolicy": { + "type": "object", + "title": "Privacy Policy Agreement", + "properties": { + "acknowledgment": { + "type": "boolean", + "title": "Privacy Policy Acknowledgment", + "description": "I acknowledge and agree that any data I share will be processed in accordance with the Catalyst FCS Privacy Policy", + "const": true + }, + "version": { + "type": "string", + "title": "Privacy Policy Version", + "description": "Version of the Privacy Policy being acknowledged", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the privacy policy was acknowledged", + "format": "date-time" + } + }, + "required": ["acknowledgment", "version", "timestamp"] + }, + "intellectualProperty": { + "type": "object", + "title": "Intellectual Property Declaration", + "properties": { + "acknowledgment": { + "type": "boolean", + "title": "IP Rights Acknowledgment", + "description": "I confirm that I have the necessary rights to all intellectual property included in this proposal", + "const": true + }, + "details": { + "type": "string", + "title": "IP Details", + "description": "Additional details about intellectual property rights (if applicable)", + "maxLength": 1000 + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the IP declaration was made", + "format": "date-time" + } + }, + "required": ["acknowledgment", "timestamp"] + }, + "compliance": { + "type": "object", + "title": "Compliance Declaration", + "properties": { + "legalCompliance": { + "type": "boolean", + "title": "Legal Compliance", + "description": "I confirm that my proposal complies with all applicable laws and regulations", + "const": true + }, + "noConflictOfInterest": { + "type": "boolean", + "title": "No Conflict of Interest", + "description": "I confirm that there are no undisclosed conflicts of interest", + "const": true + }, + "accurateInformation": { + "type": "boolean", + "title": "Information Accuracy", + "description": "I confirm that all information provided is accurate and complete", + "const": true + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the compliance declaration was made", + "format": "date-time" + } + }, + "required": ["legalCompliance", "noConflictOfInterest", "accurateInformation", "timestamp"] + }, + "additionalAcknowledgments": { + "type": "array", + "title": "Additional Acknowledgments", + "description": "Any additional acknowledgments required for specific proposal types", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "title": "Acknowledgment Type", + "minLength": 5, + "maxLength": 100 + }, + "acknowledgment": { + "type": "boolean", + "title": "Acknowledgment", + "const": true + }, + "description": { + "type": "string", + "title": "Description", + "description": "Detailed description of what is being acknowledged", + "minLength": 10, + "maxLength": 500 + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "format": "date-time" + } + }, + "required": ["type", "acknowledgment", "description", "timestamp"] + } + } + }, + "required": [ + "fundRules", + "termsAndConditions", + "privacyPolicy", + "intellectualProperty", + "compliance" + ] + } + } +} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.example.json deleted file mode 100644 index 27a616b7cc3..00000000000 --- a/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.example.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "$schema": "./proposalTemplate.F14.schema.json", - "template_version": { - "description": "The version of the schema, formatted as a ULID.", - "title": "Version", - "uid": "version_section", - "value": "01F8MECHZXK8F8D8F8D8F8D8F9" - }, - "category_segment": { - "description": "Details about the categories related to the proposal.", - "title": "Category Segment", - "uid": "category_segment_section", - "value": { - "additional_questions": { - "description": "Details about additional questions regarding the project.", - "title": "Additional Questions", - "uid": "additional_questions_section", - "value": { - "is_open_source": true, - "license_type": "anything i please", - "product_repository_url": "http://somwhere.test/a/project/url", - "project_documentation_url": "http://somwhere.test/a/project/url/for/documentation" - } - }, - "agreements": { - "description": "Terms and agreements for the proposal.", - "title": "Terms and Agreements", - "uid": "agreements_section", - "value": { - "agree_fund_rules": false, - "catalyst_terms_and_conditions": false, - "privacy_policy": true - } - }, - "milestones": { - "description": "Milestones to track project progress.", - "title": "Project Milestones", - "uid": "milestones_section", - "value": [ - { - "deliverables": [ - "something", - "something else" - ], - "description": "a multi lin\\nstring", - "title": "a single line string" - } - ] - }, - "resources": { - "description": "Information about resources and budget allocation.", - "title": "Resources and Budget", - "uid": "resources_section", - "value": { - "budget_breakdown": "multi\\nline\\nstring", - "project_team": [ - { - "experience": "multi\\nline\\nstring", - "role": "manager" - } - ] - } - } - } - }, - "category_template": { - "description": "The category to which this template belongs.", - "title": "Category", - "uid": "category_section", - "value": "can_be_any_value_without_whitespace" - }, - "proposal_setup_segment": { - "description": "Basic information about your proposal.", - "title": "Proposal Setup", - "uid": "proposal_setup_section", - "value": { - "proposal_title": { - "description": "The name of the proposal.", - "title": "Proposal Title", - "uid": "proposal_title_field_section", - "value": "a proposal title single line" - } - } - }, - "proposal_summary_segment": { - "description": "Detailed information about the proposal.", - "title": "Proposal Details", - "uid": "proposal_details_section", - "value": { - "proposal_problem": "a\\nmultiline\\nproblem", - "proposal_solution": "a\\nmultiline\\nsolution" - } - }, - "public_description_segment": { - "description": "Detailed public description information.", - "title": "Public Description", - "uid": "public_description_section", - "value": { - "topic_1": "topic 1", - "topic_2": "topic 2", - "topic_3": "topic 3", - "topic_4": "topic 4" - } - } -} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.schema.json deleted file mode 100644 index 8583da87505..00000000000 --- a/docs/src/architecture/08_concepts/document_templates/proposal/proposalTemplate.F14.schema.json +++ /dev/null @@ -1,401 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Catalyst Fund 14 Base Proposal Template", - "description": "A structured template for creating Fund 14 proposals", - "type": "object", - "properties": { - "template_version": { - "type": "object", - "properties": { - "uid": { - "const": "version_section", - "description": "Unique identifier for the version." - }, - "title": { - "const": "Version", - "description": "Title of the version." - }, - "description": { - "const": "The version of the schema, formatted as a ULID.", - "description": "Description for the version." - }, - "value": { - "type": "string", - "description": "A unique identifier for the version, formatted as a ULID.", - "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", - "default": "01F8MECHZXK8F8D8F8D8F8D8F8" - } - }, - "required": ["uid", "title", "description", "value"] - }, - "category_template": { - "type": "object", - "properties": { - "uid": { - "const": "category_section", - "description": "Unique identifier for the category." - }, - "title": { - "const": "Category", - "description": "Title of the category." - }, - "description": { - "const": "The category to which this template belongs.", - "description": "Description for the category." - }, - "value": { - "type": "string", - "description": "A unique identifier for the category.", - "pattern": "^[a-zA-Z0-9_-]+$", - "default": "cardano_open_category" - } - }, - "required": ["uid", "title", "description", "value"] - }, - "proposal_setup_segment": { - "type": "object", - "properties": { - "uid": { - "const": "proposal_setup_section", - "description": "Unique identifier for the Proposal Setup Segment." - }, - "title": { - "const": "Proposal Setup", - "description": "Title of the Proposal Setup Segment." - }, - "description": { - "const": "Basic information about your proposal.", - "description": "Description for the Proposal Setup Segment." - }, - "value": { - "type": "object", - "properties": { - "proposal_title": { - "type": "object", - "properties": { - "uid": { - "const": "proposal_title_field_section", - "description": "Unique identifier for the Proposal Title field." - }, - "title": { - "const": "Proposal Title", - "description": "Title of the Proposal Title field." - }, - "description": { - "const": "The name of the proposal.", - "description": "Description for the Proposal Title field." - }, - "value": { - "type": "string", - "maxLength": 60, - "description": "User-provided proposal title.", - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - } - }, - "required": ["uid", "title", "description", "value"] - } - } - } - }, - "required": ["uid", "title", "description", "value"] - }, - "proposal_summary_segment": { - "type": "object", - "properties": { - "uid": { - "const": "proposal_details_section", - "description": "Unique identifier for the Proposal Details." - }, - "title": { - "const": "Proposal Details", - "description": "Details about the proposal." - }, - "description": { - "const": "Detailed information about the proposal.", - "description": "Description for the Proposal Details." - }, - "value": { - "type": "object", - "properties": { - "proposal_problem": { - "type": "string", - "description": "Describe the problem you're addressing in the Cardano ecosystem.", - "maxLength": 1000, - "format": "string", - "pattern": "^[\\s\\S]*$" - }, - "proposal_solution": { - "type": "string", - "description": "Describe your solution and how it addresses the problem.", - "maxLength": 2000, - "format": "string", - "pattern": "^[\\s\\S]*$" - } - }, - "required": ["proposal_problem", "proposal_solution"] - } - }, - "required": ["uid", "title", "description", "value"] - }, - "public_description_segment": { - "type": "object", - "properties": { - "uid": { - "const": "public_description_section", - "description": "Unique identifier for the Public Description Segment." - }, - "title": { - "const": "Public Description", - "description": "Public description of the proposal." - }, - "description": { - "const": "Detailed public description information.", - "description": "Description for the Public Description Segment." - }, - "value": { - "type": "object", - "properties": { - "topic_1": { - "type": "string", - "description": "First topic of the public description.", - "maxLength": 500, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "topic_2": { - "type": "string", - "description": "Second topic of the public description.", - "maxLength": 500, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "topic_3": { - "type": "string", - "description": "Third topic of the public description.", - "maxLength": 500, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "topic_4": { - "type": "string", - "description": "Fourth topic of the public description.", - "maxLength": 500, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - } - }, - "required": ["topic_1", "topic_2", "topic_3", "topic_4"] - } - }, - "required": ["uid", "title", "description", "value"] - }, - "category_segment": { - "type": "object", - "properties": { - "uid": { - "const": "category_segment_section", - "description": "Unique identifier for the Category Segment." - }, - "title": { - "const": "Category Segment", - "description": "Segment for categorizing the proposal." - }, - "description": { - "const": "Details about the categories related to the proposal.", - "description": "Description for the Category Segment." - }, - "value": { - "type": "object", - "properties": { - "additional_questions": { - "type": "object", - "properties": { - "uid": { - "const": "additional_questions_section", - "description": "Unique identifier for Additional Questions." - }, - "title": { - "const": "Additional Questions", - "description": "Questions related to the project." - }, - "description": { - "const": "Details about additional questions regarding the project.", - "description": "Description for Additional Questions." - }, - "value": { - "type": "object", - "properties": { - "is_open_source": { - "type": "boolean", - "description": "Is the project open source?" - }, - "license_type": { - "type": "string", - "description": "Type of license used.", - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "product_repository_url": { - "type": "string", - "format": "uri", - "description": "URL to the product repository." - }, - "project_documentation_url": { - "type": "string", - "format": "uri", - "description": "URL to project documentation." - } - }, - "required": ["is_open_source", "license_type", "product_repository_url", "project_documentation_url"] - } - }, - "required": ["uid", "title", "description", "value"] - }, - "milestones": { - "type": "object", - "properties": { - "uid": { - "const": "milestones_section", - "description": "Unique identifier for the Project Milestones." - }, - "title": { - "const": "Project Milestones", - "description": "Milestones for the project." - }, - "description": { - "const": "Milestones to track project progress.", - "description": "Description for the Project Milestones." - }, - "value": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Milestone Title.", - "maxLength": 100, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "description": { - "type": "string", - "description": "Milestone Description.", - "maxLength": 500, - "format": "string", - "pattern": "^[\\s\\S]*$" - }, - "deliverables": { - "type": "array", - "items": { - "type": "string", - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - } - } - }, - "required": ["title", "description", "deliverables"] - } - } - }, - "required": ["uid", "title", "description", "value"] - }, - "resources": { - "type": "object", - "properties": { - "uid": { - "const": "resources_section", - "description": "Unique identifier for Resources and Budget." - }, - "title": { - "const": "Resources and Budget", - "description": "Details about resources and budget." - }, - "description": { - "const": "Information about resources and budget allocation.", - "description": "Description for Resources and Budget." - }, - "value": { - "type": "object", - "properties": { - "project_team": { - "type": "array", - "items": { - "type": "object", - "properties": { - "role": { - "type": "string", - "description": "Role of the team member.", - "maxLength": 100, - "format": "string", - "pattern": "^[a-zA-Z0-9 ]+$" - }, - "experience": { - "type": "string", - "description": "Relevant experience of the team member.", - "maxLength": 500, - "format": "string", - "pattern": "^[\\s\\S]*$" - } - }, - "required": ["role", "experience"] - } - }, - "budget_breakdown": { - "type": "string", - "description": "Detailed breakdown of how the funds will be used.", - "maxLength": 2000, - "format": "string", - "pattern": "^[\\s\\S]*$" - } - }, - "required": ["project_team", "budget_breakdown"] - } - }, - "required": ["uid", "title", "description", "value"] - }, - "agreements": { - "type": "object", - "properties": { - "uid": { - "const": "agreements_section", - "description": "Unique identifier for Terms and Agreements." - }, - "title": { - "const": "Terms and Agreements", - "description": "Agreements related to the proposal." - }, - "description": { - "const": "Terms and agreements for the proposal.", - "description": "Description for Terms and Agreements." - }, - "value": { - "type": "object", - "properties": { - "agree_fund_rules": { - "type": "boolean", - "description": "Agreement to fund rules." - }, - "catalyst_terms_and_conditions": { - "type": "boolean", - "description": "Agreement to terms and conditions." - }, - "privacy_policy": { - "type": "boolean", - "description": "Agreement to privacy policy." - } - }, - "required": ["agree_fund_rules", "catalyst_terms_and_conditions", "privacy_policy"] - } - }, - "required": ["uid", "title", "description", "value"] - } - }, - "required": ["additional_questions", "milestones", "resources", "agreements"] - } - }, - "required": ["uid", "title", "description", "value"] - } - } -} \ No newline at end of file From 0496a040ac711e2722ff559bfaba38568682de0e Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Wed, 4 Dec 2024 22:12:35 +0300 Subject: [PATCH 04/25] feat(cat-gateway: added proposal schema and example of f14 templates --- .../F14-Generic/example.proposal.json | 142 +++ .../proposalTemplate.F14.schema.json | 856 ++++++++++-------- 2 files changed, 613 insertions(+), 385 deletions(-) create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json new file mode 100644 index 00000000000..bd5def1aaa7 --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json @@ -0,0 +1,142 @@ +{ + "$schema": "./proposalTemplate.F14.schema.json", + "setup": { + "title": { + "title": "Cardano DeFi Integration Platform" + }, + "proposer": { + "mainApplicant": "John Smith", + "applicant_type": "Entity (Incorporated)", + "co-proposers": ["Alice Johnson", "Bob Wilson"] + } + }, + "proposalSummary": { + "budget": { + "requestedFunds": 500000 + }, + "problem": { + "description": "Current DeFi platforms on Cardano lack seamless integration capabilities, making it difficult for developers to build interconnected financial applications.", + "relevance": "This problem affects the entire Cardano ecosystem by limiting the growth and adoption of DeFi applications." + }, + "supportingLinks": { + "documentation": "https://example.com/project-docs", + "github": "https://github.com/example/defi-platform", + "media": ["https://example.com/demo-video"] + } + }, + "horizons": { + "category": { + "primaryCategory": "DeFi", + "subCategory": "Integration Tools" + }, + "tags": ["defi", "integration", "developer-tools", "smart-contracts"] + }, + "proposalDetails": { + "solution": { + "description": "Our platform will provide a unified API layer that enables seamless integration between different DeFi protocols on Cardano.", + "features": [ + "Standardized API endpoints", + "Smart contract templates", + "Cross-protocol liquidity management" + ] + }, + "impact": { + "metrics": [ + "Number of integrated protocols", + "Developer adoption rate", + "Transaction volume through the platform" + ], + "targetAudience": "DeFi developers and protocol creators on Cardano" + }, + "capability": { + "teamExperience": "Our team has 5+ years of experience in DeFi development and Cardano ecosystem", + "resources": "Fully equipped development team with blockchain expertise" + } + }, + "milestones": { + "milestonesConfig": { + "count": 4, + "duration": "6 months" + }, + "milestonesList": [ + { + "title": "Architecture Design", + "description": "Complete system architecture and API specifications", + "deliverables": ["Architecture documentation", "API specifications"], + "budget": { + "amount": 100000 + } + }, + { + "title": "Core Development", + "description": "Develop core integration layer and smart contracts", + "deliverables": ["Core platform code", "Smart contract templates"], + "budget": { + "amount": 200000 + } + }, + { + "title": "Testing and Integration", + "description": "Comprehensive testing and initial protocol integrations", + "deliverables": ["Test reports", "Integration documentation"], + "budget": { + "amount": 150000 + } + } + ], + "finalMilestone": { + "title": "Launch and Documentation", + "deliverables": ["Platform launch", "Complete documentation", "Video demonstration"], + "budget": { + "amount": 50000 + } + } + }, + "finalPitch": { + "team": { + "members": [ + { + "name": "John Smith", + "role": "Project Lead", + "experience": "10 years in blockchain development" + }, + { + "name": "Alice Johnson", + "role": "Smart Contract Developer", + "experience": "5 years Cardano development" + } + ], + "teamCapabilities": "Our team combines deep expertise in DeFi protocols, Cardano development, and system architecture." + }, + "budget": { + "totalBudget": 500000, + "categories": { + "development": 300000, + "testing": 100000, + "documentation": 50000, + "management": 50000 + }, + "timeline": { + "distributionSchedule": "Quarterly" + } + }, + "valueProposition": { + "impact": "Accelerate DeFi development on Cardano", + "sustainability": "Platform fees and maintenance contracts" + } + }, + "mandatoryAcknowledgments": { + "fundRules": { + "acknowledgment": true + }, + "privacyPolicy": { + "acknowledgment": true + }, + "intellectualProperty": { + "acknowledgment": true + }, + "compliance": { + "legalCompliance": true + } + } +} diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json index 40176e7e832..ce178642c8a 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json @@ -364,294 +364,7 @@ } } }, - "milestones": { - "type": "object", - "title": "Project Milestones", - "description": "Detailed project milestones and deliverables", - "properties": { - "milestonesConfig": { - "type": "object", - "title": "Milestones Configuration", - "description": "Configuration for number of milestones based on grant amount", - "properties": { - "grantAmount": { - "type": "number", - "title": "Grant Amount in ADA", - "description": "Total grant amount requested in ADA", - "minimum": 0, - "maximum": 1000000 - }, - "numberOfMilestones": { - "type": "integer", - "title": "Number of Milestones", - "description": "Total number of milestones including the final milestone", - "minimum": 3, - "maximum": 10 - } - }, - "required": ["grantAmount", "numberOfMilestones"] - }, - "milestonesList": { - "type": "array", - "title": "List of Milestones", - "description": "Detailed description of each project milestone", - "items": { - "type": "object", - "title": "Milestone", - "properties": { - "title": { - "type": "string", - "title": "Milestone Title", - "description": "Short, descriptive title for the milestone", - "minLength": 5, - "maxLength": 100 - }, - "description": { - "type": "string", - "title": "Milestone Description", - "description": "Detailed description of what this milestone entails", - "minLength": 50, - "maxLength": 1000 - }, - "deliverables": { - "type": "array", - "title": "Deliverables", - "description": "Specific outputs and deliverables for this milestone", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Deliverable Name", - "minLength": 5, - "maxLength": 100 - }, - "description": { - "type": "string", - "title": "Deliverable Description", - "minLength": 20, - "maxLength": 500 - }, - "type": { - "type": "string", - "title": "Deliverable Type", - "enum": [ - "Documentation", - "Software", - "Report", - "Presentation", - "Video", - "Other" - ] - } - }, - "required": ["name", "description", "type"] - }, - "minItems": 1, - "maxItems": 5 - }, - "acceptanceCriteria": { - "type": "array", - "title": "Acceptance Criteria", - "description": "Specific criteria that must be met to consider this milestone complete", - "items": { - "type": "string", - "minLength": 10, - "maxLength": 200 - }, - "minItems": 1, - "maxItems": 5 - }, - "evidenceOfCompletion": { - "type": "array", - "title": "Evidence of Completion", - "description": "How will you demonstrate that this milestone is complete?", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "title": "Evidence Type", - "enum": [ - "Code Repository", - "Documentation", - "Demo Video", - "Test Results", - "Metrics Report", - "User Feedback", - "Other" - ] - }, - "description": { - "type": "string", - "title": "Evidence Description", - "minLength": 20, - "maxLength": 300 - } - }, - "required": ["type", "description"] - }, - "minItems": 1, - "maxItems": 3 - }, - "timeline": { - "type": "object", - "title": "Timeline", - "properties": { - "startDate": { - "type": "string", - "title": "Start Date", - "format": "date" - }, - "endDate": { - "type": "string", - "title": "End Date", - "format": "date" - }, - "durationInWeeks": { - "type": "integer", - "title": "Duration in Weeks", - "minimum": 1, - "maximum": 52 - } - }, - "required": ["startDate", "endDate", "durationInWeeks"] - }, - "budget": { - "type": "object", - "title": "Milestone Budget", - "properties": { - "amount": { - "type": "number", - "title": "Amount in ADA", - "minimum": 0 - }, - "breakdown": { - "type": "array", - "title": "Budget Breakdown", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "enum": [ - "Development", - "Design", - "Marketing", - "Operations", - "Other" - ] - }, - "amount": { - "type": "number", - "minimum": 0 - }, - "description": { - "type": "string", - "minLength": 10, - "maxLength": 200 - } - }, - "required": ["category", "amount", "description"] - }, - "minItems": 1 - } - }, - "required": ["amount", "breakdown"] - } - }, - "required": [ - "title", - "description", - "deliverables", - "acceptanceCriteria", - "evidenceOfCompletion", - "timeline", - "budget" - ] - }, - "minItems": 3, - "maxItems": 10 - }, - "finalMilestone": { - "type": "object", - "title": "Final Milestone", - "description": "Project close-out milestone with final report and video", - "properties": { - "closeoutReport": { - "type": "object", - "title": "Project Close-out Report", - "properties": { - "summary": { - "type": "string", - "title": "Project Summary", - "description": "Overall summary of project achievements", - "minLength": 100, - "maxLength": 2000 - }, - "keyAchievements": { - "type": "array", - "title": "Key Achievements", - "items": { - "type": "string", - "minLength": 10, - "maxLength": 200 - }, - "minItems": 1, - "maxItems": 10 - }, - "lessonsLearned": { - "type": "array", - "title": "Lessons Learned", - "items": { - "type": "string", - "minLength": 20, - "maxLength": 500 - }, - "minItems": 1, - "maxItems": 5 - }, - "futureSteps": { - "type": "string", - "title": "Future Steps", - "description": "Plans for project continuation or future development", - "minLength": 50, - "maxLength": 1000 - } - }, - "required": ["summary", "keyAchievements", "lessonsLearned", "futureSteps"] - }, - "demoVideo": { - "type": "object", - "title": "Project Demo Video", - "properties": { - "url": { - "type": "string", - "title": "Video URL", - "format": "uri", - "pattern": "^https://" - }, - "duration": { - "type": "integer", - "title": "Duration in Minutes", - "minimum": 3, - "maximum": 15 - }, - "description": { - "type": "string", - "title": "Video Description", - "minLength": 50, - "maxLength": 500 - } - }, - "required": ["url", "duration", "description"] - } - }, - "required": ["closeoutReport", "demoVideo"] - } - }, - "required": ["milestonesConfig", "milestonesList", "finalMilestone"] - }, + "Horizons": { "type": "object", "title": "Project Horizons", @@ -917,113 +630,401 @@ "items": { "type": "object", "properties": { - "name": { - "type": "string", - "title": "Metric Name", - "minLength": 5, - "maxLength": 100 - }, - "description": { + "name": { + "type": "string", + "title": "Metric Name", + "minLength": 5, + "maxLength": 100 + }, + "description": { + "type": "string", + "title": "Metric Description", + "minLength": 20, + "maxLength": 300 + }, + "target": { + "type": "string", + "title": "Target Value", + "minLength": 1, + "maxLength": 100 + }, + "measurement": { + "type": "string", + "title": "Measurement Method", + "minLength": 20, + "maxLength": 300 + } + }, + "required": ["name", "description", "target", "measurement"] + }, + "minItems": 2, + "maxItems": 5 + }, + "outputs": { + "type": "array", + "title": "Project Outputs", + "description": "Tangible outputs and deliverables from the project", + "items": { + "type": "string", + "minLength": 10, + "maxLength": 200 + }, + "minItems": 1, + "maxItems": 10, + "uniqueItems": true + } + }, + "required": ["communityBenefit", "metrics", "outputs"] + }, + "capability": { + "type": "object", + "title": "Capability & Feasibility", + "description": "Demonstrate your ability to deliver the project successfully", + "properties": { + "teamExperience": { + "type": "string", + "title": "Team Experience", + "description": "Describe your team's relevant experience and capabilities", + "minLength": 100, + "maxLength": 1000 + }, + "feasibilityApproach": { + "type": "string", + "title": "Feasibility Approach", + "description": "How will you validate the feasibility of your approach?", + "minLength": 100, + "maxLength": 1000 + }, + "riskMitigation": { + "type": "array", + "title": "Risk Mitigation", + "description": "Key risks and mitigation strategies", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "title": "Risk Description", + "minLength": 10, + "maxLength": 200 + }, + "impact": { + "type": "string", + "enum": ["Low", "Medium", "High"], + "title": "Risk Impact" + }, + "mitigation": { + "type": "string", + "title": "Mitigation Strategy", + "minLength": 20, + "maxLength": 300 + } + }, + "required": ["risk", "impact", "mitigation"] + }, + "minItems": 1, + "maxItems": 5 + }, + "fundManagement": { + "type": "string", + "title": "Fund Management", + "description": "How will you ensure proper management and accountability of funds?", + "minLength": 100, + "maxLength": 1000 + } + }, + "required": ["teamExperience", "feasibilityApproach", "riskMitigation", "fundManagement"] + } + }, + "required": ["solution", "impact", "capability"] + }, + "milestones": { + "type": "object", + "title": "Project Milestones", + "description": "Detailed project milestones and deliverables", + "properties": { + "milestonesConfig": { + "type": "object", + "title": "Milestones Configuration", + "description": "Configuration for number of milestones based on grant amount", + "properties": { + "grantAmount": { + "type": "number", + "title": "Grant Amount in ADA", + "description": "Total grant amount requested in ADA", + "minimum": 0, + "maximum": 1000000 + }, + "numberOfMilestones": { + "type": "integer", + "title": "Number of Milestones", + "description": "Total number of milestones including the final milestone", + "minimum": 3, + "maximum": 10 + } + }, + "required": ["grantAmount", "numberOfMilestones"] + }, + "milestonesList": { + "type": "array", + "title": "List of Milestones", + "description": "Detailed description of each project milestone", + "items": { + "type": "object", + "title": "Milestone", + "properties": { + "title": { + "type": "string", + "title": "Milestone Title", + "description": "Short, descriptive title for the milestone", + "minLength": 5, + "maxLength": 100 + }, + "description": { + "type": "string", + "title": "Milestone Description", + "description": "Detailed description of what this milestone entails", + "minLength": 50, + "maxLength": 1000 + }, + "deliverables": { + "type": "array", + "title": "Deliverables", + "description": "Specific outputs and deliverables for this milestone", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Deliverable Name", + "minLength": 5, + "maxLength": 100 + }, + "description": { + "type": "string", + "title": "Deliverable Description", + "minLength": 20, + "maxLength": 500 + }, + "type": { + "type": "string", + "title": "Deliverable Type", + "enum": [ + "Documentation", + "Software", + "Report", + "Presentation", + "Video", + "Other" + ] + } + }, + "required": ["name", "description", "type"] + }, + "minItems": 1, + "maxItems": 5 + }, + "acceptanceCriteria": { + "type": "array", + "title": "Acceptance Criteria", + "description": "Specific criteria that must be met to consider this milestone complete", + "items": { + "type": "string", + "minLength": 10, + "maxLength": 200 + }, + "minItems": 1, + "maxItems": 5 + }, + "evidenceOfCompletion": { + "type": "array", + "title": "Evidence of Completion", + "description": "How will you demonstrate that this milestone is complete?", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "title": "Evidence Type", + "enum": [ + "Code Repository", + "Documentation", + "Demo Video", + "Test Results", + "Metrics Report", + "User Feedback", + "Other" + ] + }, + "description": { + "type": "string", + "title": "Evidence Description", + "minLength": 20, + "maxLength": 300 + } + }, + "required": ["type", "description"] + }, + "minItems": 1, + "maxItems": 3 + }, + "timeline": { + "type": "object", + "title": "Timeline", + "properties": { + "startDate": { "type": "string", - "title": "Metric Description", - "minLength": 20, - "maxLength": 300 + "title": "Start Date", + "format": "date" }, - "target": { + "endDate": { "type": "string", - "title": "Target Value", - "minLength": 1, - "maxLength": 100 + "title": "End Date", + "format": "date" }, - "measurement": { - "type": "string", - "title": "Measurement Method", - "minLength": 20, - "maxLength": 300 + "durationInWeeks": { + "type": "integer", + "title": "Duration in Weeks", + "minimum": 1, + "maximum": 52 } }, - "required": ["name", "description", "target", "measurement"] + "required": ["startDate", "endDate", "durationInWeeks"] }, - "minItems": 2, - "maxItems": 5 + "budget": { + "type": "object", + "title": "Milestone Budget", + "properties": { + "amount": { + "type": "number", + "title": "Amount in ADA", + "minimum": 0 + }, + "breakdown": { + "type": "array", + "title": "Budget Breakdown", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": [ + "Development", + "Design", + "Marketing", + "Operations", + "Other" + ] + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "description": { + "type": "string", + "minLength": 10, + "maxLength": 200 + } + }, + "required": ["category", "amount", "description"] + }, + "minItems": 1 + } + }, + "required": ["amount", "breakdown"] + } }, - "outputs": { - "type": "array", - "title": "Project Outputs", - "description": "Tangible outputs and deliverables from the project", - "items": { - "type": "string", - "minLength": 10, - "maxLength": 200 - }, - "minItems": 1, - "maxItems": 10, - "uniqueItems": true - } + "required": [ + "title", + "description", + "deliverables", + "acceptanceCriteria", + "evidenceOfCompletion", + "timeline", + "budget" + ] }, - "required": ["communityBenefit", "metrics", "outputs"] + "minItems": 3, + "maxItems": 10 }, - "capability": { + "finalMilestone": { "type": "object", - "title": "Capability & Feasibility", - "description": "Demonstrate your ability to deliver the project successfully", + "title": "Final Milestone", + "description": "Project close-out milestone with final report and video", "properties": { - "teamExperience": { - "type": "string", - "title": "Team Experience", - "description": "Describe your team's relevant experience and capabilities", - "minLength": 100, - "maxLength": 1000 - }, - "feasibilityApproach": { - "type": "string", - "title": "Feasibility Approach", - "description": "How will you validate the feasibility of your approach?", - "minLength": 100, - "maxLength": 1000 - }, - "riskMitigation": { - "type": "array", - "title": "Risk Mitigation", - "description": "Key risks and mitigation strategies", - "items": { - "type": "object", - "properties": { - "risk": { + "closeoutReport": { + "type": "object", + "title": "Project Close-out Report", + "properties": { + "summary": { + "type": "string", + "title": "Project Summary", + "description": "Overall summary of project achievements", + "minLength": 100, + "maxLength": 2000 + }, + "keyAchievements": { + "type": "array", + "title": "Key Achievements", + "items": { "type": "string", - "title": "Risk Description", "minLength": 10, "maxLength": 200 }, - "impact": { - "type": "string", - "enum": ["Low", "Medium", "High"], - "title": "Risk Impact" - }, - "mitigation": { + "minItems": 1, + "maxItems": 10 + }, + "lessonsLearned": { + "type": "array", + "title": "Lessons Learned", + "items": { "type": "string", - "title": "Mitigation Strategy", "minLength": 20, - "maxLength": 300 - } + "maxLength": 500 + }, + "minItems": 1, + "maxItems": 5 }, - "required": ["risk", "impact", "mitigation"] + "futureSteps": { + "type": "string", + "title": "Future Steps", + "description": "Plans for project continuation or future development", + "minLength": 50, + "maxLength": 1000 + } }, - "minItems": 1, - "maxItems": 5 + "required": ["summary", "keyAchievements", "lessonsLearned", "futureSteps"] }, - "fundManagement": { - "type": "string", - "title": "Fund Management", - "description": "How will you ensure proper management and accountability of funds?", - "minLength": 100, - "maxLength": 1000 + "demoVideo": { + "type": "object", + "title": "Project Demo Video", + "properties": { + "url": { + "type": "string", + "title": "Video URL", + "format": "uri", + "pattern": "^https://" + }, + "duration": { + "type": "integer", + "title": "Duration in Minutes", + "minimum": 3, + "maximum": 15 + }, + "description": { + "type": "string", + "title": "Video Description", + "minLength": 50, + "maxLength": 500 + } + }, + "required": ["url", "duration", "description"] } }, - "required": ["teamExperience", "feasibilityApproach", "riskMitigation", "fundManagement"] + "required": ["closeoutReport", "demoVideo"] } }, - "required": ["solution", "impact", "capability"] + "required": ["milestonesConfig", "milestonesList", "finalMilestone"] }, "finalPitch": { "type": "object", @@ -1350,13 +1351,16 @@ "type": "string", "title": "Fund Rules Version", "description": "Version of the Fund Rules being acknowledged", - "pattern": "^F[0-9]+$" + "pattern": "^F[0-9]{1,3}$", + "examples": ["F14", "F15"] }, "timestamp": { "type": "string", "title": "Acknowledgment Timestamp", "description": "When the rules were acknowledged", - "format": "date-time" + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": ["2024-01-20T15:30:00Z"] } }, "required": ["acknowledgment", "version", "timestamp"] @@ -1375,16 +1379,27 @@ "type": "string", "title": "Terms Version", "description": "Version of the Terms and Conditions being acknowledged", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", + "examples": ["1.0.0", "2.1.3"] + }, + "documentUrl": { + "type": "string", + "title": "Terms Document URL", + "description": "URL to the specific version of terms and conditions", + "format": "uri", + "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", + "contentMediaType": "text/html" }, "timestamp": { "type": "string", "title": "Acknowledgment Timestamp", "description": "When the terms were acknowledged", - "format": "date-time" + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": ["2024-01-20T15:30:00Z"] } }, - "required": ["acknowledgment", "version", "timestamp"] + "required": ["acknowledgment", "version", "timestamp", "documentUrl"] }, "privacyPolicy": { "type": "object", @@ -1400,16 +1415,27 @@ "type": "string", "title": "Privacy Policy Version", "description": "Version of the Privacy Policy being acknowledged", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", + "examples": ["1.0.0", "2.1.3"] + }, + "documentUrl": { + "type": "string", + "title": "Privacy Policy URL", + "description": "URL to the specific version of privacy policy", + "format": "uri", + "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", + "contentMediaType": "text/html" }, "timestamp": { "type": "string", "title": "Acknowledgment Timestamp", "description": "When the privacy policy was acknowledged", - "format": "date-time" + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": ["2024-01-20T15:30:00Z"] } }, - "required": ["acknowledgment", "version", "timestamp"] + "required": ["acknowledgment", "version", "timestamp", "documentUrl"] }, "intellectualProperty": { "type": "object", @@ -1425,13 +1451,46 @@ "type": "string", "title": "IP Details", "description": "Additional details about intellectual property rights (if applicable)", - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "attachments": { + "type": "array", + "title": "IP Documentation", + "description": "Supporting documentation for IP rights (if applicable)", + "items": { + "type": "object", + "properties": { + "documentType": { + "type": "string", + "enum": ["patent", "trademark", "copyright", "license", "other"], + "description": "Type of IP documentation" + }, + "documentUrl": { + "type": "string", + "format": "uri", + "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", + "contentMediaType": "application/pdf" + }, + "description": { + "type": "string", + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": ["documentType", "documentUrl", "description"] + }, + "maxItems": 10 }, "timestamp": { "type": "string", "title": "Acknowledgment Timestamp", "description": "When the IP declaration was made", - "format": "date-time" + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": ["2024-01-20T15:30:00Z"] } }, "required": ["acknowledgment", "timestamp"] @@ -1458,14 +1517,28 @@ "description": "I confirm that all information provided is accurate and complete", "const": true }, + "jurisdictions": { + "type": "array", + "title": "Applicable Jurisdictions", + "description": "List of jurisdictions where compliance is declared", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "ISO 3166-1 alpha-2 country code" + }, + "minItems": 1, + "uniqueItems": true + }, "timestamp": { "type": "string", "title": "Acknowledgment Timestamp", "description": "When the compliance declaration was made", - "format": "date-time" + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": ["2024-01-20T15:30:00Z"] } }, - "required": ["legalCompliance", "noConflictOfInterest", "accurateInformation", "timestamp"] + "required": ["legalCompliance", "noConflictOfInterest", "accurateInformation", "jurisdictions", "timestamp"] }, "additionalAcknowledgments": { "type": "array", @@ -1478,7 +1551,8 @@ "type": "string", "title": "Acknowledgment Type", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[a-zA-Z][a-zA-Z0-9_\\-\\.]*$" }, "acknowledgment": { "type": "boolean", @@ -1490,12 +1564,24 @@ "title": "Description", "description": "Detailed description of what is being acknowledged", "minLength": 10, - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "documentUrl": { + "type": "string", + "title": "Reference Document", + "description": "URL to the document being acknowledged", + "format": "uri", + "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", + "contentMediaType": "text/html" }, "timestamp": { "type": "string", "title": "Acknowledgment Timestamp", - "format": "date-time" + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": ["2024-01-20T15:30:00Z"] } }, "required": ["type", "acknowledgment", "description", "timestamp"] From 057ec964d2cf2968e25721cda5232ea9865e849b Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Wed, 4 Dec 2024 22:27:40 +0300 Subject: [PATCH 05/25] fix(cat-gateway): added better pattern per schema section of f14 template --- .../proposalTemplate.F14.schema.json | 286 +++++++++++++----- 1 file changed, 211 insertions(+), 75 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json index ce178642c8a..5cf34a23aae 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json @@ -28,7 +28,9 @@ "description": "A clear, unambiguous, and concise title for your proposal", "minLength": 1, "maxLength": 60, - "examples": ["DeFi Integration Platform for Cardano"] + "examples": ["DeFi Integration Platform for Cardano"], + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "additionalProperties": false, @@ -45,7 +47,9 @@ "description": "Name and surname of main applicant", "$comment": "Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).", "minLength": 2, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "applicant_type": { "type": "string", @@ -98,7 +102,7 @@ } } }, - "Time": { + "time": { "type": "object", "properties": { "project_duration": { @@ -112,7 +116,7 @@ }, "required": ["project_duration"] }, - "Translation": { + "translation": { "type": "object", "title": "Translation Information", "description": "Information about the proposal's language and translation status", @@ -129,13 +133,17 @@ "description": "If auto-translated, specify the original language of your proposal", "minLength": 2, "maxLength": 50, - "examples": ["Spanish", "Japanese", "French"] + "examples": ["Spanish", "Japanese", "French"], + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "translationNotes": { "type": "string", "title": "Translation Notes", "description": "Additional notes about the translation or original language content", - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["isTranslated"], @@ -150,7 +158,7 @@ "required": ["originalLanguage"] } }, - "Problem": { + "problem": { "type": "object", "title": "Problem Statement", "description": "Define the problem your proposal aims to solve", @@ -161,7 +169,9 @@ "description": "Clearly define the problem you aim to solve. This will be visible in the Catalyst voting app.", "minLength": 10, "maxLength": 200, - "examples": ["The Cardano ecosystem lacks standardized tools for cross-protocol communication, resulting in fragmented user experiences and inefficient resource utilization."] + "examples": ["The Cardano ecosystem lacks standardized tools for cross-protocol communication, resulting in fragmented user experiences and inefficient resource utilization."], + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "impactArea": { "type": "array", @@ -189,7 +199,7 @@ }, "required": ["statement", "impactArea"] }, - "Solution": { + "solution": { "type": "object", "title": "Solution Overview", "description": "Describe your proposed solution to the problem", @@ -200,13 +210,17 @@ "description": "Briefly describe your solution. Focus on what you will do or create to solve the problem.", "minLength": 10, "maxLength": 200, - "examples": ["Develop an open-source integration framework that standardizes protocol communication and provides a unified API layer for seamless DeFi interactions."] + "examples": ["Develop an open-source integration framework that standardizes protocol communication and provides a unified API layer for seamless DeFi interactions."], + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "approach": { "type": "string", "title": "Technical Approach", "description": "Outline the technical approach or methodology you will use", - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "innovationAspects": { "type": "array", @@ -214,7 +228,9 @@ "description": "Key innovative aspects of your solution", "items": { "type": "string", - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 1, "maxItems": 5, @@ -244,7 +260,9 @@ "examples": [ "https://github.com/your-org/project", "https://your-project-docs.com" - ] + ], + "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", + "contentMediaType": "text/uri-list" }, "type": { "type": "string", @@ -271,7 +289,9 @@ "examples": [ "Project's main GitHub repository containing all source code", "Technical whitepaper detailing the solution architecture" - ] + ], + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["url", "type", "description"] @@ -285,18 +305,22 @@ "format": "uri", "pattern": "^https://(github\\.com|gitlab\\.com|bitbucket\\.org)/.*", "title": "Main Code Repository", - "description": "Primary repository where the project's code will be hosted" + "description": "Primary repository where the project's code will be hosted", + "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", + "contentMediaType": "text/uri-list" }, "documentation": { "type": "string", "format": "uri", "pattern": "^https://.*", "title": "Documentation URL", - "description": "Main documentation site or resource for the project" + "description": "Main documentation site or resource for the project", + "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", + "contentMediaType": "text/uri-list" } } }, - "Dependencies": { + "dependencies": { "type": "object", "title": "Project Dependencies", "description": "External dependencies and requirements for project success", @@ -318,7 +342,9 @@ "type": "string", "title": "Dependency Name", "description": "Name of the organization, technology, or resource", - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "type": { "type": "string", @@ -336,13 +362,17 @@ "type": "string", "title": "Description", "description": "Explain why this dependency is essential and how it affects your project", - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "mitigationPlan": { "type": "string", "title": "Mitigation Plan", "description": "How will you handle potential issues with this dependency", - "maxLength": 300 + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["name", "type", "description"] @@ -365,7 +395,7 @@ } }, - "Horizons": { + "horizons": { "type": "object", "title": "Project Horizons", "description": "Long-term vision and categorization of your project", @@ -493,7 +523,9 @@ "items": { "type": "string", "minLength": 2, - "maxLength": 30 + "maxLength": 30, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 1, "maxItems": 5, @@ -535,19 +567,25 @@ "type": "string", "title": "Metric Name", "description": "Name of the metric", - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "target": { "type": "string", "title": "Target Value", "description": "Target value or goal for this metric", - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "measurement": { "type": "string", "title": "Measurement Method", "description": "How this metric will be measured", - "maxLength": 200 + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["metric", "target", "measurement"] @@ -579,14 +617,18 @@ "maxLength": 2000, "examples": [ "Our solution involves developing a decentralized education platform that will..." - ] + ], + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "uniqueValue": { "type": "string", "title": "Unique Value Proposition", "description": "What makes your solution unique and innovative?", "minLength": 50, - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "targetAudience": { "type": "array", @@ -595,7 +637,9 @@ "items": { "type": "string", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 1, "maxItems": 5, @@ -606,7 +650,9 @@ "title": "Implementation Approach", "description": "How will you implement your solution?", "minLength": 100, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["description", "uniqueValue", "targetAudience", "implementation"] @@ -621,7 +667,9 @@ "title": "Community Benefit", "description": "How will the Cardano community benefit from your project?", "minLength": 100, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "metrics": { "type": "array", @@ -634,25 +682,33 @@ "type": "string", "title": "Metric Name", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "description": { "type": "string", "title": "Metric Description", "minLength": 20, - "maxLength": 300 + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "target": { "type": "string", "title": "Target Value", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "measurement": { "type": "string", "title": "Measurement Method", "minLength": 20, - "maxLength": 300 + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["name", "description", "target", "measurement"] @@ -667,7 +723,9 @@ "items": { "type": "string", "minLength": 10, - "maxLength": 200 + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 1, "maxItems": 10, @@ -686,14 +744,18 @@ "title": "Team Experience", "description": "Describe your team's relevant experience and capabilities", "minLength": 100, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "feasibilityApproach": { "type": "string", "title": "Feasibility Approach", "description": "How will you validate the feasibility of your approach?", "minLength": 100, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "riskMitigation": { "type": "array", @@ -706,7 +768,9 @@ "type": "string", "title": "Risk Description", "minLength": 10, - "maxLength": 200 + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "impact": { "type": "string", @@ -717,7 +781,9 @@ "type": "string", "title": "Mitigation Strategy", "minLength": 20, - "maxLength": 300 + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["risk", "impact", "mitigation"] @@ -730,7 +796,9 @@ "title": "Fund Management", "description": "How will you ensure proper management and accountability of funds?", "minLength": 100, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["teamExperience", "feasibilityApproach", "riskMitigation", "fundManagement"] @@ -746,7 +814,7 @@ "milestonesConfig": { "type": "object", "title": "Milestones Configuration", - "description": "Configuration for number of milestones based on grant amount", + "description": "Configuration for number of milestones", "properties": { "grantAmount": { "type": "number", @@ -778,14 +846,18 @@ "title": "Milestone Title", "description": "Short, descriptive title for the milestone", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "description": { "type": "string", "title": "Milestone Description", "description": "Detailed description of what this milestone entails", "minLength": 50, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "deliverables": { "type": "array", @@ -798,13 +870,17 @@ "type": "string", "title": "Deliverable Name", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "description": { "type": "string", "title": "Deliverable Description", "minLength": 20, - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "type": { "type": "string", @@ -831,7 +907,9 @@ "items": { "type": "string", "minLength": 10, - "maxLength": 200 + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 1, "maxItems": 5 @@ -860,7 +938,9 @@ "type": "string", "title": "Evidence Description", "minLength": 20, - "maxLength": 300 + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["type", "description"] @@ -923,7 +1003,9 @@ "description": { "type": "string", "minLength": 10, - "maxLength": 200 + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["category", "amount", "description"] @@ -961,7 +1043,9 @@ "title": "Project Summary", "description": "Overall summary of project achievements", "minLength": 100, - "maxLength": 2000 + "maxLength": 2000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "keyAchievements": { "type": "array", @@ -969,7 +1053,9 @@ "items": { "type": "string", "minLength": 10, - "maxLength": 200 + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 1, "maxItems": 10 @@ -980,7 +1066,9 @@ "items": { "type": "string", "minLength": 20, - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 1, "maxItems": 5 @@ -990,7 +1078,9 @@ "title": "Future Steps", "description": "Plans for project continuation or future development", "minLength": 50, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["summary", "keyAchievements", "lessonsLearned", "futureSteps"] @@ -1003,7 +1093,9 @@ "type": "string", "title": "Video URL", "format": "uri", - "pattern": "^https://" + "pattern": "^https://", + "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", + "contentMediaType": "text/uri-list" }, "duration": { "type": "integer", @@ -1015,7 +1107,9 @@ "type": "string", "title": "Video Description", "minLength": 50, - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["url", "duration", "description"] @@ -1048,14 +1142,18 @@ "title": "Name", "description": "Full name of the team member", "minLength": 2, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "role": { "type": "string", "title": "Role", "description": "Primary role in the project", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "expertise": { "type": "array", @@ -1063,7 +1161,9 @@ "items": { "type": "string", "minLength": 3, - "maxLength": 50 + "maxLength": 50, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 1, "maxItems": 5, @@ -1074,7 +1174,9 @@ "title": "Relevant Experience", "description": "Brief description of relevant experience", "minLength": 50, - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "links": { "type": "array", @@ -1097,11 +1199,15 @@ "url": { "type": "string", "format": "uri", - "pattern": "^https://" + "pattern": "^https://", + "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", + "contentMediaType": "text/uri-list" }, "description": { "type": "string", - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["type", "url"] @@ -1119,7 +1225,9 @@ "title": "Team Capabilities", "description": "Overview of the team's collective capabilities and why they are best suited for this project", "minLength": 100, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "previousWork": { "type": "array", @@ -1132,25 +1240,33 @@ "type": "string", "title": "Project Name", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "description": { "type": "string", "title": "Project Description", "minLength": 50, - "maxLength": 500 + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "relevance": { "type": "string", "title": "Relevance to Current Proposal", "minLength": 50, - "maxLength": 300 + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "url": { "type": "string", "title": "Project URL", "format": "uri", - "pattern": "^https://" + "pattern": "^https://", + "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", + "contentMediaType": "text/uri-list" } }, "required": ["projectName", "description", "relevance"] @@ -1199,7 +1315,9 @@ "description": { "type": "string", "minLength": 20, - "maxLength": 300 + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "breakdown": { "type": "array", @@ -1209,7 +1327,9 @@ "item": { "type": "string", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "cost": { "type": "number", @@ -1218,7 +1338,9 @@ "justification": { "type": "string", "minLength": 20, - "maxLength": 200 + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["item", "cost", "justification"] @@ -1242,7 +1364,9 @@ "milestone": { "type": "string", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "amount": { "type": "number", @@ -1274,7 +1398,9 @@ "title": "Cost-Benefit Analysis", "description": "Analysis of the project's costs versus its benefits to the Cardano ecosystem", "minLength": 100, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "impactMetrics": { "type": "array", @@ -1287,19 +1413,25 @@ "type": "string", "title": "Metric Name", "minLength": 5, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "target": { "type": "string", "title": "Target Value", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "justification": { "type": "string", "title": "Value Justification", "minLength": 50, - "maxLength": 300 + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" } }, "required": ["metric", "target", "justification"] @@ -1312,7 +1444,9 @@ "title": "Long-term Value", "description": "Description of the long-term value and sustainability of the project", "minLength": 100, - "maxLength": 1000 + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "communityBenefits": { "type": "array", @@ -1321,7 +1455,9 @@ "items": { "type": "string", "minLength": 20, - "maxLength": 200 + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" }, "minItems": 2, "maxItems": 5 From 68cb23b48f47923c483325b82bcda1bf5fe4b7b5 Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Wed, 4 Dec 2024 22:44:17 +0300 Subject: [PATCH 06/25] fix(cat-gateway): json types and syntax update --- .../F14-Generic/example.proposal.json | 6 +- .../proposalTemplate.F14.schema.json | 157 ++++++++---------- 2 files changed, 69 insertions(+), 94 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json index bd5def1aaa7..6c1318570dd 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json @@ -6,8 +6,8 @@ }, "proposer": { "mainApplicant": "John Smith", - "applicant_type": "Entity (Incorporated)", - "co-proposers": ["Alice Johnson", "Bob Wilson"] + "applicantType": "Entity (Incorporated)", + "coProposers": ["Alice Johnson", "Bob Wilson"] } }, "proposalSummary": { @@ -27,7 +27,7 @@ "horizons": { "category": { "primaryCategory": "DeFi", - "subCategory": "Integration Tools" + "subCategory": "Developer Tools" }, "tags": ["defi", "integration", "developer-tools", "smart-contracts"] }, diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json index 5cf34a23aae..2f75889e10a 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json @@ -51,7 +51,7 @@ "pattern": "^[\\S\\s]*$", "contentMediaType": "text/plain" }, - "applicant_type": { + "applicantType": { "type": "string", "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", @@ -62,8 +62,8 @@ "Entity (Not Incorporated)" ] }, - "co-proposers": { - "type": "string", + "coProposers": { + "type": "array", "title": "Co-proposers and additional applicants", "description": "Co-proposers and additional applicants", "$comment": "List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 13 rules for added detail.", @@ -105,7 +105,7 @@ "time": { "type": "object", "properties": { - "project_duration": { + "projectDuration": { "type": "integer", "title": "Project Duration in Months", "description": "Specify the expected duration of your project. Projects must be completable within 2-12 months.", @@ -114,7 +114,7 @@ "examples": ["Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out."] } }, - "required": ["project_duration"] + "required": ["projectDuration"] }, "translation": { "type": "object", @@ -428,91 +428,67 @@ "type": "string", "title": "Sub-category", "description": "Specific area within the main category", - "enum": { - "Governance": [ - "DAO", - "Voting", - "Treasury Management" - ], - "Education": [ - "Learn to Earn", - "Training", - "Translation" - ], - "Community & Outreach": [ - "Connected Community", - "Social Media", - "Community Building" - ], - "Development & Tools": [ - "Developer Tools", - "L2 Infrastructure", - "Analytics", - "AI Research", - "UTXO", - "P2P" - ], - "Identity & Security": [ - "Identity & Verification", - "Cybersecurity", - "Authentication", - "Privacy" - ], - "DeFi": [ - "Payments", - "Stablecoin", - "Risk Management", - "Yield", - "Staking", - "Lending" - ], - "Real World Applications": [ - "Wallet", - "Marketplace", - "Manufacturing", - "IoT", - "Financial Services", - "E-commerce", - "Business Services", - "Supply Chain", - "Real Estate", - "Healthcare", - "Tourism", - "Entertainment", - "RWA", - "Music", - "Tokenization" - ], - "Events & Marketing": [ - "Events", - "Marketing", - "Hackathons", - "Accelerator", - "Incubator" - ], - "Interoperability": [ - "Cross-chain", - "Off-chain", - "Bridges" - ], - "Legal & Policy": [ - "Policy", - "Advocacy", - "Standards", - "Compliance" - ], - "Sustainability": [ - "Environment", - "Agriculture", - "Clean Energy" - ], - "Smart Contracts": [ - "Development", - "Security", - "Templates", - "Auditing" - ] - } + "enum": [ + "DAO", + "Voting", + "Treasury Management", + "Learn to Earn", + "Training", + "Translation", + "Connected Community", + "Social Media", + "Community Building", + "Developer Tools", + "L2 Infrastructure", + "Analytics", + "AI Research", + "UTXO", + "P2P", + "Identity & Verification", + "Cybersecurity", + "Authentication", + "Privacy", + "Payments", + "Stablecoin", + "Risk Management", + "Yield", + "Staking", + "Lending", + "Wallet", + "Marketplace", + "Manufacturing", + "IoT", + "Financial Services", + "E-commerce", + "Business Services", + "Supply Chain", + "Real Estate", + "Healthcare", + "Tourism", + "Entertainment", + "RWA", + "Music", + "Tokenization", + "Events", + "Marketing", + "Hackathons", + "Accelerator", + "Incubator", + "Cross-chain", + "Off-chain", + "Bridges", + "Policy", + "Advocacy", + "Standards", + "Compliance", + "Environment", + "Agriculture", + "Clean Energy", + "Development", + "Security", + "Templates", + "Auditing" + ] } } }, @@ -884,7 +860,6 @@ }, "type": { "type": "string", - "title": "Deliverable Type", "enum": [ "Documentation", "Software", From 4efb65c36629869e27035ee14f227e491a3ee5d6 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Thu, 5 Dec 2024 12:10:50 +0700 Subject: [PATCH 07/25] docs(cat-gateway): add example of common field types to help UI render --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 152 ++++++++++-------- .../F14-Generic/proposal.F14.example.json | 6 +- 2 files changed, 88 insertions(+), 70 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 07bcf365c27..63abfbcede2 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -2,6 +2,55 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Catalyst Fund 14 Base Proposal Template", "description": "A structured template for creating Fund 14 proposals", + "definitions": { + "singleLineTextEntry": { + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^.*$" + }, + "multiLineTextEntry": { + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$" + }, + "dropDownSingleSelect": { + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "format": "dropDownSingleSelect" + }, + "tokenValueCardanoADA": { + "type": "integer", + "format": "token:cardano:ada" + }, + "durationInMonths": { + "type": "integer", + "format": "datetime:duration:months" + }, + "agreementConfirmation": { + "type": "boolean", + "format": "checkbox", + "default": false, + "const": true + }, + "yesNoChoice": { + "type": "boolean", + "format": "yes/no", + "default": false + }, + "uriList": { + "type": "array", + "format": "uriList", + "uniqueItems": true, + "default": [], + "items": { + "type": "string", + "format": "uri", + "contentMediaType": "text/plain", + "maxLength": 1024 + } + } + }, "type": "object", "properties": { "$schema": { @@ -14,41 +63,23 @@ "type": "object", "properties": { "title": { - "type": "string", + "$ref": "#/definitions/singleLineTextEntry", "title": "Proposal title", "description": "

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.


The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

", - "contentMediaType": "text/plain", - "pattern": "^.*$", "maxLength": 80, "minLength": 0 }, - { - "email": { - "type": "string", - "title": "Email", - "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", - "contentMediaType": "text/plain", - "format": "email", - "pattern": "^.*$", - "maxLength": 80, - "minLength": 0 - }, - - }, "applicant": { - "type": "string", + "$ref": "#/definitions/singleLineTextEntry", "title": "Name and surname of main applicant", "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", - "contentMediaType": "text/plain", - "pattern": "^.*$", "maxLength": 80, "minLength": 0 }, "applicant_type": { - "type": "string", + "$ref": "#/definitions/dropDownSingleSelect", "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", "description": "

Please select from one of the following:

", - "contentMediaType": "text/plain", "enum": [ "Individual", "Entity (Incorporated)", @@ -57,90 +88,68 @@ "default": "Individual" }, "co-proposers": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "Co-proposers and additional applicants", "description": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals / accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers.


IMPORTANT - A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund13 rules for added detail.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", "maxLength": 1024, "minLength": 0 }, "requested_funds": { - "type": "integer", + "$ref": "#/definitions/tokenValueCardanoADA", "title": "Requested funds in ada", "description": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:


Minimum Funding Amount per proposal:

  • Cardano Open: ₳15,000
  • Cardano Uses Cases: ₳15,000
  • Cardano Partners: ₳500,000


Maximum Funding Amount per proposal:

  • Cardano Open: 
  • Developers (technical): ₳200,000
  • Ecosystem (non-technical): ₳100,000
  • Cardano Uses Cases:
  • Concept: ₳150,000
  • Product: ₳500,000
  • Cardano Partners:
  • Enterprise R&D: ₳2,000,000 
  • Growth & Acceleration: ₳2,000,000
", "minimum": 1, - "maximum": 18446744073709551615, - "format": "cardano:ada" + "maximum": 18446744073709551615 }, "duration": { - "type": "integer", + "$ref": "#/definitions/durationInMonths", "title": "Please specify how many months you expect your project to last (from 2-12 months)", "description": "

Minimum 2 months - Maximum 12 months.


The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months.


If you believe your project will take longer than 12 months, consider reducing the project’s scope so that it becomes achievable within 12 months.


If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", "minimum": 2, - "maximum": 12, - "format": "datetime:months" + "maximum": 12 }, "translated": { - "type": "boolean", + "$ref": "#/definitions/yesNoChoice", "title": "Please indicate if your proposal has been auto-translated into English from another language", - "description": "

YES/NO - Tick YES so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections.


You can either link a document with your proposal in its original language OR provide your response in your native language after the English language in each question if you wish.


Tick NO if your proposal has not been auto-translated into English from another language.

", - "format": "yes/no" + "description": "

YES/NO - Tick YES so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections.


You can either link a document with your proposal in its original language OR provide your response in your native language after the English language in each question if you wish.


Tick NO if your proposal has not been auto-translated into English from another language.

" }, "problem": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "What is the problem you want to solve? (200-character limit including spaces)", "description": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", "maxLength": 200, "minLength": 1 }, "solution": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "Summarize your solution to the problem (200-character limit including spaces)", "description": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a...'


Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how'.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", "maxLength": 200, "minLength": 1 }, "links": { - "type": "array", + "$ref": "#/definitions/uriList", "title": "Website / GitHub repository, White paper, Marketing or any other relevant link", "description": "

Here, provide links to yours or your partner organization’s website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal.


Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere.


If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include ‘https’. Without these steps, the form will not be submittable and show errors.

", - "items": { - "type": "string", - "format": "uri", - "contentMediaType": "text/plain", - "maxLength": 1024 - }, - "uniqueItems": true, - "default": [], "minItems": 0, "maxItems": 3 }, "dependencies": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "If you have any dependencies then, please describe what the dependency is and why you believe it is essential for your project’s delivery. If NO, please write “No dependencies.”", "description": "

Here you should list any dependencies and prerequisites for your project’s success. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", "maxLength": 1024, "minLength": 0 }, "open_source": { - "type": "boolean", + "$ref": "#/definitions/yesNoChoice", "title": "Will your project’s output/s be fully open source?", - "description": "

Open source refers to something people can modify and share because its design is publicly accessible. 


Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software.

", - "format": "yes/no" + "description": "

Open source refers to something people can modify and share because its design is publicly accessible. 


Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software.

" }, "license_info": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "[GENERAL] Please provide here more information on the open source status of your project outputs", "description": "

If you answered YES to the above question:


If declaring the project is open source in the application form, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository.


Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs. 


If only certain elements of your code will be open source please clarify which elements will be open source here. 


If you answered NO to the above question, please give further details as to why your projects outputs will not be open source.

", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$", "maxLength": 1024, "minLength": 0 } @@ -237,19 +246,26 @@ "type": "object", "properties": { "fund_rules": { - "type": "string", + "$ref": "#/definitions/agreementConfirmation", "title": "Fund Rules:", - "description": "

By submitting a proposal to Project Catalyst Fund13, I confirm that I have read and agree to be bound by the Fund Rules.

", - "contentMediaType": "text/plain", - "enum": [ - "Yes", - "No" - ], - "default": "No", - "pattern": "Yes", - "format": "checkbox" + "description": "

By submitting a proposal to Project Catalyst Fund13, I confirm that I have read and agree to be bound by the Fund Rules.

" + }, + "terms_and_conditions": { + "$ref": "#/definitions/agreementConfirmation", + "title": "Terms and Conditions:", + "description": "

By submitting a proposal to Project Catalyst Fund13, I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions.

" + }, + "privacy_policy": { + "$ref": "#/definitions/agreementConfirmation", + "title": "Privacy Policy: ", + "description": "

I acknowledge and agree that any data I share in connection with my participation in Project Catalyst Fund13 will be collected, stored, used and processed in accordance with the Catalyst FC’s Privacy Policy.

" } - } + }, + "required": [ + "fund_rules", + "terms_and_conditions", + "privacy_policy" + ] } } } \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json index d77c676cb9e..13813ee0f09 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposal.F14.example.json @@ -1,7 +1,7 @@ { "$schema": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", "general": { - "title": "Single line plain text ..............................", + "title": "Single line plain text .....", "links": [ "http://notauri", "ftp://some_old_site", @@ -14,6 +14,8 @@ "tag": "Learn to Earn" }, "agreements": { - "fund_rules": "Yes" + "fund_rules": true, + "terms_and_conditions": true, + "privacy_policy": true } } \ No newline at end of file From e89574f21f2e0f13bb2839fa6d788a7036930da8 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Thu, 5 Dec 2024 13:22:57 +0700 Subject: [PATCH 08/25] docs(cat-gateway): Suggested restructuring --- .config/dictionaries/project.dic | 1 + ...8-9258-4fbc-a62e-7faa6e58318f.schema.json} | 577 +++++++++++++----- .../F14-Generic/example.proposal.json | 2 +- .../proposalTemplate.F14.example.json | 0 4 files changed, 436 insertions(+), 144 deletions(-) rename docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/{proposalTemplate.F14.schema.json => 0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json} (80%) delete mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.example.json diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 4b146db4d44..517820b0998 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -56,6 +56,7 @@ codepoints collabs commitlog concatcp +coproposers coti coverallsapp CQLSH diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json similarity index 80% rename from docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json rename to docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 2f75889e10a..b55c82a2a36 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -1,184 +1,266 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cardano.org/schemas/catalyst/f14/proposal", - "version": "1.0.0", "title": "F14 Submission Form", "description": "Schema for the F14 Catalyst Proposal Submission Form", - "type": "object", - "properties": { - "$schema": { + "definitions": { + "schemaReferenceNonUI": { + "$comment": "NOT UI: used to identify the kind of template document used.", "type": "string", "format": "path", - "const": "./proposalTemplate.F14.schema.json", "readOnly": true }, - "setup": { + "section": { + "$comment": "UI - Logical Document Section Break.", + "type": "object", + "additionalProperties": false + }, + "subsection": { + "$comment": "UI - Logical Document Sub-Section Break.", "type": "object", + "additionalProperties": false + }, + "singleLineTextEntry": { + "$comment": "UI - Single Line text entry without any markup or rich text capability.", + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^.*$" + }, + "multiLineTextEntry": { + "$comment": "UI - Multiline text entry without any markup or rich text capability.", + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]$" + }, + "dropDownSingleSelect": { + "$comment": "UI - Drop Down Selection of a single entry from the defined enum.", + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "format": "dropDownSingleSelect" + }, + "multiSelect": { + "$comment": "UI - Multiselect from the given items.", + "type": "array", + "uniqueItems": true, + "format": "multiSelect" + }, + "singleLineTextEntryList": { + "$comment": "UI - A Growable List of single line text (no markup or richtext).", + "type": "array", + "format": "singleLineTextEntryList", + "uniqueItems": true, + "default": [], + "items": { + "$ref": "#/definitions/singleLineTextEntry", + "maxLength": 1024 + } + }, + "tokenValueCardanoADA": { + "$comment": "UI - A Token Value denominated in Cardano ADA.", + "type": "integer", + "format": "token:cardano:ada" + }, + "durationInMonths": { + "$comment": "UI - A Duration represented in total months.", + "type": "integer", + "format": "datetime:duration:months" + }, + "yesNoChoice": { + "$comment": "UI - A Boolean choice, represented as a Yes/No selection. Yes = true.", + "type": "boolean", + "format": "yes/no", + "default": false + } + }, + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "$ref": "#/definitions/schemaReferenceNonUI", + "default": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", + "const": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json" + }, + "setup": { + "$ref": "#/definitions/section", "title": "proposal setup", "description": "Proposal title", "properties": { "title": { - "type": "object", + "$ref": "#/definitions/subsection", "title": "proposal setup", "description": "Proposal title", "properties": { "title": { - "type": "string", + "$ref": "#/definitions/singleLineTextEntry", "title": "Proposal Title", "description": "A clear, unambiguous, and concise title for your proposal", "minLength": 1, "maxLength": 60, - "examples": ["DeFi Integration Platform for Cardano"], - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "examples": [ + "DeFi Integration Platform for Cardano" + ] } }, - "additionalProperties": false, "required": [ "title" ] }, "proposer": { - "type": "object", + "$ref": "#/definitions/subsection", "properties": { - "mainApplicant": { - "type": "string", + "applicant": { + "$ref": "#/definitions/singleLineTextEntry", "title": "Name and surname of main applicant", "description": "Name and surname of main applicant", - "$comment": "Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).", + "x-guidance": "Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).", "minLength": 2, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "maxLength": 100 }, - "applicantType": { - "type": "string", + "type": { + "$ref": "#/definitions/dropDownSingleSelect", "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", - "$comment": "Please select from one of the following: 1. Individual 2. Entity (Incorporated) 3. Entity (Not Incorporated)", + "x-guidance": "Please select from one of the following: 1. Individual 2. Entity (Incorporated) 3. Entity (Not Incorporated)", "enum": [ "Individual", "Entity (Incorporated)", "Entity (Not Incorporated)" - ] + ], + "default": "Individual" }, - "coProposers": { - "type": "array", + "coproposers": { + "$ref": "#/definitions/singleLineTextEntryList", "title": "Co-proposers and additional applicants", "description": "Co-proposers and additional applicants", - "$comment": "List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 13 rules for added detail.", - "items": { - "type": "string" - }, - "maxItems": 5 + "x-guidance": "List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 13 rules for added detail.", + "maxItems": 5, + "minItems": 0 } }, - "required": ["mainApplicant", "applicantType"] + "required": [ + "applicant", + "type" + ] } }, - "required": ["title", "proposer"] + "required": [ + "title", + "proposer" + ] }, - "proposalSummary": { - "type": "object", + "summary": { + "$ref": "#/definitions/section", "title": "Proposal Summary", "description": "Key information about your proposal", "properties": { "budget": { - "type": "object", + "$ref": "#/definitions/subsection", "title": "Budget Information", "properties": { "requestedFunds": { - "type": "number", + "$ref": "#/definitions/tokenValueCardanoADA", "title": "Requested funds in ADA", "description": "The amount of funding requested for your proposal", + "x-guidance": "There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category: Minimum Funding Amount per proposal: Cardano Open: A15,000 Cardano Uses Cases: A15,000 Cardano Partners: A500,000 Maximum Funding Amount per proposal: Cardano Open: Developers (technical): A200,000 • Ecosystem (non-technical): A100,000 Cardano Uses Cases: Concept A150,000 Product: A500,000 Cardano Partners: Enterprise R&D A2,000,000 Growth & Acceleration: A2,000,000", "minimum": 15000, - "maximum": 2000000, - "examples": ["There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category: Minimum Funding Amount per proposal: Cardano Open: A15,000 Cardano Uses Cases: A15,000 Cardano Partners: A500,000 Maximum Funding Amount per proposal: Cardano Open: Developers (technical): A200,000 • Ecosystem (non-technical): A100,000 Cardano Uses Cases: Concept A150,000 Product: A500,000 Cardano Partners: Enterprise R&D A2,000,000 Growth & Acceleration: A2,000,000"], - "format": "cardano:ada", - "errorMessage": { - "minimum": "Minimum funding amount is 15,000 ADA", - "maximum": "Maximum funding amount is 2,000,000 ADA" - } + "maximum": 2000000 } - } + }, + "required": [ + "requestedFunds" + ] }, "time": { - "type": "object", + "$ref": "#/definitions/subsection", "properties": { - "projectDuration": { - "type": "integer", + "duration": { + "$ref": "#/definitions.durationInMonths", "title": "Project Duration in Months", "description": "Specify the expected duration of your project. Projects must be completable within 2-12 months.", + "x-guidance": "Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.", "minimum": 2, - "maximum": 12, - "examples": ["Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out."] + "maximum": 12 } }, - "required": ["projectDuration"] + "required": [ + "projectDuration" + ] }, "translation": { - "type": "object", + "$ref": "#/definitions/subsection", "title": "Translation Information", "description": "Information about the proposal's language and translation status", "properties": { - "isTranslated": { - "type": "boolean", + "translated": { + "$ref": "#/definitions/yesNoChoice", "title": "Auto-translated Status", - "description": "Indicate if your proposal has been auto-translated into English from another language", - "default": false + "description": "Indicate if your proposal has been auto-translated into English from another language" }, - "originalLanguage": { - "type": "string", + "original": { + "$ref": "#/definitions/singleLineTextEntry", "title": "Original Language", "description": "If auto-translated, specify the original language of your proposal", "minLength": 2, "maxLength": 50, - "examples": ["Spanish", "Japanese", "French"], - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "examples": [ + "Spanish", + "Japanese", + "French" + ] }, - "translationNotes": { - "type": "string", + "notes": { + "$ref": "#/definitions/multiLineTextEntry", "title": "Translation Notes", "description": "Additional notes about the translation or original language content", - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "maxLength": 500 } }, - "required": ["isTranslated"], + "required": [ + "isTranslated" + ], "dependencies": { - "originalLanguage": ["isTranslated"], - "translationNotes": ["isTranslated"] + "originalLanguage": [ + "isTranslated" + ], + "translationNotes": [ + "isTranslated" + ] }, "if": { - "properties": { "isTranslated": { "const": true } } + "properties": { + "isTranslated": { + "const": true + } + } }, "then": { - "required": ["originalLanguage"] + "required": [ + "originalLanguage" + ] } }, "problem": { - "type": "object", + "$ref": "#/definitions/subsection", "title": "Problem Statement", "description": "Define the problem your proposal aims to solve", "properties": { "statement": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "Problem Description", "description": "Clearly define the problem you aim to solve. This will be visible in the Catalyst voting app.", "minLength": 10, "maxLength": 200, - "examples": ["The Cardano ecosystem lacks standardized tools for cross-protocol communication, resulting in fragmented user experiences and inefficient resource utilization."], - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "examples": [ + "The Cardano ecosystem lacks standardized tools for cross-protocol communication, resulting in fragmented user experiences and inefficient resource utilization." + ] }, - "impactArea": { - "type": "array", + "impact": { + "$ref": "#/definitions/multiSelect", "title": "Impact Areas", "description": "Select the areas that will be most impacted by solving this problem", "items": { - "type": "string", + "$ref": "singleLineTextEntry", "enum": [ "Technical Infrastructure", "User Experience", @@ -193,26 +275,28 @@ ] }, "minItems": 1, - "maxItems": 3, - "uniqueItems": true + "maxItems": 3 } }, - "required": ["statement", "impactArea"] + "required": [ + "statement", + "impact" + ] }, "solution": { - "type": "object", + "$ref": "#/definitions/subsection", "title": "Solution Overview", "description": "Describe your proposed solution to the problem", "properties": { "summary": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "Solution Summary", "description": "Briefly describe your solution. Focus on what you will do or create to solve the problem.", "minLength": 10, "maxLength": 200, - "examples": ["Develop an open-source integration framework that standardizes protocol communication and provides a unified API layer for seamless DeFi interactions."], - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "examples": [ + "Develop an open-source integration framework that standardizes protocol communication and provides a unified API layer for seamless DeFi interactions." + ] }, "approach": { "type": "string", @@ -237,7 +321,10 @@ "uniqueItems": true } }, - "required": ["summary", "approach"] + "required": [ + "summary", + "approach" + ] }, "SupportingLinks": { "type": "object", @@ -294,7 +381,11 @@ "contentMediaType": "text/plain" } }, - "required": ["url", "type", "description"] + "required": [ + "url", + "type", + "description" + ] }, "minItems": 0, "maxItems": 10, @@ -375,26 +466,39 @@ "contentMediaType": "text/plain" } }, - "required": ["name", "type", "description"] + "required": [ + "name", + "type", + "description" + ] }, "minItems": 0, "maxItems": 10 } }, - "required": ["hasDependencies"], + "required": [ + "hasDependencies" + ], "dependencies": { - "details": ["hasDependencies"] + "details": [ + "hasDependencies" + ] }, "if": { - "properties": { "hasDependencies": { "const": true } } + "properties": { + "hasDependencies": { + "const": true + } + } }, "then": { - "required": ["details"] + "required": [ + "details" + ] } } } }, - "horizons": { "type": "object", "title": "Project Horizons", @@ -564,16 +668,29 @@ "contentMediaType": "text/plain" } }, - "required": ["metric", "target", "measurement"] + "required": [ + "metric", + "target", + "measurement" + ] }, "minItems": 1, "maxItems": 5 } }, - "required": ["timeframe", "scale", "metrics"] + "required": [ + "timeframe", + "scale", + "metrics" + ] } }, - "required": ["primaryCategory", "subCategory", "tags", "impact"] + "required": [ + "primaryCategory", + "subCategory", + "tags", + "impact" + ] }, "proposalDetails": { "type": "object", @@ -631,7 +748,12 @@ "contentMediaType": "text/plain" } }, - "required": ["description", "uniqueValue", "targetAudience", "implementation"] + "required": [ + "description", + "uniqueValue", + "targetAudience", + "implementation" + ] }, "impact": { "type": "object", @@ -687,7 +809,12 @@ "contentMediaType": "text/plain" } }, - "required": ["name", "description", "target", "measurement"] + "required": [ + "name", + "description", + "target", + "measurement" + ] }, "minItems": 2, "maxItems": 5 @@ -708,7 +835,11 @@ "uniqueItems": true } }, - "required": ["communityBenefit", "metrics", "outputs"] + "required": [ + "communityBenefit", + "metrics", + "outputs" + ] }, "capability": { "type": "object", @@ -750,7 +881,11 @@ }, "impact": { "type": "string", - "enum": ["Low", "Medium", "High"], + "enum": [ + "Low", + "Medium", + "High" + ], "title": "Risk Impact" }, "mitigation": { @@ -762,7 +897,11 @@ "contentMediaType": "text/plain" } }, - "required": ["risk", "impact", "mitigation"] + "required": [ + "risk", + "impact", + "mitigation" + ] }, "minItems": 1, "maxItems": 5 @@ -777,10 +916,19 @@ "contentMediaType": "text/plain" } }, - "required": ["teamExperience", "feasibilityApproach", "riskMitigation", "fundManagement"] + "required": [ + "teamExperience", + "feasibilityApproach", + "riskMitigation", + "fundManagement" + ] } }, - "required": ["solution", "impact", "capability"] + "required": [ + "solution", + "impact", + "capability" + ] }, "milestones": { "type": "object", @@ -807,7 +955,10 @@ "maximum": 10 } }, - "required": ["grantAmount", "numberOfMilestones"] + "required": [ + "grantAmount", + "numberOfMilestones" + ] }, "milestonesList": { "type": "array", @@ -870,7 +1021,11 @@ ] } }, - "required": ["name", "description", "type"] + "required": [ + "name", + "description", + "type" + ] }, "minItems": 1, "maxItems": 5 @@ -918,7 +1073,10 @@ "contentMediaType": "text/plain" } }, - "required": ["type", "description"] + "required": [ + "type", + "description" + ] }, "minItems": 1, "maxItems": 3 @@ -944,7 +1102,11 @@ "maximum": 52 } }, - "required": ["startDate", "endDate", "durationInWeeks"] + "required": [ + "startDate", + "endDate", + "durationInWeeks" + ] }, "budget": { "type": "object", @@ -983,12 +1145,19 @@ "contentMediaType": "text/plain" } }, - "required": ["category", "amount", "description"] + "required": [ + "category", + "amount", + "description" + ] }, "minItems": 1 } }, - "required": ["amount", "breakdown"] + "required": [ + "amount", + "breakdown" + ] } }, "required": [ @@ -1058,7 +1227,12 @@ "contentMediaType": "text/plain" } }, - "required": ["summary", "keyAchievements", "lessonsLearned", "futureSteps"] + "required": [ + "summary", + "keyAchievements", + "lessonsLearned", + "futureSteps" + ] }, "demoVideo": { "type": "object", @@ -1087,13 +1261,24 @@ "contentMediaType": "text/plain" } }, - "required": ["url", "duration", "description"] + "required": [ + "url", + "duration", + "description" + ] } }, - "required": ["closeoutReport", "demoVideo"] + "required": [ + "closeoutReport", + "demoVideo" + ] } }, - "required": ["milestonesConfig", "milestonesList", "finalMilestone"] + "required": [ + "milestonesConfig", + "milestonesList", + "finalMilestone" + ] }, "finalPitch": { "type": "object", @@ -1185,12 +1370,20 @@ "contentMediaType": "text/plain" } }, - "required": ["type", "url"] + "required": [ + "type", + "url" + ] }, "maxItems": 5 } }, - "required": ["name", "role", "expertise", "experience"] + "required": [ + "name", + "role", + "expertise", + "experience" + ] }, "minItems": 1, "maxItems": 10 @@ -1244,12 +1437,19 @@ "contentMediaType": "text/uri-list" } }, - "required": ["projectName", "description", "relevance"] + "required": [ + "projectName", + "description", + "relevance" + ] }, "maxItems": 5 } }, - "required": ["members", "teamCapabilities"] + "required": [ + "members", + "teamCapabilities" + ] }, "budget": { "type": "object", @@ -1318,12 +1518,21 @@ "contentMediaType": "text/plain" } }, - "required": ["item", "cost", "justification"] + "required": [ + "item", + "cost", + "justification" + ] }, "minItems": 1 } }, - "required": ["category", "amount", "description", "breakdown"] + "required": [ + "category", + "amount", + "description", + "breakdown" + ] }, "minItems": 1 }, @@ -1353,15 +1562,25 @@ "maximum": 100 } }, - "required": ["milestone", "amount", "percentage"] + "required": [ + "milestone", + "amount", + "percentage" + ] }, "minItems": 1 } }, - "required": ["distributionSchedule"] + "required": [ + "distributionSchedule" + ] } }, - "required": ["totalBudget", "categories", "timeline"] + "required": [ + "totalBudget", + "categories", + "timeline" + ] }, "valueProposition": { "type": "object", @@ -1409,7 +1628,11 @@ "contentMediaType": "text/plain" } }, - "required": ["metric", "target", "justification"] + "required": [ + "metric", + "target", + "justification" + ] }, "minItems": 2, "maxItems": 5 @@ -1438,10 +1661,19 @@ "maxItems": 5 } }, - "required": ["costBenefitAnalysis", "impactMetrics", "longTermValue", "communityBenefits"] + "required": [ + "costBenefitAnalysis", + "impactMetrics", + "longTermValue", + "communityBenefits" + ] } }, - "required": ["team", "budget", "valueProposition"] + "required": [ + "team", + "budget", + "valueProposition" + ] }, "mandatoryAcknowledgments": { "type": "object", @@ -1463,7 +1695,10 @@ "title": "Fund Rules Version", "description": "Version of the Fund Rules being acknowledged", "pattern": "^F[0-9]{1,3}$", - "examples": ["F14", "F15"] + "examples": [ + "F14", + "F15" + ] }, "timestamp": { "type": "string", @@ -1471,10 +1706,16 @@ "description": "When the rules were acknowledged", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": ["2024-01-20T15:30:00Z"] + "examples": [ + "2024-01-20T15:30:00Z" + ] } }, - "required": ["acknowledgment", "version", "timestamp"] + "required": [ + "acknowledgment", + "version", + "timestamp" + ] }, "termsAndConditions": { "type": "object", @@ -1491,7 +1732,10 @@ "title": "Terms Version", "description": "Version of the Terms and Conditions being acknowledged", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", - "examples": ["1.0.0", "2.1.3"] + "examples": [ + "1.0.0", + "2.1.3" + ] }, "documentUrl": { "type": "string", @@ -1507,10 +1751,17 @@ "description": "When the terms were acknowledged", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": ["2024-01-20T15:30:00Z"] + "examples": [ + "2024-01-20T15:30:00Z" + ] } }, - "required": ["acknowledgment", "version", "timestamp", "documentUrl"] + "required": [ + "acknowledgment", + "version", + "timestamp", + "documentUrl" + ] }, "privacyPolicy": { "type": "object", @@ -1527,7 +1778,10 @@ "title": "Privacy Policy Version", "description": "Version of the Privacy Policy being acknowledged", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", - "examples": ["1.0.0", "2.1.3"] + "examples": [ + "1.0.0", + "2.1.3" + ] }, "documentUrl": { "type": "string", @@ -1543,10 +1797,17 @@ "description": "When the privacy policy was acknowledged", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": ["2024-01-20T15:30:00Z"] + "examples": [ + "2024-01-20T15:30:00Z" + ] } }, - "required": ["acknowledgment", "version", "timestamp", "documentUrl"] + "required": [ + "acknowledgment", + "version", + "timestamp", + "documentUrl" + ] }, "intellectualProperty": { "type": "object", @@ -1575,7 +1836,13 @@ "properties": { "documentType": { "type": "string", - "enum": ["patent", "trademark", "copyright", "license", "other"], + "enum": [ + "patent", + "trademark", + "copyright", + "license", + "other" + ], "description": "Type of IP documentation" }, "documentUrl": { @@ -1591,7 +1858,11 @@ "contentMediaType": "text/plain" } }, - "required": ["documentType", "documentUrl", "description"] + "required": [ + "documentType", + "documentUrl", + "description" + ] }, "maxItems": 10 }, @@ -1601,10 +1872,15 @@ "description": "When the IP declaration was made", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": ["2024-01-20T15:30:00Z"] + "examples": [ + "2024-01-20T15:30:00Z" + ] } }, - "required": ["acknowledgment", "timestamp"] + "required": [ + "acknowledgment", + "timestamp" + ] }, "compliance": { "type": "object", @@ -1646,10 +1922,18 @@ "description": "When the compliance declaration was made", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": ["2024-01-20T15:30:00Z"] + "examples": [ + "2024-01-20T15:30:00Z" + ] } }, - "required": ["legalCompliance", "noConflictOfInterest", "accurateInformation", "jurisdictions", "timestamp"] + "required": [ + "legalCompliance", + "noConflictOfInterest", + "accurateInformation", + "jurisdictions", + "timestamp" + ] }, "additionalAcknowledgments": { "type": "array", @@ -1692,10 +1976,17 @@ "title": "Acknowledgment Timestamp", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": ["2024-01-20T15:30:00Z"] + "examples": [ + "2024-01-20T15:30:00Z" + ] } }, - "required": ["type", "acknowledgment", "description", "timestamp"] + "required": [ + "type", + "acknowledgment", + "description", + "timestamp" + ] } } }, diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json index 6c1318570dd..25a479f83b6 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json @@ -1,5 +1,5 @@ { - "$schema": "./proposalTemplate.F14.schema.json", + "$schema": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", "setup": { "title": { "title": "Cardano DeFi Integration Platform" diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/proposalTemplate.F14.example.json deleted file mode 100644 index e69de29bb2d..00000000000 From d4b99dea2ac9dc09b7cc65d12886c915101964cd Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Thu, 5 Dec 2024 16:59:52 +0300 Subject: [PATCH 09/25] fix: added all guidance as html, added additional sub fields fields --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 408 +++--------------- .../proposal/F14-Generic/readModifications.md | 192 +++++++++ 2 files changed, 261 insertions(+), 339 deletions(-) create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index b55c82a2a36..36ce520028e 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -94,11 +94,11 @@ "title": { "$ref": "#/definitions/singleLineTextEntry", "title": "Proposal Title", - "description": "A clear, unambiguous, and concise title for your proposal", + "description": "

Proposal title

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.

", "minLength": 1, "maxLength": 60, "examples": [ - "DeFi Integration Platform for Cardano" + "

The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

" ] } }, @@ -113,7 +113,7 @@ "$ref": "#/definitions/singleLineTextEntry", "title": "Name and surname of main applicant", "description": "Name and surname of main applicant", - "x-guidance": "Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).", + "x-guidance": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", "minLength": 2, "maxLength": 100 }, @@ -121,7 +121,7 @@ "$ref": "#/definitions/dropDownSingleSelect", "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", - "x-guidance": "Please select from one of the following: 1. Individual 2. Entity (Incorporated) 3. Entity (Not Incorporated)", + "x-guidance": "

Please select from one of the following:

  1. Individual
  2. Entity (Incorporated)
  3. Entity (Not Incorporated)
", "enum": [ "Individual", "Entity (Incorporated)", @@ -133,7 +133,7 @@ "$ref": "#/definitions/singleLineTextEntryList", "title": "Co-proposers and additional applicants", "description": "Co-proposers and additional applicants", - "x-guidance": "List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 13 rules for added detail.", + "x-guidance": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 13 rules for added detail.

", "maxItems": 5, "minItems": 0 } @@ -162,7 +162,7 @@ "$ref": "#/definitions/tokenValueCardanoADA", "title": "Requested funds in ADA", "description": "The amount of funding requested for your proposal", - "x-guidance": "There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category: Minimum Funding Amount per proposal: Cardano Open: A15,000 Cardano Uses Cases: A15,000 Cardano Partners: A500,000 Maximum Funding Amount per proposal: Cardano Open: Developers (technical): A200,000 • Ecosystem (non-technical): A100,000 Cardano Uses Cases: Concept A150,000 Product: A500,000 Cardano Partners: Enterprise R&D A2,000,000 Growth & Acceleration: A2,000,000", + "x-guidance": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:

Minimum Funding Amount per proposal:

Cardano Open: A15,000

Cardano Uses Cases: A15,000

Cardano Partners: A500,000

Maximum Funding Amount per proposal:

Cardano Open:

  • Developers (technical): A200,000
  • Ecosystem (non-technical): A100,000

Cardano Uses Cases:

  • Concept A150,000
  • Product: A500,000

Cardano Partners:

  • Enterprise R&D A2,000,000
  • Growth & Acceleration: A2,000,000
", "minimum": 15000, "maximum": 2000000 } @@ -178,7 +178,7 @@ "$ref": "#/definitions.durationInMonths", "title": "Project Duration in Months", "description": "Specify the expected duration of your project. Projects must be completable within 2-12 months.", - "x-guidance": "Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.", + "x-guidance": "

Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", "minimum": 2, "maximum": 12 } @@ -195,7 +195,8 @@ "translated": { "$ref": "#/definitions/yesNoChoice", "title": "Auto-translated Status", - "description": "Indicate if your proposal has been auto-translated into English from another language" + "description": "Indicate if your proposal has been auto-translated into English from another language", + "x-guidance": "

Tick YES if your proposal has been auto-translated into English from another language so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections. Tick NO if your proposal has not been auto-translated into English from another language

" }, "original": { "$ref": "#/definitions/singleLineTextEntry", @@ -251,9 +252,7 @@ "description": "Clearly define the problem you aim to solve. This will be visible in the Catalyst voting app.", "minLength": 10, "maxLength": 200, - "examples": [ - "The Cardano ecosystem lacks standardized tools for cross-protocol communication, resulting in fragmented user experiences and inefficient resource utilization." - ] + "x-guidance": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly. This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

" }, "impact": { "$ref": "#/definitions/multiSelect", @@ -294,9 +293,7 @@ "description": "Briefly describe your solution. Focus on what you will do or create to solve the problem.", "minLength": 10, "maxLength": 200, - "examples": [ - "Develop an open-source integration framework that standardizes protocol communication and provides a unified API layer for seamless DeFi interactions." - ] + "x-guidance":"

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how' This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

" }, "approach": { "type": "string", @@ -335,6 +332,7 @@ "type": "array", "title": "Resource Links", "description": "Links to relevant documentation, code repositories, or marketing materials. All links must use HTTPS.", + "x-guidance": "

Here, provide links to yours or your partner organization's website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal. Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere. If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include https. Without these steps, the form will not be submittable and show errors

", "items": { "type": "object", "properties": { @@ -426,6 +424,7 @@ "type": "array", "title": "Dependency Details", "description": "List and describe each dependency", + "x-guidance": "

Here you should list any dependencies and prerequisites for your project's success. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", "items": { "type": "object", "properties": { @@ -496,6 +495,49 @@ "details" ] } + }, + "open_source": { + "title": "Project Open Source", + "description": "Will your project's output/be s fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", + "x-guidance":"

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

", + "properties": { + "open_source": { + "$ref": "#/definitions/yesNoChoice", + "title": "Is Project Open Source?", + "description": "Will your project's output/be s fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", + "x-guidance": "

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

" + }, + "more_information": { + "$ref": "#/definitions/multiLineTextEntry", + "title": "More Information", + "description": "Please provide here more information on the open source status of your project outputs", + "maxLength": 500, + "x-guidance": "

If you answered YES to the above question If declaring the project is open source in the application form, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository. Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs If only certain elements of your code will be open source please clarify which elements will be open source here. If you answered NO to the above question, please give further details as to why your projects outputs will not be open source METADATA

" + } + }, + "required": [ + "isOpenSource" + ], + "dependencies": { + "open_source": [ + "isOpenSource" + ], + "more_information": [ + "isOpenSource" + ] + }, + "if": { + "properties": { + "isOpenSource": { + "const": true + } + } + }, + "then": { + "required": [ + "more_information" + ] + } } } }, @@ -508,6 +550,7 @@ "type": "object", "title": "Project Category", "description": "Select the most relevant category and tags for your project", + "x-guidance": "

Please choose the most relevant category group and tag related to the outcomes of your proposal. Can select only one group and one tag.

", "properties": { "primaryCategory": { "type": "string", @@ -701,6 +744,7 @@ "type": "object", "title": "Solution Description", "description": "Detailed description of your proposed solution", + "x-guidance": "

YOUR PROJECT AND SOLUTION

How you write this section will depend on what type of proposal you are writing. You might want to include details on:

  • How do you perceive the problem you are solving?
  • What are your reasons for approaching it in the way that you have?
  • Who will your project engage?
  • How will you demonstrate or prove your impact?

Explain what is unique about your solution, who will benefit, and why this is important to Cardano.

", "properties": { "description": { "type": "string", @@ -759,6 +803,7 @@ "type": "object", "title": "Project Impact", "description": "Define and measure the impact of your project", + "x-guidance": "

Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:

  • In what way will the success of your project bring value to the Cardano Community?
  • How will you measure this impact?
  • How will you share the outputs and opportunities that result from your project?
", "properties": { "communityBenefit": { "type": "string", @@ -845,6 +890,7 @@ "type": "object", "title": "Capability & Feasibility", "description": "Demonstrate your ability to deliver the project successfully", + "x-guidance":"

Please describe your existing capabilities that demonstrate how and why you believe you're best suited to deliver this project? Please include the steps or processes that demonstrate that you can be trusted to manage funds properly.

", "properties": { "teamExperience": { "type": "string", @@ -864,48 +910,6 @@ "pattern": "^[\\S\\s]*$", "contentMediaType": "text/plain" }, - "riskMitigation": { - "type": "array", - "title": "Risk Mitigation", - "description": "Key risks and mitigation strategies", - "items": { - "type": "object", - "properties": { - "risk": { - "type": "string", - "title": "Risk Description", - "minLength": 10, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "impact": { - "type": "string", - "enum": [ - "Low", - "Medium", - "High" - ], - "title": "Risk Impact" - }, - "mitigation": { - "type": "string", - "title": "Mitigation Strategy", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "risk", - "impact", - "mitigation" - ] - }, - "minItems": 1, - "maxItems": 5 - }, "fundManagement": { "type": "string", "title": "Fund Management", @@ -919,7 +923,6 @@ "required": [ "teamExperience", "feasibilityApproach", - "riskMitigation", "fundManagement" ] } @@ -934,6 +937,7 @@ "type": "object", "title": "Project Milestones", "description": "Detailed project milestones and deliverables", + "x-guidance": "

A clear set of milestones and acceptance criteria will demonstrate your capability to deliver the project as proposed. More guidance on submitting milestones as part of your project proposal can be found here

For Grant Amounts of up to 75k ada, at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)

For Grant Amounts over 75k ada up to 150k ada, at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)

For Grant Amounts over 150k ada up to 300k ada, at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)

For Grant Amounts exceeding 300k ada, at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)

", "properties": { "milestonesConfig": { "type": "object", @@ -951,8 +955,8 @@ "type": "integer", "title": "Number of Milestones", "description": "Total number of milestones including the final milestone", - "minimum": 3, - "maximum": 10 + "minimum": 2, + "maximum": 6 } }, "required": [ @@ -1172,112 +1176,11 @@ }, "minItems": 3, "maxItems": 10 - }, - "finalMilestone": { - "type": "object", - "title": "Final Milestone", - "description": "Project close-out milestone with final report and video", - "properties": { - "closeoutReport": { - "type": "object", - "title": "Project Close-out Report", - "properties": { - "summary": { - "type": "string", - "title": "Project Summary", - "description": "Overall summary of project achievements", - "minLength": 100, - "maxLength": 2000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "keyAchievements": { - "type": "array", - "title": "Key Achievements", - "items": { - "type": "string", - "minLength": 10, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 10 - }, - "lessonsLearned": { - "type": "array", - "title": "Lessons Learned", - "items": { - "type": "string", - "minLength": 20, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 5 - }, - "futureSteps": { - "type": "string", - "title": "Future Steps", - "description": "Plans for project continuation or future development", - "minLength": 50, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "summary", - "keyAchievements", - "lessonsLearned", - "futureSteps" - ] - }, - "demoVideo": { - "type": "object", - "title": "Project Demo Video", - "properties": { - "url": { - "type": "string", - "title": "Video URL", - "format": "uri", - "pattern": "^https://", - "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", - "contentMediaType": "text/uri-list" - }, - "duration": { - "type": "integer", - "title": "Duration in Minutes", - "minimum": 3, - "maximum": 15 - }, - "description": { - "type": "string", - "title": "Video Description", - "minLength": 50, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "url", - "duration", - "description" - ] - } - }, - "required": [ - "closeoutReport", - "demoVideo" - ] - } + } }, "required": [ "milestonesConfig", - "milestonesList", - "finalMilestone" + "milestonesList" ] }, "finalPitch": { @@ -1387,68 +1290,10 @@ }, "minItems": 1, "maxItems": 10 - }, - "teamCapabilities": { - "type": "string", - "title": "Team Capabilities", - "description": "Overview of the team's collective capabilities and why they are best suited for this project", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "previousWork": { - "type": "array", - "title": "Previous Work", - "description": "Examples of relevant previous work or projects", - "items": { - "type": "object", - "properties": { - "projectName": { - "type": "string", - "title": "Project Name", - "minLength": 3, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "description": { - "type": "string", - "title": "Project Description", - "minLength": 50, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "relevance": { - "type": "string", - "title": "Relevance to Current Proposal", - "minLength": 50, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "url": { - "type": "string", - "title": "Project URL", - "format": "uri", - "pattern": "^https://", - "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", - "contentMediaType": "text/uri-list" - } - }, - "required": [ - "projectName", - "description", - "relevance" - ] - }, - "maxItems": 5 } }, "required": [ - "members", - "teamCapabilities" + "members" ] }, "budget": { @@ -1493,94 +1338,21 @@ "maxLength": 300, "pattern": "^[\\S\\s]*$", "contentMediaType": "text/plain" - }, - "breakdown": { - "type": "array", - "items": { - "type": "object", - "properties": { - "item": { - "type": "string", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "cost": { - "type": "number", - "minimum": 0 - }, - "justification": { - "type": "string", - "minLength": 20, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "item", - "cost", - "justification" - ] - }, - "minItems": 1 } }, "required": [ "category", "amount", - "description", - "breakdown" + "description" ] }, "minItems": 1 - }, - "timeline": { - "type": "object", - "title": "Budget Timeline", - "properties": { - "distributionSchedule": { - "type": "array", - "items": { - "type": "object", - "properties": { - "milestone": { - "type": "string", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "amount": { - "type": "number", - "minimum": 0 - }, - "percentage": { - "type": "number", - "minimum": 0, - "maximum": 100 - } - }, - "required": [ - "milestone", - "amount", - "percentage" - ] - }, - "minItems": 1 - } - }, - "required": [ - "distributionSchedule" - ] } }, "required": [ "totalBudget", - "categories", - "timeline" - ] + "categories" + ] }, "valueProposition": { "type": "object", @@ -1596,47 +1368,6 @@ "pattern": "^[\\S\\s]*$", "contentMediaType": "text/plain" }, - "impactMetrics": { - "type": "array", - "title": "Impact Metrics", - "description": "Specific metrics that demonstrate value for money", - "items": { - "type": "object", - "properties": { - "metric": { - "type": "string", - "title": "Metric Name", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "target": { - "type": "string", - "title": "Target Value", - "minLength": 1, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "justification": { - "type": "string", - "title": "Value Justification", - "minLength": 50, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "metric", - "target", - "justification" - ] - }, - "minItems": 2, - "maxItems": 5 - }, "longTermValue": { "type": "string", "title": "Long-term Value", @@ -1663,7 +1394,6 @@ }, "required": [ "costBenefitAnalysis", - "impactMetrics", "longTermValue", "communityBenefits" ] diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md new file mode 100644 index 00000000000..8ca17618a52 --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md @@ -0,0 +1,192 @@ +# F14-Generic +## Changes to the proposal template fields + +**Problem:** + - Statement + - Impact (select): + - "Technical Infrastructure", + - "User Experience", + - "Developer Tooling", + - "Community Growth", + - "Economic Sustainability", + - "Interoperability", + - "Security", + - "Scalability", + - "Education", + - "Adoption" + + + +**Solution:** + - summary + - approach + - innovative aspects + +**Supporting links:** + - Links + - Url + - Description + - Type (select): + - "GitHub Repository", + - "Documentation", + - "Whitepaper", + - "Website", + - "Marketing Material", + - "Technical Specification", + - "Research Paper", + - "Blog Post", + - "Social Media", + - "Other" + - Main Repository link + - Documentation link + +**Dependencies** + - detail: + - name + - description + - type: + - "Technical" + - "Organizational" + - "Legal" + - "Financial" + - "Other" + - mitigationPlan + +**Horizons:** + - tags: + - additional tags to be entered for the project -> on top of category and subcategory + + Impact: + + timeframe: + + "Short-term (0-6 months)" + + "Medium-term (6-18 months)" + + "Long-term (18+ months)" + + scale: + + "Local", + + "Regional", + + "Global" + + metrics: + + "metric name", + + "target", + + "measurement" + +- Product Details: + - Solution: + - description + + unique value proposition + + target audience + + implementation approach + - Impact: + + communityBenefit + + Metrics + + name + + description + + target + + measurement + + Project outputs + - Capability: + + Team Experience + + Feasibility Approach + + Fund Management: (How will you ensure proper management and accountability of funds?) + + +**Milestones:** + + milestonesConfig: + + grantAmount + + numberOfMilestones + + milestonesList: + - milestone: + - title + + description + + deliverables + + name + + description + + type + + "Documentation" + + "Software" + + "Report" + + "Presentation" + + "Video" + + "Other" + + evidenceOfCompletion + + type + + "Code Repository" + + "Documentation" + + "Demo Video" + + "Test Results" + + "Metrics Report" + + "User Feedback" + + "Other" + + description + + timeline + + startDate + + endDate + + durationInWeeks + + budget + + amount + + breakdown + + category + + "Development" + + "Design" + + "Marketing" + + "Operations" + + "Other" + + amount + + description +**Final Pitch** + + team + + members + + name + + role + + expertise + + experience + + links + + type + + url + + description + + budget + + total budget + + budget categories + + category: + + "Development", + + "Design", + + "Marketing", + + "Operations", + + "Research", + + "Community Management", + + "Legal", + + "Other" + + amount + + description + +**Value for money** + + costBenefitAnalysis + + longTermValue + + communityBenefits + +**Mandatory Acknowledgments** + + fundRules + + acknowledgment + + version + + timestamp + + termsAndConditions + + acknowledgment + + version + + documentUrl + + timestamp + + privacyPolicy + + acknowledgment + + version + + documentUrl + + timestamp + + compliance + + legalCompliance + + noConflictOfInterest + + accurateInformation + + jurisdictions + + timestamp + + additionalAcknowledgments + + type + + acknowledgment + + description + + documentUrl + + timestamp \ No newline at end of file From 9422df09a3ee5b59006c3a506215c33365729ee5 Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Thu, 5 Dec 2024 17:16:52 +0300 Subject: [PATCH 10/25] fix: captured all new fields added in this file --- .../proposal/F14-Generic/readModifications.md | 161 ++++++++++-------- 1 file changed, 88 insertions(+), 73 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md index 8ca17618a52..055d0b00db9 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md @@ -1,60 +1,58 @@ -# F14-Generic -## Changes to the proposal template fields +# F14-Generic Proposal Template +## New Fields added to the proposal template **Problem:** - - Statement - - Impact (select): - - "Technical Infrastructure", - - "User Experience", - - "Developer Tooling", - - "Community Growth", - - "Economic Sustainability", - - "Interoperability", - - "Security", - - "Scalability", - - "Education", - - "Adoption" - - + + Statement + + Impact (select): + + "Technical Infrastructure", + + "User Experience", + + "Developer Tooling", + + "Community Growth", + + "Economic Sustainability", + + "Interoperability", + + "Security", + + "Scalability", + + "Education", + + "Adoption" **Solution:** - - summary - - approach - - innovative aspects + + Summary + + Approach + + Innovative aspects **Supporting links:** - - Links - - Url - - Description - - Type (select): - - "GitHub Repository", - - "Documentation", - - "Whitepaper", - - "Website", - - "Marketing Material", - - "Technical Specification", - - "Research Paper", - - "Blog Post", - - "Social Media", - - "Other" - - Main Repository link - - Documentation link + + Links + + Url + + Description + + Type (select): + + "GitHub Repository", + + "Documentation", + + "Whitepaper", + + "Website", + + "Marketing Material", + + "Technical Specification", + + "Research Paper", + + "Blog Post", + + "Social Media", + + "Other" + + Main Repository link + + Documentation link **Dependencies** - - detail: - - name - - description - - type: - - "Technical" - - "Organizational" - - "Legal" - - "Financial" - - "Other" - - mitigationPlan + + detail: + + name + + description + + type: + + "Technical" + + "Organizational" + + "Legal" + + "Financial" + + "Other" + + mitigationPlan **Horizons:** - - tags: - - additional tags to be entered for the project -> on top of category and subcategory + + tags: + + additional tags to be entered for the project -> on top of category and subcategory + Impact: + timeframe: + "Short-term (0-6 months)" @@ -164,29 +162,46 @@ + communityBenefits **Mandatory Acknowledgments** - + fundRules - + acknowledgment - + version - + timestamp - + termsAndConditions - + acknowledgment - + version - + documentUrl - + timestamp - + privacyPolicy - + acknowledgment - + version - + documentUrl - + timestamp - + compliance - + legalCompliance - + noConflictOfInterest - + accurateInformation - + jurisdictions - + timestamp - + additionalAcknowledgments - + type - + acknowledgment - + description - + documentUrl - + timestamp \ No newline at end of file + + Fund Rules + + Acknowledgment + + Version + + Timestamp + + + Terms And Conditions + + Acknowledgment + + Version + + DocumentUrl + + Timestamp + + + Privacy Policy + + Acknowledgment + + Version + + DocumentUrl + + Timestamp + + + Intellectual Property + + Acknowledgment + + Details + + Attachments + + Document Type + + "patent", + + "trademark", + + "copyright", + + "license", + + "other" + + Document Url + + Description + + + Compliances (checkboxes) + + Legal Compliance + + No Conflict Of Interest + + Accurate Information + + Jurisdictions + + Timestamp + + + Additional Acknowledgments + + Type + + Acknowledgment + + Description + + Document Url + + Timestamp \ No newline at end of file From 3fe083f801f3ea53f710de06873fae32a859845f Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Thu, 5 Dec 2024 19:10:18 +0300 Subject: [PATCH 11/25] fix: updated example proposal --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 6 +- .../F14-Generic/example.proposal.json | 229 +++++++++++------- 2 files changed, 149 insertions(+), 86 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 36ce520028e..8642ba3ebb3 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -97,9 +97,7 @@ "description": "

Proposal title

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.

", "minLength": 1, "maxLength": 60, - "examples": [ - "

The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

" - ] + "x-guidance": "

The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

" } }, "required": [ @@ -121,7 +119,7 @@ "$ref": "#/definitions/dropDownSingleSelect", "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", - "x-guidance": "

Please select from one of the following:

  1. Individual
  2. Entity (Incorporated)
  3. Entity (Not Incorporated)
", + "x-mad-guidance": "

Please select from one of the following:

  1. Individual
  2. Entity (Incorporated)
  3. Entity (Not Incorporated)
", "enum": [ "Individual", "Entity (Incorporated)", diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json index 25a479f83b6..d524fec31ae 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json @@ -2,141 +2,206 @@ "$schema": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", "setup": { "title": { - "title": "Cardano DeFi Integration Platform" + "title": "Example Catalyst Proposal" }, "proposer": { - "mainApplicant": "John Smith", - "applicantType": "Entity (Incorporated)", - "coProposers": ["Alice Johnson", "Bob Wilson"] + "applicant": "John Doe", + "type": "Individual", + "coproposers": [] } }, - "proposalSummary": { + "summary": { "budget": { - "requestedFunds": 500000 + "requestedFunds": 150000 + }, + "time": { + "duration": 6 + }, + "translation": { + "translated": false, + "original": "English", + "notes": "Original proposal in English" }, "problem": { - "description": "Current DeFi platforms on Cardano lack seamless integration capabilities, making it difficult for developers to build interconnected financial applications.", - "relevance": "This problem affects the entire Cardano ecosystem by limiting the growth and adoption of DeFi applications." + "statement": "Current challenge in the Cardano ecosystem...", + "impact": ["Technical Infrastructure", "Developer Tooling", "Adoption"] + }, + "solution": { + "summary": "Our solution provides a comprehensive toolkit...", + "approach": "We will implement this solution using...", + "innovationAspects": [ + "Novel testing framework", + "Automated integration tools" + ] }, - "supportingLinks": { - "documentation": "https://example.com/project-docs", - "github": "https://github.com/example/defi-platform", - "media": ["https://example.com/demo-video"] + "SupportingLinks": { + "links": [ + { + "url": "https://github.com/example/project", + "type": "GitHub Repository", + "description": "Project's main repository" + } + ], + "mainRepository": "https://github.com/example/project", + "documentation": "https://docs.example.com" } }, "horizons": { "category": { - "primaryCategory": "DeFi", + "primaryCategory": "Development & Tools", "subCategory": "Developer Tools" }, - "tags": ["defi", "integration", "developer-tools", "smart-contracts"] + "tags": ["defi", "developer-tools", "infrastructure"], + "impact": { + "timeframe": "Medium-term (6-18 months)", + "scale": "Global", + "metrics": [ + { + "metric": "Developer Adoption", + "target": "500 active developers", + "measurement": "GitHub analytics" + } + ] + } }, "proposalDetails": { "solution": { - "description": "Our platform will provide a unified API layer that enables seamless integration between different DeFi protocols on Cardano.", - "features": [ - "Standardized API endpoints", - "Smart contract templates", - "Cross-protocol liquidity management" - ] + "description": "Our solution provides a comprehensive toolkit...", + "uniqueValue": "First integrated testing framework for Cardano", + "targetAudience": ["Cardano Developers", "DApp Teams"], + "implementation": "We will use an agile development approach..." }, "impact": { + "communityBenefit": "Significantly reduces development time and improves code quality", "metrics": [ - "Number of integrated protocols", - "Developer adoption rate", - "Transaction volume through the platform" + { + "name": "Developer Adoption", + "description": "Number of active developers using the toolkit", + "target": "500 developers", + "measurement": "GitHub analytics and usage statistics" + } ], - "targetAudience": "DeFi developers and protocol creators on Cardano" + "outputs": [ + "Testing Framework", + "Documentation", + "Training Materials" + ] }, "capability": { - "teamExperience": "Our team has 5+ years of experience in DeFi development and Cardano ecosystem", - "resources": "Fully equipped development team with blockchain expertise" + "teamExperience": "Our team has extensive experience in blockchain development...", + "feasibilityApproach": "We have already developed a proof of concept...", + "fundManagement": "Funds will be managed through a transparent process..." } }, "milestones": { "milestonesConfig": { - "count": 4, - "duration": "6 months" + "grantAmount": 150000, + "numberOfMilestones": 4 }, "milestonesList": [ { - "title": "Architecture Design", - "description": "Complete system architecture and API specifications", - "deliverables": ["Architecture documentation", "API specifications"], - "budget": { - "amount": 100000 - } - }, - { - "title": "Core Development", - "description": "Develop core integration layer and smart contracts", - "deliverables": ["Core platform code", "Smart contract templates"], - "budget": { - "amount": 200000 - } - }, - { - "title": "Testing and Integration", - "description": "Comprehensive testing and initial protocol integrations", - "deliverables": ["Test reports", "Integration documentation"], + "title": "Initial Setup and Planning", + "description": "Project setup and detailed planning phase", + "deliverables": [ + { + "name": "Project Plan", + "description": "Detailed project planning documentation", + "type": "Documentation" + } + ], + "acceptanceCriteria": [ + "Completed project plan", + "Technical specifications approved" + ], + "evidenceOfCompletion": [ + { + "type": "Documentation", + "description": "Project planning documents and specifications" + } + ], + "timeline": { + "startDate": "2024-03-01", + "endDate": "2024-04-01", + "durationInWeeks": 4 + }, "budget": { - "amount": 150000 + "amount": 37500, + "breakdown": [ + { + "category": "Development", + "amount": 30000, + "description": "Initial development work" + } + ] } } - ], - "finalMilestone": { - "title": "Launch and Documentation", - "deliverables": ["Platform launch", "Complete documentation", "Video demonstration"], - "budget": { - "amount": 50000 - } - } + ] }, "finalPitch": { "team": { "members": [ { - "name": "John Smith", + "name": "John Doe", "role": "Project Lead", - "experience": "10 years in blockchain development" - }, - { - "name": "Alice Johnson", - "role": "Smart Contract Developer", - "experience": "5 years Cardano development" + "expertise": ["Blockchain Development", "Smart Contracts"], + "experience": "10 years in software development", + "links": [ + { + "type": "GitHub", + "url": "https://github.com/johndoe", + "description": "GitHub Profile" + } + ] } - ], - "teamCapabilities": "Our team combines deep expertise in DeFi protocols, Cardano development, and system architecture." + ] }, "budget": { - "totalBudget": 500000, - "categories": { - "development": 300000, - "testing": 100000, - "documentation": 50000, - "management": 50000 - }, - "timeline": { - "distributionSchedule": "Quarterly" - } + "totalBudget": 150000, + "categories": [ + { + "category": "Development", + "amount": 100000, + "description": "Core development team costs" + } + ] }, "valueProposition": { - "impact": "Accelerate DeFi development on Cardano", - "sustainability": "Platform fees and maintenance contracts" + "costBenefitAnalysis": "The project provides significant value...", + "longTermValue": "The solution will continue to benefit the ecosystem...", + "communityBenefits": [ + "Increased developer productivity", + "Better code quality" + ] } }, "mandatoryAcknowledgments": { "fundRules": { - "acknowledgment": true + "acknowledgment": true, + "version": "F14", + "timestamp": "2024-01-20T15:30:00Z" + }, + "termsAndConditions": { + "acknowledgment": true, + "version": "1.0.0", + "documentUrl": "https://cardano.org/terms", + "timestamp": "2024-01-20T15:30:00Z" }, "privacyPolicy": { - "acknowledgment": true + "acknowledgment": true, + "version": "1.0.0", + "documentUrl": "https://cardano.org/privacy", + "timestamp": "2024-01-20T15:30:00Z" }, "intellectualProperty": { - "acknowledgment": true + "acknowledgment": true, + "timestamp": "2024-01-20T15:30:00Z" }, "compliance": { - "legalCompliance": true + "legalCompliance": true, + "noConflictOfInterest": true, + "accurateInformation": true, + "jurisdictions": ["US", "GB"], + "timestamp": "2024-01-20T15:30:00Z" } } -} +} \ No newline at end of file From a07f3bcfdf073e3614ed0196651ff441ea4bb3c9 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Fri, 6 Dec 2024 08:17:25 +0700 Subject: [PATCH 12/25] fix(cat-gateway): rename section breaks to align with design --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 36ce520028e..874ba784814 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -10,12 +10,12 @@ "format": "path", "readOnly": true }, - "section": { + "segment": { "$comment": "UI - Logical Document Section Break.", "type": "object", "additionalProperties": false }, - "subsection": { + "section": { "$comment": "UI - Logical Document Sub-Section Break.", "type": "object", "additionalProperties": false @@ -82,12 +82,12 @@ "const": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json" }, "setup": { - "$ref": "#/definitions/section", + "$ref": "#/definitions/segment", "title": "proposal setup", "description": "Proposal title", "properties": { "title": { - "$ref": "#/definitions/subsection", + "$ref": "#/definitions/section", "title": "proposal setup", "description": "Proposal title", "properties": { @@ -107,7 +107,7 @@ ] }, "proposer": { - "$ref": "#/definitions/subsection", + "$ref": "#/definitions/section", "properties": { "applicant": { "$ref": "#/definitions/singleLineTextEntry", @@ -150,12 +150,12 @@ ] }, "summary": { - "$ref": "#/definitions/section", + "$ref": "#/definitions/segment", "title": "Proposal Summary", "description": "Key information about your proposal", "properties": { "budget": { - "$ref": "#/definitions/subsection", + "$ref": "#/definitions/section", "title": "Budget Information", "properties": { "requestedFunds": { @@ -172,7 +172,7 @@ ] }, "time": { - "$ref": "#/definitions/subsection", + "$ref": "#/definitions/section", "properties": { "duration": { "$ref": "#/definitions.durationInMonths", @@ -188,7 +188,7 @@ ] }, "translation": { - "$ref": "#/definitions/subsection", + "$ref": "#/definitions/section", "title": "Translation Information", "description": "Information about the proposal's language and translation status", "properties": { @@ -242,7 +242,7 @@ } }, "problem": { - "$ref": "#/definitions/subsection", + "$ref": "#/definitions/section", "title": "Problem Statement", "description": "Define the problem your proposal aims to solve", "properties": { @@ -283,7 +283,7 @@ ] }, "solution": { - "$ref": "#/definitions/subsection", + "$ref": "#/definitions/section", "title": "Solution Overview", "description": "Describe your proposed solution to the problem", "properties": { From 5eee04d94d798b0bc65dad725c6ebba3338de57c Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Fri, 6 Dec 2024 13:23:26 +0700 Subject: [PATCH 13/25] docs(cat-gateway): Finish the template as at F13 state --- .config/dictionaries/project.dic | 2 + ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 11 +- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 1780 +++++------------ .../F14-Generic/example.proposal.json | 39 +- .../F14-Generic/extra-definitions.txt | 1079 ++++++++++ .../proposal/F14-Generic/readModifications.md | 374 ++-- 6 files changed, 1796 insertions(+), 1489 deletions(-) create mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/extra-definitions.txt diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 517820b0998..bd4d4cd78dc 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -50,6 +50,7 @@ chrono ciphertext ciphertexts CIPs +CNFT COCOAPODS codegen codepoints @@ -109,6 +110,7 @@ gethostname Gitbook gmtime gradlew +Hackathons headful headlessui HIDPI diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 63abfbcede2..831508f8194 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -38,16 +38,19 @@ "format": "yes/no", "default": false }, + "uri": { + "type": "string", + "format": "uri", + "contentMediaType": "text/plain", + "maxLength": 1024 + }, "uriList": { "type": "array", "format": "uriList", "uniqueItems": true, "default": [], "items": { - "type": "string", - "format": "uri", - "contentMediaType": "text/plain", - "maxLength": 1024 + "$ref": "#/definitions/uri" } } }, diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 9315bc8281c..dc5812c5be1 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -26,11 +26,23 @@ "contentMediaType": "text/plain", "pattern": "^.*$" }, + "singleLineHttpsURLEntry": { + "$comment": "UI - Single Line text entry for HTTPS Urls.", + "type": "string", + "format": "uri", + "pattern": "^https:.*" + }, "multiLineTextEntry": { "$comment": "UI - Multiline text entry without any markup or rich text capability.", "type": "string", "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$" + "pattern": "^[\\S\\s]*$" + }, + "multiLineTextEntryMarkdown": { + "$comment": "UI - Multiline text entry with Markdown content.", + "type": "string", + "contentMediaType": "text/markdown", + "pattern": "^[\\S\\s]*$" }, "dropDownSingleSelect": { "$comment": "UI - Drop Down Selection of a single entry from the defined enum.", @@ -56,6 +68,59 @@ "maxLength": 1024 } }, + "multiLineTextEntryListMarkdown": { + "$comment": "UI - A Growable List of markdown formatted text fields.", + "type": "array", + "format": "multiLineTextEntryListMarkdown", + "uniqueItems": true, + "default": [], + "items": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "maxLength": 10240 + } + }, + "singleLineHttpsURLEntryList": { + "$comment": "UI - A Growable List of HTTPS URLs.", + "type": "array", + "format": "singleLineHttpsURLEntryList", + "uniqueItems": true, + "default": [], + "items": { + "$ref": "#/definitions/singleLineHttpsURLEntry", + "maxLength": 1024 + } + }, + "nestedQuestionsList": { + "$comment": "UI - A Growable List of Questions. The contents are an object, that can have any UI elements within.", + "type": "array", + "format": "nestedQuestionsList", + "uniqueItems": true, + "default": [] + }, + "nestedQuestions": { + "$comment": "UI - The container for a nested question set.", + "type": "object", + "format": "nestedQuestions", + "additionalProperties": false + }, + "singleGroupedTagSelector": { + "$comment": "UI - A selector where a top level selection, gives a single choice from a list of tags.", + "type": "object", + "format": "singleGroupedTagSelector", + "additionalProperties": false + }, + "tagGroup": { + "$comment": "UI - An individual group within a singleGroupedTagSelector.", + "type": "string", + "format": "tagGroup", + "pattern": "^.*$" + }, + "tagSelection": { + "$comment": "UI - An individual tag within the group of a singleGroupedTagSelector.", + "type": "string", + "format": "tagSelection", + "pattern": "^.*$" + }, "tokenValueCardanoADA": { "$comment": "UI - A Token Value denominated in Cardano ADA.", "type": "integer", @@ -69,8 +134,22 @@ "yesNoChoice": { "$comment": "UI - A Boolean choice, represented as a Yes/No selection. Yes = true.", "type": "boolean", - "format": "yes/no", + "format": "yesNoChoice", "default": false + }, + "agreementConfirmation": { + "$comment": "UI - A Boolean choice, defaults to `false` but its invalid if its not set to `true`.", + "type": "boolean", + "format": "agreementConfirmation", + "default": false, + "const": true + }, + "spdxLicenseOrURL": { + "$comment": "UI - Drop Down Selection of any valid SPDX Identifier. This is a complex type, it should let the user select one of the valid SPDX licenses, or enter a URL of the license if its proprietary. In the form its just a string.", + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "format": "spdxLicenseOrURL" } }, "type": "object", @@ -131,7 +210,7 @@ "$ref": "#/definitions/singleLineTextEntryList", "title": "Co-proposers and additional applicants", "description": "Co-proposers and additional applicants", - "x-guidance": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 13 rules for added detail.

", + "x-guidance": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 14 rules for added detail.

", "maxItems": 5, "minItems": 0 } @@ -173,7 +252,7 @@ "$ref": "#/definitions/section", "properties": { "duration": { - "$ref": "#/definitions.durationInMonths", + "$ref": "#/definitions/durationInMonths", "title": "Project Duration in Months", "description": "Specify the expected duration of your project. Projects must be completable within 2-12 months.", "x-guidance": "

Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", @@ -182,7 +261,7 @@ } }, "required": [ - "projectDuration" + "duration" ] }, "translation": { @@ -257,7 +336,7 @@ "title": "Impact Areas", "description": "Select the areas that will be most impacted by solving this problem", "items": { - "$ref": "singleLineTextEntry", + "$ref": "#/definitions/singleLineTextEntry", "enum": [ "Technical Infrastructure", "User Experience", @@ -291,29 +370,20 @@ "description": "Briefly describe your solution. Focus on what you will do or create to solve the problem.", "minLength": 10, "maxLength": 200, - "x-guidance":"

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how' This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

" + "x-guidance": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how' This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

" }, "approach": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "Technical Approach", "description": "Outline the technical approach or methodology you will use", - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "maxLength": 500 }, "innovationAspects": { - "type": "array", + "$ref": "#/definitions/singleLineTextEntryList", "title": "Innovation Aspects", "description": "Key innovative aspects of your solution", - "items": { - "type": "string", - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, "minItems": 1, - "maxItems": 5, - "uniqueItems": true + "maxItems": 5 } }, "required": [ @@ -322,145 +392,72 @@ ] }, "SupportingLinks": { - "type": "object", + "$ref": "#/definitions/section", "title": "Supporting Documentation", "description": "Additional resources and documentation for your proposal", + "x-guidance": "

Here, provide links to yours or your partner organization's website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal. Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere. If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include https. Without these steps, the form will not be submittable and show errors

", "properties": { - "links": { - "type": "array", - "title": "Resource Links", - "description": "Links to relevant documentation, code repositories, or marketing materials. All links must use HTTPS.", - "x-guidance": "

Here, provide links to yours or your partner organization's website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal. Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere. If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include https. Without these steps, the form will not be submittable and show errors

", - "items": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://.*", - "title": "Resource URL", - "description": "URL must start with https://", - "examples": [ - "https://github.com/your-org/project", - "https://your-project-docs.com" - ], - "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", - "contentMediaType": "text/uri-list" - }, - "type": { - "type": "string", - "enum": [ - "GitHub Repository", - "Documentation", - "Whitepaper", - "Website", - "Marketing Material", - "Technical Specification", - "Research Paper", - "Blog Post", - "Social Media", - "Other" - ], - "title": "Resource Type", - "description": "Type of resource being linked" - }, - "description": { - "type": "string", - "maxLength": 200, - "title": "Resource Description", - "description": "Brief description explaining what this resource contains and why it's relevant", - "examples": [ - "Project's main GitHub repository containing all source code", - "Technical whitepaper detailing the solution architecture" - ], - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "url", - "type", - "description" - ] - }, - "minItems": 0, - "maxItems": 10, - "uniqueItems": true - }, "mainRepository": { - "type": "string", - "format": "uri", - "pattern": "^https://(github\\.com|gitlab\\.com|bitbucket\\.org)/.*", + "$ref": "#/definitions/singleLineHttpsURLEntry", "title": "Main Code Repository", - "description": "Primary repository where the project's code will be hosted", - "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", - "contentMediaType": "text/uri-list" + "description": "Primary repository where the project's code will be hosted" }, "documentation": { - "type": "string", - "format": "uri", - "pattern": "^https://.*", + "$ref": "#/definitions/singleLineHttpsURLEntry", "title": "Documentation URL", - "description": "Main documentation site or resource for the project", - "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", - "contentMediaType": "text/uri-list" + "description": "Main documentation site or resource for the project" + }, + "other": { + "$ref": "#/definitions/singleLineHttpsURLEntryList", + "title": "Resource Links", + "description": "Links to any other relevant documentation, code repositories, or marketing materials. All links must use HTTPS.", + "minItems": 0, + "maxItems": 5 } } }, "dependencies": { - "type": "object", + "$ref": "#/definitions/section", "title": "Project Dependencies", "description": "External dependencies and requirements for project success", + "x-guidance": "

If your project has any dependencies and prerequisites for your project's success, list them here. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", "properties": { - "hasDependencies": { - "type": "boolean", - "title": "Has Dependencies", - "description": "Indicate if your project has any dependencies on other organizations or technologies", - "default": false - }, "details": { - "type": "array", + "$ref": "#/definitions/nestedQuestionsList", "title": "Dependency Details", "description": "List and describe each dependency", - "x-guidance": "

Here you should list any dependencies and prerequisites for your project's success. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", "items": { - "type": "object", + "$ref": "#/definitions/nestedQuestions", "properties": { "name": { - "type": "string", + "$ref": "#/definitions/singleLineTextEntry", "title": "Dependency Name", "description": "Name of the organization, technology, or resource", - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "maxLength": 100 }, "type": { - "type": "string", + "$ref": "#/definitions/dropDownSingleSelect", + "title": "Dependency Type", + "description": "Type of dependency", "enum": [ "Technical", "Organizational", "Legal", "Financial", "Other" - ], - "title": "Dependency Type", - "description": "Type of dependency" + ] }, "description": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "Description", "description": "Explain why this dependency is essential and how it affects your project", - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "maxLength": 500 }, "mitigationPlan": { - "type": "string", + "$ref": "#/definitions/multiLineTextEntry", "title": "Mitigation Plan", "description": "How will you handle potential issues with this dependency", - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "maxLength": 300 } }, "required": [ @@ -472,1259 +469,448 @@ "minItems": 0, "maxItems": 10 } - }, - "required": [ - "hasDependencies" - ], - "dependencies": { - "details": [ - "hasDependencies" - ] - }, - "if": { - "properties": { - "hasDependencies": { - "const": true - } - } - }, - "then": { - "required": [ - "details" - ] } }, "open_source": { + "$ref": "#/definitions/section", "title": "Project Open Source", "description": "Will your project's output/be s fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", - "x-guidance":"

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

", + "x-guidance": "

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

", "properties": { - "open_source": { - "$ref": "#/definitions/yesNoChoice", - "title": "Is Project Open Source?", - "description": "Will your project's output/be s fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", - "x-guidance": "

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

" + "source_code": { + "$ref": "#/definitions/spdxLicenseOrURL" }, - "more_information": { + "documentation": { + "$ref": "#/definitions/spdxLicenseOrURL" + }, + "note": { "$ref": "#/definitions/multiLineTextEntry", "title": "More Information", "description": "Please provide here more information on the open source status of your project outputs", "maxLength": 500, - "x-guidance": "

If you answered YES to the above question If declaring the project is open source in the application form, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository. Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs If only certain elements of your code will be open source please clarify which elements will be open source here. If you answered NO to the above question, please give further details as to why your projects outputs will not be open source METADATA

" + "x-guidance": "

If you did not answer PROPRIETARY to the above questions, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository. Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs If only certain elements of your code will be open source please clarify which elements will be open source here. If you answered NO to the above question, please give further details as to why your projects outputs will not be open source METADATA

" } }, "required": [ - "isOpenSource" - ], - "dependencies": { - "open_source": [ - "isOpenSource" - ], - "more_information": [ - "isOpenSource" - ] - }, - "if": { - "properties": { - "isOpenSource": { - "const": true - } - } - }, - "then": { - "required": [ - "more_information" - ] - } + "source_code", + "documentation" + ] } } }, "horizons": { - "type": "object", - "title": "Project Horizons", - "description": "Long-term vision and categorization of your project", + "$ref": "#/definitions/segment", + "title": "Horizons", "properties": { - "category": { - "type": "object", - "title": "Project Category", - "description": "Select the most relevant category and tags for your project", - "x-guidance": "

Please choose the most relevant category group and tag related to the outcomes of your proposal. Can select only one group and one tag.

", - "properties": { - "primaryCategory": { - "type": "string", - "title": "Primary Category", - "description": "Main category that best describes your project", - "enum": [ - "Governance", - "Education", - "Community & Outreach", - "Development & Tools", - "Identity & Security", - "DeFi", - "Real World Applications", - "Events & Marketing", - "Interoperability", - "Legal & Policy", - "Sustainability", - "Smart Contracts" - ] - }, - "subCategory": { - "type": "string", - "title": "Sub-category", - "description": "Specific area within the main category", - "enum": [ - "DAO", - "Voting", - "Treasury Management", - "Learn to Earn", - "Training", - "Translation", - "Connected Community", - "Social Media", - "Community Building", - "Developer Tools", - "L2 Infrastructure", - "Analytics", - "AI Research", - "UTXO", - "P2P", - "Identity & Verification", - "Cybersecurity", - "Authentication", - "Privacy", - "Payments", - "Stablecoin", - "Risk Management", - "Yield", - "Staking", - "Lending", - "Wallet", - "Marketplace", - "Manufacturing", - "IoT", - "Financial Services", - "E-commerce", - "Business Services", - "Supply Chain", - "Real Estate", - "Healthcare", - "Tourism", - "Entertainment", - "RWA", - "Music", - "Tokenization", - "Events", - "Marketing", - "Hackathons", - "Accelerator", - "Incubator", - "Cross-chain", - "Off-chain", - "Bridges", - "Policy", - "Advocacy", - "Standards", - "Compliance", - "Environment", - "Agriculture", - "Clean Energy", - "Development", - "Security", - "Templates", - "Auditing" - ] - } - } - }, - "tags": { - "type": "array", - "title": "Project Tags", - "description": "Additional tags to help categorize your project", - "items": { - "type": "string", - "minLength": 2, - "maxLength": 30, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 5, - "uniqueItems": true - }, - "impact": { - "type": "object", - "title": "Project Impact", - "description": "Describe the expected impact of your project", + "theme": { + "$ref": "#/definitions/section", + "title": "Horizons", + "description": "Long-term vision and categorization of your project", "properties": { - "timeframe": { - "type": "string", - "enum": [ - "Short-term (0-6 months)", - "Medium-term (6-18 months)", - "Long-term (18+ months)" - ], - "title": "Impact Timeframe", - "description": "Expected timeframe to see meaningful impact" - }, - "scale": { - "type": "string", - "enum": [ - "Local", - "Regional", - "Global" - ], - "title": "Impact Scale", - "description": "Geographic scale of impact" - }, - "metrics": { - "type": "array", - "title": "Impact Metrics", - "description": "Key metrics to measure project success", - "items": { - "type": "object", - "properties": { - "metric": { - "type": "string", - "title": "Metric Name", - "description": "Name of the metric", - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "target": { - "type": "string", - "title": "Target Value", - "description": "Target value or goal for this metric", - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "measurement": { - "type": "string", - "title": "Measurement Method", - "description": "How this metric will be measured", - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "grouped_tag": { + "$ref": "#/definitions/singleGroupedTagSelector", + "oneOf": [ + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Governance" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Governance", + "DAO" + ] + } } }, - "required": [ - "metric", - "target", - "measurement" - ] - }, - "minItems": 1, - "maxItems": 5 - } - }, - "required": [ - "timeframe", - "scale", - "metrics" - ] - } - }, - "required": [ - "primaryCategory", - "subCategory", - "tags", - "impact" - ] - }, - "proposalDetails": { - "type": "object", - "title": "Proposal Details", - "description": "Detailed information about your proposal's solution, impact, and feasibility", - "properties": { - "solution": { - "type": "object", - "title": "Solution Description", - "description": "Detailed description of your proposed solution", - "x-guidance": "

YOUR PROJECT AND SOLUTION

How you write this section will depend on what type of proposal you are writing. You might want to include details on:

  • How do you perceive the problem you are solving?
  • What are your reasons for approaching it in the way that you have?
  • Who will your project engage?
  • How will you demonstrate or prove your impact?

Explain what is unique about your solution, who will benefit, and why this is important to Cardano.

", - "properties": { - "description": { - "type": "string", - "title": "Solution Description", - "description": "Provide a comprehensive description of your proposed solution", - "minLength": 100, - "maxLength": 2000, - "examples": [ - "Our solution involves developing a decentralized education platform that will..." - ], - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "uniqueValue": { - "type": "string", - "title": "Unique Value Proposition", - "description": "What makes your solution unique and innovative?", - "minLength": 50, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "targetAudience": { - "type": "array", - "title": "Target Audience", - "description": "Who will benefit from your solution?", - "items": { - "type": "string", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 5, - "uniqueItems": true - }, - "implementation": { - "type": "string", - "title": "Implementation Approach", - "description": "How will you implement your solution?", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "description", - "uniqueValue", - "targetAudience", - "implementation" - ] - }, - "impact": { - "type": "object", - "title": "Project Impact", - "description": "Define and measure the impact of your project", - "x-guidance": "

Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:

  • In what way will the success of your project bring value to the Cardano Community?
  • How will you measure this impact?
  • How will you share the outputs and opportunities that result from your project?
", - "properties": { - "communityBenefit": { - "type": "string", - "title": "Community Benefit", - "description": "How will the Cardano community benefit from your project?", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "metrics": { - "type": "array", - "title": "Impact Metrics", - "description": "Specific metrics to measure project success", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Metric Name", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "description": { - "type": "string", - "title": "Metric Description", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "target": { - "type": "string", - "title": "Target Value", - "minLength": 1, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "measurement": { - "type": "string", - "title": "Measurement Method", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Education" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Education", + "Learn to Earn", + "Training", + "Translation" + ] + } } }, - "required": [ - "name", - "description", - "target", - "measurement" - ] - }, - "minItems": 2, - "maxItems": 5 - }, - "outputs": { - "type": "array", - "title": "Project Outputs", - "description": "Tangible outputs and deliverables from the project", - "items": { - "type": "string", - "minLength": 10, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 10, - "uniqueItems": true - } - }, - "required": [ - "communityBenefit", - "metrics", - "outputs" - ] - }, - "capability": { - "type": "object", - "title": "Capability & Feasibility", - "description": "Demonstrate your ability to deliver the project successfully", - "x-guidance":"

Please describe your existing capabilities that demonstrate how and why you believe you're best suited to deliver this project? Please include the steps or processes that demonstrate that you can be trusted to manage funds properly.

", - "properties": { - "teamExperience": { - "type": "string", - "title": "Team Experience", - "description": "Describe your team's relevant experience and capabilities", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "feasibilityApproach": { - "type": "string", - "title": "Feasibility Approach", - "description": "How will you validate the feasibility of your approach?", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "fundManagement": { - "type": "string", - "title": "Fund Management", - "description": "How will you ensure proper management and accountability of funds?", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "teamExperience", - "feasibilityApproach", - "fundManagement" - ] - } - }, - "required": [ - "solution", - "impact", - "capability" - ] - }, - "milestones": { - "type": "object", - "title": "Project Milestones", - "description": "Detailed project milestones and deliverables", - "x-guidance": "

A clear set of milestones and acceptance criteria will demonstrate your capability to deliver the project as proposed. More guidance on submitting milestones as part of your project proposal can be found here

For Grant Amounts of up to 75k ada, at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)

For Grant Amounts over 75k ada up to 150k ada, at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)

For Grant Amounts over 150k ada up to 300k ada, at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)

For Grant Amounts exceeding 300k ada, at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)

", - "properties": { - "milestonesConfig": { - "type": "object", - "title": "Milestones Configuration", - "description": "Configuration for number of milestones", - "properties": { - "grantAmount": { - "type": "number", - "title": "Grant Amount in ADA", - "description": "Total grant amount requested in ADA", - "minimum": 0, - "maximum": 1000000 - }, - "numberOfMilestones": { - "type": "integer", - "title": "Number of Milestones", - "description": "Total number of milestones including the final milestone", - "minimum": 2, - "maximum": 6 - } - }, - "required": [ - "grantAmount", - "numberOfMilestones" - ] - }, - "milestonesList": { - "type": "array", - "title": "List of Milestones", - "description": "Detailed description of each project milestone", - "items": { - "type": "object", - "title": "Milestone", - "properties": { - "title": { - "type": "string", - "title": "Milestone Title", - "description": "Short, descriptive title for the milestone", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "description": { - "type": "string", - "title": "Milestone Description", - "description": "Detailed description of what this milestone entails", - "minLength": 50, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "deliverables": { - "type": "array", - "title": "Deliverables", - "description": "Specific outputs and deliverables for this milestone", - "items": { - "type": "object", + { "properties": { - "name": { - "type": "string", - "title": "Deliverable Name", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Community & Outreach" }, - "description": { - "type": "string", - "title": "Deliverable Description", - "minLength": 20, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Connected Community", + "Community", + "Community Outreach", + "Social Media" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Development & Tools" }, - "type": { - "type": "string", + "tag": { + "$ref": "#/definitions/tagSelection", "enum": [ - "Documentation", - "Software", - "Report", - "Presentation", - "Video", - "Other" + "Developer Tools", + "L2", + "Infrastructure", + "Analytics", + "AI", + "Research", + "UTXO", + "P2P" ] } - }, - "required": [ - "name", - "description", - "type" - ] + } }, - "minItems": 1, - "maxItems": 5 - }, - "acceptanceCriteria": { - "type": "array", - "title": "Acceptance Criteria", - "description": "Specific criteria that must be met to consider this milestone complete", - "items": { - "type": "string", - "minLength": 10, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Identity & Security" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Identity & Verification", + "Cybersecurity", + "Security", + "Authentication", + "Privacy" + ] + } + } }, - "minItems": 1, - "maxItems": 5 - }, - "evidenceOfCompletion": { - "type": "array", - "title": "Evidence of Completion", - "description": "How will you demonstrate that this milestone is complete?", - "items": { - "type": "object", + { "properties": { - "type": { - "type": "string", - "title": "Evidence Type", + "group": { + "$ref": "#/definitions/tagGroup", + "const": "DeFi" + }, + "tag": { + "$ref": "#/definitions/tagSelection", "enum": [ - "Code Repository", - "Documentation", - "Demo Video", - "Test Results", - "Metrics Report", - "User Feedback", - "Other" + "DeFi", + "Payments", + "Stablecoin", + "Risk Management", + "Yield", + "Staking", + "Lending" ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Real World Applications" }, - "description": { - "type": "string", - "title": "Evidence Description", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Wallet", + "Marketplace", + "Manufacturing", + "IoT", + "Financial Services", + "E-commerce", + "Business Services", + "Supply Chain", + "Real Estate", + "Healthcare", + "Tourism", + "Entertainments", + "RWA", + "Music", + "Tokenization" + ] } - }, - "required": [ - "type", - "description" - ] + } }, - "minItems": 1, - "maxItems": 3 - }, - "timeline": { - "type": "object", - "title": "Timeline", - "properties": { - "startDate": { - "type": "string", - "title": "Start Date", - "format": "date" - }, - "endDate": { - "type": "string", - "title": "End Date", - "format": "date" - }, - "durationInWeeks": { - "type": "integer", - "title": "Duration in Weeks", - "minimum": 1, - "maximum": 52 + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Events & Marketing" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Events", + "Marketing", + "Hackathons", + "Accelerator", + "Incubator" + ] + } } }, - "required": [ - "startDate", - "endDate", - "durationInWeeks" - ] - }, - "budget": { - "type": "object", - "title": "Milestone Budget", - "properties": { - "amount": { - "type": "number", - "title": "Amount in ADA", - "minimum": 0 - }, - "breakdown": { - "type": "array", - "title": "Budget Breakdown", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "enum": [ - "Development", - "Design", - "Marketing", - "Operations", - "Other" - ] - }, - "amount": { - "type": "number", - "minimum": 0 - }, - "description": { - "type": "string", - "minLength": 10, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "category", - "amount", - "description" + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Interoperability" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Cross-chain", + "Interoperability", + "Off-chain", + "Legal", + "Policy Advocacy", + "Standards" ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Sustainability" }, - "minItems": 1 + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Sustainability", + "Environment", + "Agriculture" + ] + } } }, - "required": [ - "amount", - "breakdown" - ] - } - }, - "required": [ - "title", - "description", - "deliverables", - "acceptanceCriteria", - "evidenceOfCompletion", - "timeline", - "budget" - ] - }, - "minItems": 3, - "maxItems": 10 - } - }, - "required": [ - "milestonesConfig", - "milestonesList" - ] - }, - "finalPitch": { - "type": "object", - "title": "Final Pitch", - "description": "Final project pitch including team, budget, and value proposition", - "properties": { - "team": { - "type": "object", - "title": "Team Information", - "description": "Details about the project team and their capabilities", - "properties": { - "members": { - "type": "array", - "title": "Team Members", - "description": "List of team members and their roles", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "Full name of the team member", - "minLength": 2, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "role": { - "type": "string", - "title": "Role", - "description": "Primary role in the project", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "expertise": { - "type": "array", - "title": "Areas of Expertise", - "items": { - "type": "string", - "minLength": 3, - "maxLength": 50, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Smart Contracts" }, - "minItems": 1, - "maxItems": 5, - "uniqueItems": true - }, - "experience": { - "type": "string", - "title": "Relevant Experience", - "description": "Brief description of relevant experience", - "minLength": 50, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "links": { - "type": "array", - "title": "Professional Links", - "description": "Links to professional profiles or past work", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "LinkedIn", - "GitHub", - "Portfolio", - "Twitter", - "Website", - "Other" - ] - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", - "contentMediaType": "text/uri-list" - }, - "description": { - "type": "string", - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "type", - "url" + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Smart Contract", + "Smart Contracts", + "Audit", + "Oracles" ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "GameFi" }, - "maxItems": 5 + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Gaming", + "Gaming (GameFi)", + "Entertainment", + "Metaverse" + ] + } } }, - "required": [ - "name", - "role", - "expertise", - "experience" - ] - }, - "minItems": 1, - "maxItems": 10 + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "NFT" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "NFT", + "CNFT", + "Collectibles", + "Digital Twin" + ] + } + } + } + ] } - }, - "required": [ - "members" - ] + } + } + } + }, + "details": { + "$ref": "#/definitions/segment", + "title": "Your Project and Solution", + "properties": { + "solution": { + "$ref": "#/definitions/section", + "title": "Solution", + "description": "

How you write this section will depend on what type of proposal you are writing. You might want to include details on:


  • How do you perceive the problem you are solving?
  • What are your reasons for approaching it in the way that you have?
  • Who will your project engage?
  • How will you demonstrate or prove your impact?


Explain what is unique about your solution, who will benefit, and why this is important to Cardano.

", + "properties": { + "solution": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "minLength": 1, + "maxLength": 10240, + "examples": [ + "Our solution involves developing a decentralized education platform that will..." + ] + } + } }, - "budget": { - "type": "object", - "title": "Budget Details", - "description": "Detailed budget breakdown and justification", + "impact": { + "$ref": "#/definitions/section", + "title": "Impact", + "description": "

Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:


  • In what way will the success of your project bring value to the Cardano Community? 
  • How will you measure this impact? 
  • How will you share the outputs and opportunities that result from your project?
", "properties": { - "totalBudget": { - "type": "number", - "title": "Total Budget (ADA)", - "description": "Total amount requested in ADA", - "minimum": 0, - "maximum": 1000000 - }, - "categories": { - "type": "array", - "title": "Budget Categories", - "description": "Breakdown of budget by category", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "enum": [ - "Development", - "Design", - "Marketing", - "Operations", - "Research", - "Community Management", - "Legal", - "Other" - ] - }, - "amount": { - "type": "number", - "minimum": 0 - }, - "description": { - "type": "string", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "category", - "amount", - "description" - ] - }, - "minItems": 1 + "impact": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "minLength": 1, + "maxLength": 10240 } - }, - "required": [ - "totalBudget", - "categories" - ] + } }, - "valueProposition": { - "type": "object", - "title": "Value Proposition", - "description": "Justification of the project's value for money", + "feasibility": { + "$ref": "#/definitions/section", + "title": "Capabilities & Feasibility", + "description": "

Please describe your existing capabilities that demonstrate how and why you believe you’re best suited to deliver this project?

Please include the steps or processes that demonstrate that you can be trusted to manage funds properly.

", "properties": { - "costBenefitAnalysis": { - "type": "string", - "title": "Cost-Benefit Analysis", - "description": "Analysis of the project's costs versus its benefits to the Cardano ecosystem", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "longTermValue": { - "type": "string", - "title": "Long-term Value", - "description": "Description of the long-term value and sustainability of the project", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "communityBenefits": { - "type": "array", - "title": "Community Benefits", - "description": "Specific benefits to the Cardano community", - "items": { - "type": "string", - "minLength": 20, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 2, - "maxItems": 5 + "feasibility": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "minLength": 1, + "maxLength": 10240 } - }, - "required": [ - "costBenefitAnalysis", - "longTermValue", - "communityBenefits" - ] + } } - }, - "required": [ - "team", - "budget", - "valueProposition" - ] + } }, - "mandatoryAcknowledgments": { - "type": "object", - "title": "Mandatory Acknowledgments", - "description": "Required acknowledgments and agreements for proposal submission", + "milestones": { + "$ref": "#/definitions/segment", + "title": "Milestones", "properties": { - "fundRules": { - "type": "object", - "title": "Fund Rules Agreement", + "milestones": { + "$ref": "#/definitions/section", + "description": "

A clear set of milestones and acceptance criteria will demonstrate your capability to deliver the project as proposed. More guidance on submitting milestones as part of your project proposal can be found here.


Milestones guidance


  • For Grant Amounts of up to 75k ada, at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)
  • For Grant Amounts over 75k ada up to 150k ada, at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)
  • For Grant Amounts over 150k ada up to 300k ada, at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)
  • For Grant Amounts exceeding 300k ada, at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)
", "properties": { - "acknowledgment": { - "type": "boolean", - "title": "Fund Rules Acknowledgment", - "description": "I confirm that I have read and agree to be bound by the Fund Rules", - "const": true - }, - "version": { - "type": "string", - "title": "Fund Rules Version", - "description": "Version of the Fund Rules being acknowledged", - "pattern": "^F[0-9]{1,3}$", - "examples": [ - "F14", - "F15" - ] - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the rules were acknowledged", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] + "declared": { + "$ref": "#/definitions/multiLineTextEntryListMarkdown", + "minItems": 3, + "maxItems": 6 } - }, - "required": [ - "acknowledgment", - "version", - "timestamp" - ] - }, - "termsAndConditions": { - "type": "object", - "title": "Terms and Conditions Agreement", + } + } + } + }, + "pitch": { + "$ref": "#/definitions/segment", + "title": "Final Pitch", + "properties": { + "team": { + "$ref": "#/definitions/section", + "title": "Team", "properties": { - "acknowledgment": { - "type": "boolean", - "title": "Terms and Conditions Acknowledgment", - "description": "I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions", - "const": true - }, - "version": { - "type": "string", - "title": "Terms Version", - "description": "Version of the Terms and Conditions being acknowledged", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", - "examples": [ - "1.0.0", - "2.1.3" - ] - }, - "documentUrl": { - "type": "string", - "title": "Terms Document URL", - "description": "URL to the specific version of terms and conditions", - "format": "uri", - "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", - "contentMediaType": "text/html" - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the terms were acknowledged", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] + "who": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Who is in the project team and what are their roles?", + "description": "p>List your team, their Linkedin profiles (or similar) and state what aspect of the proposal’s work each team member will undertake.


If you are planning to recruit additional team members, please state what specific skills you will be looking for in the people you recruit, so readers can see that you understand what skills will be needed to complete the project.


You are expected to have already engaged the relevant members of the organizations referenced so you understand if they are willing and/or have capacity to support the project. If you have not taken any steps to engage with your team yet, it is likely that the resources will not be available if you are approved for funding, which can jeopardize the project before it has even begun. The Catalyst team cannot help with this, meaning you are expected to have understood the requirements and engaged the necessary people before submitting a proposal.


Have you engaged anyone on any of the technical group channels (eg Discord or Telegram), or do you have a direct line of communications with the people and resources required?


Important: Catalyst funding is not anonymous, and some level of ‘proof of life’ verifications will take place before initial funding is released. Also remember that your proposal will be publicly available, so make sure to obtain any consent required before including confidential or third party information.


All Project Participants must disclose their role and scope of services across any submitted proposals, even if they are not in the lead or co-proposer role, such as an implementer, vendor, service provider, etc. Failure to disclose this information may lead to disqualification from the current grant round.

", + "minLength": 1, + "maxLength": 10240 } - }, - "required": [ - "acknowledgment", - "version", - "timestamp", - "documentUrl" - ] + } }, - "privacyPolicy": { - "type": "object", - "title": "Privacy Policy Agreement", + "budget": { + "$ref": "#/definitions/section", + "title": "Budget & Costs", "properties": { - "acknowledgment": { - "type": "boolean", - "title": "Privacy Policy Acknowledgment", - "description": "I acknowledge and agree that any data I share will be processed in accordance with the Catalyst FCS Privacy Policy", - "const": true - }, - "version": { - "type": "string", - "title": "Privacy Policy Version", - "description": "Version of the Privacy Policy being acknowledged", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", - "examples": [ - "1.0.0", - "2.1.3" - ] - }, - "documentUrl": { - "type": "string", - "title": "Privacy Policy URL", - "description": "URL to the specific version of privacy policy", - "format": "uri", - "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", - "contentMediaType": "text/html" - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the privacy policy was acknowledged", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] + "costs": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Please provide a cost breakdown of the proposed work and resources", + "description": "

Make sure every element mentioned in your plan reflects its cost. It may be helpful to refer to your plan and timeline, list all the resources you will need at each stage, and what they cost.


Here, provide a clear description of any third party product or service you will be using. This could be hardware, software licenses, professional services (legal, accounting, code auditing, etc) but does not need to include the use of contracted programmers and developers.


The exact budget elements you include will depend on what type of work you are doing, and you might need to give less detail for a small, low-budget proposal. If the cost of the project will exceed the funding request, please provide information about alternative sources of funding.


Consider including budget elements for publicity / marketing / promotion / community engagement; project management; documentation; and reporting back to the community. Most proposals need these, but many proposers forget to include them.


It is the project team’s responsibility to properly manage the funds provided. Make sure to reference Fund Rules to understand eligibility around costs.

", + "minLength": 1, + "maxLength": 10240 } - }, - "required": [ - "acknowledgment", - "version", - "timestamp", - "documentUrl" - ] + } }, - "intellectualProperty": { - "type": "object", - "title": "Intellectual Property Declaration", + "value": { + "$ref": "#/definitions/section", + "title": "Value for Money", "properties": { - "acknowledgment": { - "type": "boolean", - "title": "IP Rights Acknowledgment", - "description": "I confirm that I have the necessary rights to all intellectual property included in this proposal", - "const": true - }, - "details": { - "type": "string", - "title": "IP Details", - "description": "Additional details about intellectual property rights (if applicable)", - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "attachments": { - "type": "array", - "title": "IP Documentation", - "description": "Supporting documentation for IP rights (if applicable)", - "items": { - "type": "object", - "properties": { - "documentType": { - "type": "string", - "enum": [ - "patent", - "trademark", - "copyright", - "license", - "other" - ], - "description": "Type of IP documentation" - }, - "documentUrl": { - "type": "string", - "format": "uri", - "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", - "contentMediaType": "application/pdf" - }, - "description": { - "type": "string", - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "documentType", - "documentUrl", - "description" - ] - }, - "maxItems": 10 - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the IP declaration was made", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] + "note": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "How does the cost of the project represent value for money for the Cardano ecosystem?", + "description": "

Use the response to provide the context about the costs you listed previously, particularly if they are high.


It may be helpful to include some brief information on how you have decided on the costs of the project. 


For instance, can you justify with supporting evidence that costs are proportional to the average wage in your country, or typical freelance rates in your industry? Is there anything else that helps to support how the project represents value for money?

", + "minLength": 1, + "maxLength": 10240 } - }, - "required": [ - "acknowledgment", - "timestamp" - ] - }, - "compliance": { - "type": "object", - "title": "Compliance Declaration", + } + } + } + }, + "agreements": { + "$ref": "#/definitions/segment", + "title": "Acknowledgements", + "properties": { + "mandatory": { + "$ref": "#/definitions/section", + "title": "Mandatory", "properties": { - "legalCompliance": { - "type": "boolean", - "title": "Legal Compliance", - "description": "I confirm that my proposal complies with all applicable laws and regulations", - "const": true - }, - "noConflictOfInterest": { - "type": "boolean", - "title": "No Conflict of Interest", - "description": "I confirm that there are no undisclosed conflicts of interest", - "const": true - }, - "accurateInformation": { - "type": "boolean", - "title": "Information Accuracy", - "description": "I confirm that all information provided is accurate and complete", - "const": true - }, - "jurisdictions": { - "type": "array", - "title": "Applicable Jurisdictions", - "description": "List of jurisdictions where compliance is declared", - "items": { - "type": "string", - "pattern": "^[A-Z]{2}$", - "description": "ISO 3166-1 alpha-2 country code" - }, - "minItems": 1, - "uniqueItems": true - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the compliance declaration was made", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] + "fund_rules": { + "$ref": "#/definitions/agreementConfirmation", + "title": "Fund Rules:", + "description": "

By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the Fund Rules.

" + }, + "terms_and_conditions": { + "$ref": "#/definitions/agreementConfirmation", + "title": "Terms and Conditions:", + "description": "

By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions.

" + }, + "privacy_policy": { + "$ref": "#/definitions/agreementConfirmation", + "title": "Privacy Policy: ", + "description": "

I acknowledge and agree that any data I share in connection with my participation in Project Catalyst Fund14 will be collected, stored, used and processed in accordance with the Catalyst FC’s Privacy Policy.

" } }, "required": [ - "legalCompliance", - "noConflictOfInterest", - "accurateInformation", - "jurisdictions", - "timestamp" + "fund_rules", + "terms_and_conditions", + "privacy_policy" + ], + "x-order": [ + "fund_rules", + "terms_and_conditions", + "privacy_policy" ] - }, - "additionalAcknowledgments": { - "type": "array", - "title": "Additional Acknowledgments", - "description": "Any additional acknowledgments required for specific proposal types", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "title": "Acknowledgment Type", - "minLength": 5, - "maxLength": 100, - "pattern": "^[a-zA-Z][a-zA-Z0-9_\\-\\.]*$" - }, - "acknowledgment": { - "type": "boolean", - "title": "Acknowledgment", - "const": true - }, - "description": { - "type": "string", - "title": "Description", - "description": "Detailed description of what is being acknowledged", - "minLength": 10, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "documentUrl": { - "type": "string", - "title": "Reference Document", - "description": "URL to the document being acknowledged", - "format": "uri", - "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", - "contentMediaType": "text/html" - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] - } - }, - "required": [ - "type", - "acknowledgment", - "description", - "timestamp" - ] - } } }, - "required": [ - "fundRules", - "termsAndConditions", - "privacyPolicy", - "intellectualProperty", - "compliance" + "x-order": [ + "mandatory" ] } - } + }, + "x-order": [ + "setup", + "summary", + "horizons", + "details", + "milestones", + "pitch", + "agreement" + ] } \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json index d524fec31ae..4b2d77cdbc3 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json @@ -24,7 +24,11 @@ }, "problem": { "statement": "Current challenge in the Cardano ecosystem...", - "impact": ["Technical Infrastructure", "Developer Tooling", "Adoption"] + "impact": [ + "Technical Infrastructure", + "Developer Tooling", + "Adoption" + ] }, "solution": { "summary": "Our solution provides a comprehensive toolkit...", @@ -51,7 +55,11 @@ "primaryCategory": "Development & Tools", "subCategory": "Developer Tools" }, - "tags": ["defi", "developer-tools", "infrastructure"], + "tags": [ + "defi", + "developer-tools", + "infrastructure" + ], "impact": { "timeframe": "Medium-term (6-18 months)", "scale": "Global", @@ -68,7 +76,10 @@ "solution": { "description": "Our solution provides a comprehensive toolkit...", "uniqueValue": "First integrated testing framework for Cardano", - "targetAudience": ["Cardano Developers", "DApp Teams"], + "targetAudience": [ + "Cardano Developers", + "DApp Teams" + ], "implementation": "We will use an agile development approach..." }, "impact": { @@ -94,6 +105,18 @@ } }, "milestones": { + "milestones": { + "declared": [ + "a", + "bb", + "cccccccccccccccccccccccc", + "dd", + "eee", + "fff" + ] + } + }, + "milestonez": { "milestonesConfig": { "grantAmount": 150000, "numberOfMilestones": 4 @@ -143,7 +166,10 @@ { "name": "John Doe", "role": "Project Lead", - "expertise": ["Blockchain Development", "Smart Contracts"], + "expertise": [ + "Blockchain Development", + "Smart Contracts" + ], "experience": "10 years in software development", "links": [ { @@ -200,7 +226,10 @@ "legalCompliance": true, "noConflictOfInterest": true, "accurateInformation": true, - "jurisdictions": ["US", "GB"], + "jurisdictions": [ + "US", + "GB" + ], "timestamp": "2024-01-20T15:30:00Z" } } diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/extra-definitions.txt b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/extra-definitions.txt new file mode 100644 index 00000000000..8278ccf1d2f --- /dev/null +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/extra-definitions.txt @@ -0,0 +1,1079 @@ +Not in the F13 proposal, or the F14 base document? + + + "proposalDetails": { + "type": "object", + "title": "Proposal Details", + "description": "Detailed information about your proposal's solution, impact, and feasibility", + "properties": { + "solution": { + "type": "object", + "title": "Solution Description", + "description": "Detailed description of your proposed solution", + "x-guidance": "

YOUR PROJECT AND SOLUTION

How you write this section will depend on what type of proposal you are writing. You might want to include details on:

  • How do you perceive the problem you are solving?
  • What are your reasons for approaching it in the way that you have?
  • Who will your project engage?
  • How will you demonstrate or prove your impact?

Explain what is unique about your solution, who will benefit, and why this is important to Cardano.

", + "properties": { + "description": { + "type": "string", + "title": "Solution Description", + "description": "Provide a comprehensive description of your proposed solution", + "minLength": 100, + "maxLength": 2000, + "examples": [ + "Our solution involves developing a decentralized education platform that will..." + ], + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "uniqueValue": { + "type": "string", + "title": "Unique Value Proposition", + "description": "What makes your solution unique and innovative?", + "minLength": 50, + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "targetAudience": { + "type": "array", + "title": "Target Audience", + "description": "Who will benefit from your solution?", + "items": { + "type": "string", + "minLength": 5, + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "minItems": 1, + "maxItems": 5, + "uniqueItems": true + }, + "implementation": { + "type": "string", + "title": "Implementation Approach", + "description": "How will you implement your solution?", + "minLength": 100, + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "description", + "uniqueValue", + "targetAudience", + "implementation" + ] + }, + "impact": { + "type": "object", + "title": "Project Impact", + "description": "Define and measure the impact of your project", + "x-guidance": "

Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:

  • In what way will the success of your project bring value to the Cardano Community?
  • How will you measure this impact?
  • How will you share the outputs and opportunities that result from your project?
", + "properties": { + "communityBenefit": { + "type": "string", + "title": "Community Benefit", + "description": "How will the Cardano community benefit from your project?", + "minLength": 100, + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "metrics": { + "type": "array", + "title": "Impact Metrics", + "description": "Specific metrics to measure project success", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Metric Name", + "minLength": 5, + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "description": { + "type": "string", + "title": "Metric Description", + "minLength": 20, + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "target": { + "type": "string", + "title": "Target Value", + "minLength": 1, + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "measurement": { + "type": "string", + "title": "Measurement Method", + "minLength": 20, + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "name", + "description", + "target", + "measurement" + ] + }, + "minItems": 2, + "maxItems": 5 + }, + "outputs": { + "type": "array", + "title": "Project Outputs", + "description": "Tangible outputs and deliverables from the project", + "items": { + "type": "string", + "minLength": 10, + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "minItems": 1, + "maxItems": 10, + "uniqueItems": true + } + }, + "required": [ + "communityBenefit", + "metrics", + "outputs" + ] + }, + "capability": { + "type": "object", + "title": "Capability & Feasibility", + "description": "Demonstrate your ability to deliver the project successfully", + "x-guidance": "

Please describe your existing capabilities that demonstrate how and why you believe you're best suited to deliver this project? Please include the steps or processes that demonstrate that you can be trusted to manage funds properly.

", + "properties": { + "teamExperience": { + "type": "string", + "title": "Team Experience", + "description": "Describe your team's relevant experience and capabilities", + "minLength": 100, + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "feasibilityApproach": { + "type": "string", + "title": "Feasibility Approach", + "description": "How will you validate the feasibility of your approach?", + "minLength": 100, + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "fundManagement": { + "type": "string", + "title": "Fund Management", + "description": "How will you ensure proper management and accountability of funds?", + "minLength": 100, + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "teamExperience", + "feasibilityApproach", + "fundManagement" + ] + } + }, + "required": [ + "solution", + "impact", + "capability" + ] +}, +"milestones": { + "type": "object", + "title": "Project Milestones", + "description": "Detailed project milestones and deliverables", + "x-guidance": "

A clear set of milestones and acceptance criteria will demonstrate your capability to deliver the project as proposed. More guidance on submitting milestones as part of your project proposal can be found here

For Grant Amounts of up to 75k ada, at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)

For Grant Amounts over 75k ada up to 150k ada, at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)

For Grant Amounts over 150k ada up to 300k ada, at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)

For Grant Amounts exceeding 300k ada, at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)

", + "properties": { + "milestonesConfig": { + "type": "object", + "title": "Milestones Configuration", + "description": "Configuration for number of milestones", + "properties": { + "grantAmount": { + "type": "number", + "title": "Grant Amount in ADA", + "description": "Total grant amount requested in ADA", + "minimum": 0, + "maximum": 1000000 + }, + "numberOfMilestones": { + "type": "integer", + "title": "Number of Milestones", + "description": "Total number of milestones including the final milestone", + "minimum": 2, + "maximum": 6 + } + }, + "required": [ + "grantAmount", + "numberOfMilestones" + ] + }, + "milestonesList": { + "type": "array", + "title": "List of Milestones", + "description": "Detailed description of each project milestone", + "items": { + "type": "object", + "title": "Milestone", + "properties": { + "title": { + "type": "string", + "title": "Milestone Title", + "description": "Short, descriptive title for the milestone", + "minLength": 5, + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "description": { + "type": "string", + "title": "Milestone Description", + "description": "Detailed description of what this milestone entails", + "minLength": 50, + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "deliverables": { + "type": "array", + "title": "Deliverables", + "description": "Specific outputs and deliverables for this milestone", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Deliverable Name", + "minLength": 5, + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "description": { + "type": "string", + "title": "Deliverable Description", + "minLength": 20, + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "type": { + "type": "string", + "enum": [ + "Documentation", + "Software", + "Report", + "Presentation", + "Video", + "Other" + ] + } + }, + "required": [ + "name", + "description", + "type" + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "acceptanceCriteria": { + "type": "array", + "title": "Acceptance Criteria", + "description": "Specific criteria that must be met to consider this milestone complete", + "items": { + "type": "string", + "minLength": 10, + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "minItems": 1, + "maxItems": 5 + }, + "evidenceOfCompletion": { + "type": "array", + "title": "Evidence of Completion", + "description": "How will you demonstrate that this milestone is complete?", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "title": "Evidence Type", + "enum": [ + "Code Repository", + "Documentation", + "Demo Video", + "Test Results", + "Metrics Report", + "User Feedback", + "Other" + ] + }, + "description": { + "type": "string", + "title": "Evidence Description", + "minLength": 20, + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "type", + "description" + ] + }, + "minItems": 1, + "maxItems": 3 + }, + "timeline": { + "type": "object", + "title": "Timeline", + "properties": { + "startDate": { + "type": "string", + "title": "Start Date", + "format": "date" + }, + "endDate": { + "type": "string", + "title": "End Date", + "format": "date" + }, + "durationInWeeks": { + "type": "integer", + "title": "Duration in Weeks", + "minimum": 1, + "maximum": 52 + } + }, + "required": [ + "startDate", + "endDate", + "durationInWeeks" + ] + }, + "budget": { + "type": "object", + "title": "Milestone Budget", + "properties": { + "amount": { + "type": "number", + "title": "Amount in ADA", + "minimum": 0 + }, + "breakdown": { + "type": "array", + "title": "Budget Breakdown", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": [ + "Development", + "Design", + "Marketing", + "Operations", + "Other" + ] + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "description": { + "type": "string", + "minLength": 10, + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "category", + "amount", + "description" + ] + }, + "minItems": 1 + } + }, + "required": [ + "amount", + "breakdown" + ] + } + }, + "required": [ + "title", + "description", + "deliverables", + "acceptanceCriteria", + "evidenceOfCompletion", + "timeline", + "budget" + ] + }, + "minItems": 3, + "maxItems": 10 + } + }, + "required": [ + "milestonesConfig", + "milestonesList" + ] +}, +"finalPitch": { + "type": "object", + "title": "Final Pitch", + "description": "Final project pitch including team, budget, and value proposition", + "properties": { + "team": { + "type": "object", + "title": "Team Information", + "description": "Details about the project team and their capabilities", + "properties": { + "members": { + "type": "array", + "title": "Team Members", + "description": "List of team members and their roles", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "Full name of the team member", + "minLength": 2, + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "role": { + "type": "string", + "title": "Role", + "description": "Primary role in the project", + "minLength": 5, + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "expertise": { + "type": "array", + "title": "Areas of Expertise", + "items": { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "minItems": 1, + "maxItems": 5, + "uniqueItems": true + }, + "experience": { + "type": "string", + "title": "Relevant Experience", + "description": "Brief description of relevant experience", + "minLength": 50, + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "links": { + "type": "array", + "title": "Professional Links", + "description": "Links to professional profiles or past work", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "LinkedIn", + "GitHub", + "Portfolio", + "Twitter", + "Website", + "Other" + ] + }, + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", + "contentMediaType": "text/uri-list" + }, + "description": { + "type": "string", + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "type", + "url" + ] + }, + "maxItems": 5 + } + }, + "required": [ + "name", + "role", + "expertise", + "experience" + ] + }, + "minItems": 1, + "maxItems": 10 + } + }, + "required": [ + "members" + ] + }, + "budget": { + "type": "object", + "title": "Budget Details", + "description": "Detailed budget breakdown and justification", + "properties": { + "totalBudget": { + "type": "number", + "title": "Total Budget (ADA)", + "description": "Total amount requested in ADA", + "minimum": 0, + "maximum": 1000000 + }, + "categories": { + "type": "array", + "title": "Budget Categories", + "description": "Breakdown of budget by category", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": [ + "Development", + "Design", + "Marketing", + "Operations", + "Research", + "Community Management", + "Legal", + "Other" + ] + }, + "amount": { + "type": "number", + "minimum": 0 + }, + "description": { + "type": "string", + "minLength": 20, + "maxLength": 300, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "category", + "amount", + "description" + ] + }, + "minItems": 1 + } + }, + "required": [ + "totalBudget", + "categories" + ] + }, + "valueProposition": { + "type": "object", + "title": "Value Proposition", + "description": "Justification of the project's value for money", + "properties": { + "costBenefitAnalysis": { + "type": "string", + "title": "Cost-Benefit Analysis", + "description": "Analysis of the project's costs versus its benefits to the Cardano ecosystem", + "minLength": 100, + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "longTermValue": { + "type": "string", + "title": "Long-term Value", + "description": "Description of the long-term value and sustainability of the project", + "minLength": 100, + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "communityBenefits": { + "type": "array", + "title": "Community Benefits", + "description": "Specific benefits to the Cardano community", + "items": { + "type": "string", + "minLength": 20, + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "minItems": 2, + "maxItems": 5 + } + }, + "required": [ + "costBenefitAnalysis", + "longTermValue", + "communityBenefits" + ] + } + }, + "required": [ + "team", + "budget", + "valueProposition" + ] +}, +"mandatoryAcknowledgments": { + "type": "object", + "title": "Mandatory Acknowledgments", + "description": "Required acknowledgments and agreements for proposal submission", + "properties": { + "fundRules": { + "type": "object", + "title": "Fund Rules Agreement", + "properties": { + "acknowledgment": { + "type": "boolean", + "title": "Fund Rules Acknowledgment", + "description": "I confirm that I have read and agree to be bound by the Fund Rules", + "const": true + }, + "version": { + "type": "string", + "title": "Fund Rules Version", + "description": "Version of the Fund Rules being acknowledged", + "pattern": "^F[0-9]{1,3}$", + "examples": [ + "F14", + "F15" + ] + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the rules were acknowledged", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": [ + "2024-01-20T15:30:00Z" + ] + } + }, + "required": [ + "acknowledgment", + "version", + "timestamp" + ] + }, + "termsAndConditions": { + "type": "object", + "title": "Terms and Conditions Agreement", + "properties": { + "acknowledgment": { + "type": "boolean", + "title": "Terms and Conditions Acknowledgment", + "description": "I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions", + "const": true + }, + "version": { + "type": "string", + "title": "Terms Version", + "description": "Version of the Terms and Conditions being acknowledged", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", + "examples": [ + "1.0.0", + "2.1.3" + ] + }, + "documentUrl": { + "type": "string", + "title": "Terms Document URL", + "description": "URL to the specific version of terms and conditions", + "format": "uri", + "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", + "contentMediaType": "text/html" + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the terms were acknowledged", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": [ + "2024-01-20T15:30:00Z" + ] + } + }, + "required": [ + "acknowledgment", + "version", + "timestamp", + "documentUrl" + ] + }, + "privacyPolicy": { + "type": "object", + "title": "Privacy Policy Agreement", + "properties": { + "acknowledgment": { + "type": "boolean", + "title": "Privacy Policy Acknowledgment", + "description": "I acknowledge and agree that any data I share will be processed in accordance with the Catalyst FCS Privacy Policy", + "const": true + }, + "version": { + "type": "string", + "title": "Privacy Policy Version", + "description": "Version of the Privacy Policy being acknowledged", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", + "examples": [ + "1.0.0", + "2.1.3" + ] + }, + "documentUrl": { + "type": "string", + "title": "Privacy Policy URL", + "description": "URL to the specific version of privacy policy", + "format": "uri", + "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", + "contentMediaType": "text/html" + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the privacy policy was acknowledged", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": [ + "2024-01-20T15:30:00Z" + ] + } + }, + "required": [ + "acknowledgment", + "version", + "timestamp", + "documentUrl" + ] + }, + "intellectualProperty": { + "type": "object", + "title": "Intellectual Property Declaration", + "properties": { + "acknowledgment": { + "type": "boolean", + "title": "IP Rights Acknowledgment", + "description": "I confirm that I have the necessary rights to all intellectual property included in this proposal", + "const": true + }, + "details": { + "type": "string", + "title": "IP Details", + "description": "Additional details about intellectual property rights (if applicable)", + "maxLength": 1000, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "attachments": { + "type": "array", + "title": "IP Documentation", + "description": "Supporting documentation for IP rights (if applicable)", + "items": { + "type": "object", + "properties": { + "documentType": { + "type": "string", + "enum": [ + "patent", + "trademark", + "copyright", + "license", + "other" + ], + "description": "Type of IP documentation" + }, + "documentUrl": { + "type": "string", + "format": "uri", + "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", + "contentMediaType": "application/pdf" + }, + "description": { + "type": "string", + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "documentType", + "documentUrl", + "description" + ] + }, + "maxItems": 10 + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the IP declaration was made", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": [ + "2024-01-20T15:30:00Z" + ] + } + }, + "required": [ + "acknowledgment", + "timestamp" + ] + }, + "compliance": { + "type": "object", + "title": "Compliance Declaration", + "properties": { + "legalCompliance": { + "type": "boolean", + "title": "Legal Compliance", + "description": "I confirm that my proposal complies with all applicable laws and regulations", + "const": true + }, + "noConflictOfInterest": { + "type": "boolean", + "title": "No Conflict of Interest", + "description": "I confirm that there are no undisclosed conflicts of interest", + "const": true + }, + "accurateInformation": { + "type": "boolean", + "title": "Information Accuracy", + "description": "I confirm that all information provided is accurate and complete", + "const": true + }, + "jurisdictions": { + "type": "array", + "title": "Applicable Jurisdictions", + "description": "List of jurisdictions where compliance is declared", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "ISO 3166-1 alpha-2 country code" + }, + "minItems": 1, + "uniqueItems": true + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "description": "When the compliance declaration was made", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": [ + "2024-01-20T15:30:00Z" + ] + } + }, + "required": [ + "legalCompliance", + "noConflictOfInterest", + "accurateInformation", + "jurisdictions", + "timestamp" + ] + }, + "additionalAcknowledgments": { + "type": "array", + "title": "Additional Acknowledgments", + "description": "Any additional acknowledgments required for specific proposal types", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "title": "Acknowledgment Type", + "minLength": 5, + "maxLength": 100, + "pattern": "^[a-zA-Z][a-zA-Z0-9_\\-\\.]*$" + }, + "acknowledgment": { + "type": "boolean", + "title": "Acknowledgment", + "const": true + }, + "description": { + "type": "string", + "title": "Description", + "description": "Detailed description of what is being acknowledged", + "minLength": 10, + "maxLength": 500, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "documentUrl": { + "type": "string", + "title": "Reference Document", + "description": "URL to the document being acknowledged", + "format": "uri", + "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", + "contentMediaType": "text/html" + }, + "timestamp": { + "type": "string", + "title": "Acknowledgment Timestamp", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", + "examples": [ + "2024-01-20T15:30:00Z" + ] + } + }, + "required": [ + "type", + "acknowledgment", + "description", + "timestamp" + ] + } + } + }, + "required": [ + "fundRules", + "termsAndConditions", + "privacyPolicy", + "intellectualProperty", + "compliance" + ] +} +} +} + "impact": { + "type": "object", + "title": "Project Impact", + "description": "Describe the expected impact of your project", + "properties": { + "timeframe": { + "type": "string", + "enum": [ + "Short-term (0-6 months)", + "Medium-term (6-18 months)", + "Long-term (18+ months)" + ], + "title": "Impact Timeframe", + "description": "Expected timeframe to see meaningful impact" + }, + "scale": { + "type": "string", + "enum": [ + "Local", + "Regional", + "Global" + ], + "title": "Impact Scale", + "description": "Geographic scale of impact" + }, + "metrics": { + "type": "array", + "title": "Impact Metrics", + "description": "Key metrics to measure project success", + "items": { + "type": "object", + "properties": { + "metric": { + "type": "string", + "title": "Metric Name", + "description": "Name of the metric", + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "target": { + "type": "string", + "title": "Target Value", + "description": "Target value or goal for this metric", + "maxLength": 100, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + }, + "measurement": { + "type": "string", + "title": "Measurement Method", + "description": "How this metric will be measured", + "maxLength": 200, + "pattern": "^[\\S\\s]*$", + "contentMediaType": "text/plain" + } + }, + "required": [ + "metric", + "target", + "measurement" + ] + }, + "minItems": 1, + "maxItems": 5 + } + }, + "required": [ + "timeframe", + "scale", + "metrics" + ] + } +}, +"required": [ + "primaryCategory", + "subCategory", + "tags", + "impact" +] +}, diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md index 055d0b00db9..96c4254bbb0 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md @@ -1,207 +1,215 @@ # F14-Generic Proposal Template + ## New Fields added to the proposal template **Problem:** - + Statement - + Impact (select): - + "Technical Infrastructure", - + "User Experience", - + "Developer Tooling", - + "Community Growth", - + "Economic Sustainability", - + "Interoperability", - + "Security", - + "Scalability", - + "Education", - + "Adoption" + +* Statement +* Impact (select): + * "Technical Infrastructure", + * "User Experience", + * "Developer Tooling", + * "Community Growth", + * "Economic Sustainability", + * "Interoperability", + * "Security", + * "Scalability", + * "Education", + * "Adoption" **Solution:** - + Summary - + Approach - + Innovative aspects + +* Summary +* Approach +* Innovative aspects **Supporting links:** - + Links - + Url - + Description - + Type (select): - + "GitHub Repository", - + "Documentation", - + "Whitepaper", - + "Website", - + "Marketing Material", - + "Technical Specification", - + "Research Paper", - + "Blog Post", - + "Social Media", - + "Other" - + Main Repository link - + Documentation link + +* Links + * URL + * Description + * Type (select): + * "GitHub Repository", + * "Documentation", + * "Whitepaper", + * "Website", + * "Marketing Material", + * "Technical Specification", + * "Research Paper", + * "Blog Post", + * "Social Media", + * "Other" +* Main Repository link +* Documentation link **Dependencies** - + detail: - + name - + description - + type: - + "Technical" - + "Organizational" - + "Legal" - + "Financial" - + "Other" - + mitigationPlan + +* detail: + * name + * description + * type: + * "Technical" + * "Organizational" + * "Legal" + * "Financial" + * "Other" + * mitigationPlan **Horizons:** - + tags: - + additional tags to be entered for the project -> on top of category and subcategory - + Impact: - + timeframe: - + "Short-term (0-6 months)" - + "Medium-term (6-18 months)" - + "Long-term (18+ months)" - + scale: - + "Local", - + "Regional", - + "Global" - + metrics: - + "metric name", - + "target", - + "measurement" - -- Product Details: - - Solution: - - description - + unique value proposition - + target audience - + implementation approach - - Impact: - + communityBenefit - + Metrics - + name - + description - + target - + measurement - + Project outputs - - Capability: - + Team Experience - + Feasibility Approach - + Fund Management: (How will you ensure proper management and accountability of funds?) - + +* tags: + * additional tags to be entered for the project -> on top of category and subcategory +* Impact: + * timeframe: + * "Short-term (0-6 months)" + * "Medium-term (6-18 months)" + * "Long-term (18+ months)" + * scale: + * "Local", + * "Regional", + * "Global" + * metrics: + * "metric name", + * "target", + * "measurement" + +* Product Details: + * Solution: + * description + * unique value proposition + * target audience + * implementation approach + * Impact: + * communityBenefit + * Metrics + * name + * description + * target + * measurement + * Project outputs + * Capability: + * Team Experience + * Feasibility Approach + * Fund Management: (How will you ensure proper management and accountability of funds?) **Milestones:** - + milestonesConfig: - + grantAmount - + numberOfMilestones - + milestonesList: - - milestone: - - title - + description - + deliverables - + name - + description - + type - + "Documentation" - + "Software" - + "Report" - + "Presentation" - + "Video" - + "Other" - + evidenceOfCompletion - + type - + "Code Repository" - + "Documentation" - + "Demo Video" - + "Test Results" - + "Metrics Report" - + "User Feedback" - + "Other" - + description - + timeline - + startDate - + endDate - + durationInWeeks - + budget - + amount - + breakdown - + category - + "Development" - + "Design" - + "Marketing" - + "Operations" - + "Other" - + amount - + description + +* milestonesConfig: + * grantAmount + * numberOfMilestones +* milestonesList: + * milestone: + * title + * description + * deliverables + * name + * description + * type + * "Documentation" + * "Software" + * "Report" + * "Presentation" + * "Video" + * "Other" + * evidenceOfCompletion + * type + * "Code Repository" + * "Documentation" + * "Demo Video" + * "Test Results" + * "Metrics Report" + * "User Feedback" + * "Other" + * description + * timeline + * startDate + * endDate + * durationInWeeks + * budget + * amount + * breakdown + * category + * "Development" + * "Design" + * "Marketing" + * "Operations" + * "Other" + * amount + * description **Final Pitch** - + team - + members - + name - + role - + expertise - + experience - + links - + type - + url - + description - + budget - + total budget - + budget categories - + category: - + "Development", - + "Design", - + "Marketing", - + "Operations", - + "Research", - + "Community Management", - + "Legal", - + "Other" - + amount - + description +* team + * members + * name + * role + * expertise + * experience + * links + * type + * url + * description +* budget + * total budget + * budget categories + * category: + * "Development", + * "Design", + * "Marketing", + * "Operations", + * "Research", + * "Community Management", + * "Legal", + * "Other" + * amount + * description **Value for money** - + costBenefitAnalysis - + longTermValue - + communityBenefits + +* costBenefitAnalysis +* longTermValue +* communityBenefits **Mandatory Acknowledgments** - + Fund Rules - + Acknowledgment - + Version - + Timestamp + +* Fund Rules + * Acknowledgment + * Version + * Timestamp - + Terms And Conditions - + Acknowledgment - + Version - + DocumentUrl - + Timestamp +* Terms And Conditions + * Acknowledgment + * Version + * DocumentUrl + * Timestamp - + Privacy Policy - + Acknowledgment - + Version - + DocumentUrl - + Timestamp +* Privacy Policy + * Acknowledgment + * Version + * DocumentUrl + * Timestamp - + Intellectual Property - + Acknowledgment - + Details - + Attachments - + Document Type - + "patent", - + "trademark", - + "copyright", - + "license", - + "other" - + Document Url - + Description +* Intellectual Property + * Acknowledgment + * Details + * Attachments + * Document Type + * "patent", + * "trademark", + * "copyright", + * "license", + * "other" + * Document Url + * Description - + Compliances (checkboxes) - + Legal Compliance - + No Conflict Of Interest - + Accurate Information - + Jurisdictions - + Timestamp +* Compliances (checkboxes) + * Legal Compliance + * No Conflict Of Interest + * Accurate Information + * Jurisdictions + * Timestamp - + Additional Acknowledgments - + Type - + Acknowledgment - + Description - + Document Url - + Timestamp \ No newline at end of file +* Additional Acknowledgments + * Type + * Acknowledgment + * Description + * Document Url + * Timestamp From 428885d997134d7b29f8c6ca3972e42912a16d9d Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Sun, 8 Dec 2024 12:04:38 +0300 Subject: [PATCH 14/25] fix: CI check failure, over spelling and markdown --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 8 +- .../F14-Generic/example.proposal.json | 212 ++++------------- .../proposal/F14-Generic/readModifications.md | 215 ------------------ 3 files changed, 44 insertions(+), 391 deletions(-) delete mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index dc5812c5be1..82d3cfb24c5 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -269,13 +269,13 @@ "title": "Translation Information", "description": "Information about the proposal's language and translation status", "properties": { - "translated": { + "isTranslated": { "$ref": "#/definitions/yesNoChoice", "title": "Auto-translated Status", "description": "Indicate if your proposal has been auto-translated into English from another language", "x-guidance": "

Tick YES if your proposal has been auto-translated into English from another language so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections. Tick NO if your proposal has not been auto-translated into English from another language

" }, - "original": { + "originalLanguage": { "$ref": "#/definitions/singleLineTextEntry", "title": "Original Language", "description": "If auto-translated, specify the original language of your proposal", @@ -287,7 +287,7 @@ "French" ] }, - "notes": { + "translationNotes": { "$ref": "#/definitions/multiLineTextEntry", "title": "Translation Notes", "description": "Additional notes about the translation or original language content", @@ -391,7 +391,7 @@ "approach" ] }, - "SupportingLinks": { + "supportingLinks": { "$ref": "#/definitions/section", "title": "Supporting Documentation", "description": "Additional resources and documentation for your proposal", diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json index 4b2d77cdbc3..17aafe4cd15 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json @@ -18,9 +18,9 @@ "duration": 6 }, "translation": { - "translated": false, - "original": "English", - "notes": "Original proposal in English" + "isTranslated": true, + "originalLanguage": "Spanish", + "translationNotes": "Translated using DeepL with manual review" }, "problem": { "statement": "Current challenge in the Cardano ecosystem...", @@ -38,199 +38,67 @@ "Automated integration tools" ] }, - "SupportingLinks": { - "links": [ - { - "url": "https://github.com/example/project", - "type": "GitHub Repository", - "description": "Project's main repository" - } - ], + "supportingLinks": { "mainRepository": "https://github.com/example/project", - "documentation": "https://docs.example.com" + "documentation": "https://docs.example.com", + "other": [ + "https://github.com/example/project" + ] + }, + "dependencies": { + "details": [] + }, + "open_source": { + "source_code": "MIT", + "documentation": "MIT", + "note": "All project outputs will be open source under MIT license" } }, "horizons": { - "category": { - "primaryCategory": "Development & Tools", - "subCategory": "Developer Tools" - }, - "tags": [ - "defi", - "developer-tools", - "infrastructure" - ], - "impact": { - "timeframe": "Medium-term (6-18 months)", - "scale": "Global", - "metrics": [ - { - "metric": "Developer Adoption", - "target": "500 active developers", - "measurement": "GitHub analytics" - } - ] + "theme": { + "grouped_tag": { + "group": "DeFi", + "tag": "Staking" + } } }, - "proposalDetails": { + "details": { "solution": { - "description": "Our solution provides a comprehensive toolkit...", - "uniqueValue": "First integrated testing framework for Cardano", - "targetAudience": [ - "Cardano Developers", - "DApp Teams" - ], - "implementation": "We will use an agile development approach..." + "solution": "Our solution provides a comprehensive toolkit..." }, "impact": { - "communityBenefit": "Significantly reduces development time and improves code quality", - "metrics": [ - { - "name": "Developer Adoption", - "description": "Number of active developers using the toolkit", - "target": "500 developers", - "measurement": "GitHub analytics and usage statistics" - } - ], - "outputs": [ - "Testing Framework", - "Documentation", - "Training Materials" - ] + "impact": "The project will significantly impact developer productivity..." }, - "capability": { - "teamExperience": "Our team has extensive experience in blockchain development...", - "feasibilityApproach": "We have already developed a proof of concept...", - "fundManagement": "Funds will be managed through a transparent process..." + "feasibility": { + "feasibility": "Our team has extensive experience in blockchain development..." } }, "milestones": { "milestones": { "declared": [ - "a", - "bb", - "cccccccccccccccccccccccc", - "dd", - "eee", - "fff" + "# Initial Setup and Planning\n\nProject setup and infrastructure...", + "# Core Development\n\nImplementation of main features...", + "# Testing and Documentation\n\nComprehensive testing and documentation...", + "# Final Release\n\nProject completion and deployment with Project Close-out Report and Video..." ] } }, - "milestonez": { - "milestonesConfig": { - "grantAmount": 150000, - "numberOfMilestones": 4 - }, - "milestonesList": [ - { - "title": "Initial Setup and Planning", - "description": "Project setup and detailed planning phase", - "deliverables": [ - { - "name": "Project Plan", - "description": "Detailed project planning documentation", - "type": "Documentation" - } - ], - "acceptanceCriteria": [ - "Completed project plan", - "Technical specifications approved" - ], - "evidenceOfCompletion": [ - { - "type": "Documentation", - "description": "Project planning documents and specifications" - } - ], - "timeline": { - "startDate": "2024-03-01", - "endDate": "2024-04-01", - "durationInWeeks": 4 - }, - "budget": { - "amount": 37500, - "breakdown": [ - { - "category": "Development", - "amount": 30000, - "description": "Initial development work" - } - ] - } - } - ] - }, - "finalPitch": { + "pitch": { "team": { - "members": [ - { - "name": "John Doe", - "role": "Project Lead", - "expertise": [ - "Blockchain Development", - "Smart Contracts" - ], - "experience": "10 years in software development", - "links": [ - { - "type": "GitHub", - "url": "https://github.com/johndoe", - "description": "GitHub Profile" - } - ] - } - ] + "who": "Our team consists of experienced blockchain developers..." }, "budget": { - "totalBudget": 150000, - "categories": [ - { - "category": "Development", - "amount": 100000, - "description": "Core development team costs" - } - ] + "costs": "Budget breakdown:\n- Development: 70%\n- Testing: 15%\n- Documentation: 15%" }, - "valueProposition": { - "costBenefitAnalysis": "The project provides significant value...", - "longTermValue": "The solution will continue to benefit the ecosystem...", - "communityBenefits": [ - "Increased developer productivity", - "Better code quality" - ] + "value": { + "note": "This project provides excellent value for money..." } }, - "mandatoryAcknowledgments": { - "fundRules": { - "acknowledgment": true, - "version": "F14", - "timestamp": "2024-01-20T15:30:00Z" - }, - "termsAndConditions": { - "acknowledgment": true, - "version": "1.0.0", - "documentUrl": "https://cardano.org/terms", - "timestamp": "2024-01-20T15:30:00Z" - }, - "privacyPolicy": { - "acknowledgment": true, - "version": "1.0.0", - "documentUrl": "https://cardano.org/privacy", - "timestamp": "2024-01-20T15:30:00Z" - }, - "intellectualProperty": { - "acknowledgment": true, - "timestamp": "2024-01-20T15:30:00Z" - }, - "compliance": { - "legalCompliance": true, - "noConflictOfInterest": true, - "accurateInformation": true, - "jurisdictions": [ - "US", - "GB" - ], - "timestamp": "2024-01-20T15:30:00Z" + "agreements": { + "mandatory": { + "fund_rules": true, + "terms_and_conditions": true, + "privacy_policy": true } } } \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md deleted file mode 100644 index 96c4254bbb0..00000000000 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/readModifications.md +++ /dev/null @@ -1,215 +0,0 @@ -# F14-Generic Proposal Template - -## New Fields added to the proposal template - -**Problem:** - -* Statement -* Impact (select): - * "Technical Infrastructure", - * "User Experience", - * "Developer Tooling", - * "Community Growth", - * "Economic Sustainability", - * "Interoperability", - * "Security", - * "Scalability", - * "Education", - * "Adoption" - -**Solution:** - -* Summary -* Approach -* Innovative aspects - -**Supporting links:** - -* Links - * URL - * Description - * Type (select): - * "GitHub Repository", - * "Documentation", - * "Whitepaper", - * "Website", - * "Marketing Material", - * "Technical Specification", - * "Research Paper", - * "Blog Post", - * "Social Media", - * "Other" -* Main Repository link -* Documentation link - -**Dependencies** - -* detail: - * name - * description - * type: - * "Technical" - * "Organizational" - * "Legal" - * "Financial" - * "Other" - * mitigationPlan - -**Horizons:** - -* tags: - * additional tags to be entered for the project -> on top of category and subcategory -* Impact: - * timeframe: - * "Short-term (0-6 months)" - * "Medium-term (6-18 months)" - * "Long-term (18+ months)" - * scale: - * "Local", - * "Regional", - * "Global" - * metrics: - * "metric name", - * "target", - * "measurement" - -* Product Details: - * Solution: - * description - * unique value proposition - * target audience - * implementation approach - * Impact: - * communityBenefit - * Metrics - * name - * description - * target - * measurement - * Project outputs - * Capability: - * Team Experience - * Feasibility Approach - * Fund Management: (How will you ensure proper management and accountability of funds?) - -**Milestones:** - -* milestonesConfig: - * grantAmount - * numberOfMilestones -* milestonesList: - * milestone: - * title - * description - * deliverables - * name - * description - * type - * "Documentation" - * "Software" - * "Report" - * "Presentation" - * "Video" - * "Other" - * evidenceOfCompletion - * type - * "Code Repository" - * "Documentation" - * "Demo Video" - * "Test Results" - * "Metrics Report" - * "User Feedback" - * "Other" - * description - * timeline - * startDate - * endDate - * durationInWeeks - * budget - * amount - * breakdown - * category - * "Development" - * "Design" - * "Marketing" - * "Operations" - * "Other" - * amount - * description -**Final Pitch** -* team - * members - * name - * role - * expertise - * experience - * links - * type - * url - * description -* budget - * total budget - * budget categories - * category: - * "Development", - * "Design", - * "Marketing", - * "Operations", - * "Research", - * "Community Management", - * "Legal", - * "Other" - * amount - * description - -**Value for money** - -* costBenefitAnalysis -* longTermValue -* communityBenefits - -**Mandatory Acknowledgments** - -* Fund Rules - * Acknowledgment - * Version - * Timestamp - -* Terms And Conditions - * Acknowledgment - * Version - * DocumentUrl - * Timestamp - -* Privacy Policy - * Acknowledgment - * Version - * DocumentUrl - * Timestamp - -* Intellectual Property - * Acknowledgment - * Details - * Attachments - * Document Type - * "patent", - * "trademark", - * "copyright", - * "license", - * "other" - * Document Url - * Description - -* Compliances (checkboxes) - * Legal Compliance - * No Conflict Of Interest - * Accurate Information - * Jurisdictions - * Timestamp - -* Additional Acknowledgments - * Type - * Acknowledgment - * Description - * Document Url - * Timestamp From 59e8a998dc1067ab6994cd7ed98bc8c26b1646d4 Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Mon, 9 Dec 2024 21:30:27 +0300 Subject: [PATCH 15/25] fix: added first wave of x-orders to ssegments and sections --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 82d3cfb24c5..642f89b6939 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -198,7 +198,7 @@ "$ref": "#/definitions/dropDownSingleSelect", "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", - "x-mad-guidance": "

Please select from one of the following:

  1. Individual
  2. Entity (Incorporated)
  3. Entity (Not Incorporated)
", + "x-guidance": "

Please select from one of the following:

  1. Individual
  2. Entity (Incorporated)
  3. Entity (Not Incorporated)
", "enum": [ "Individual", "Entity (Incorporated)", @@ -218,13 +218,15 @@ "required": [ "applicant", "type" - ] + ], + "x-order": ["applicant", "type", "coproposers"] } }, "required": [ "title", "proposer" - ] + ], + "x-order": ["title", "proposer"] }, "summary": { "$ref": "#/definitions/segment", @@ -246,7 +248,8 @@ }, "required": [ "requestedFunds" - ] + ], + "x-order": ["requestedFunds"] }, "time": { "$ref": "#/definitions/section", @@ -494,9 +497,20 @@ "required": [ "source_code", "documentation" - ] + ], + "x-order": ["source_code", "documentation", "note"] } - } + }, + "x-order": [ + "budget", + "time", + "translation", + "problem", + "solution", + "supportingLinks", + "dependencies", + "open_source" + ] }, "horizons": { "$ref": "#/definitions/segment", @@ -752,9 +766,11 @@ } ] } - } + }, + "x-order": ["theme"] } - } + }, + "x-order": ["theme"] }, "details": { "$ref": "#/definitions/segment", @@ -799,7 +815,8 @@ } } } - } + }, + "x-order": ["solution", "impact", "feasibility"] }, "milestones": { "$ref": "#/definitions/segment", @@ -814,9 +831,11 @@ "minItems": 3, "maxItems": 6 } - } + }, + "x-order": ["declared"] } - } + }, + "x-order": ["milestones"] }, "pitch": { "$ref": "#/definitions/segment", @@ -861,7 +880,8 @@ } } } - } + }, + "x-order": ["team", "budget", "value"] }, "agreements": { "$ref": "#/definitions/segment", @@ -899,9 +919,7 @@ ] } }, - "x-order": [ - "mandatory" - ] + "x-order": ["mandatory"] } }, "x-order": [ @@ -911,6 +929,6 @@ "details", "milestones", "pitch", - "agreement" + "agreements" ] } \ No newline at end of file From 3cc0b7ffa555dcd8d3de04b936c1345720d591a6 Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Thu, 12 Dec 2024 17:51:44 +0300 Subject: [PATCH 16/25] fix: Translation: added originalDocumentLink, if/then condition modified --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 642f89b6939..780593af2ba 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -282,44 +282,56 @@ "$ref": "#/definitions/singleLineTextEntry", "title": "Original Language", "description": "If auto-translated, specify the original language of your proposal", - "minLength": 2, - "maxLength": 50, - "examples": [ - "Spanish", + "enum": [ + "Arabic", + "Chinese", + "French", + "German", + "Indonesian", + "Italian", "Japanese", - "French" + "Korean", + "Portuguese", + "Russian", + "Spanish", + "Turkish", + "Vietnamese", + "Other" ] }, - "translationNotes": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "Translation Notes", - "description": "Additional notes about the translation or original language content", - "maxLength": 500 + "originalDocumentLink": { + "$ref": "#/definitions/singleLineHttpsURLEntry", + "title": "Original Document Link", + "description": "Provide a link to the original proposal document in its original language" } }, - "required": [ - "isTranslated" - ], - "dependencies": { - "originalLanguage": [ - "isTranslated" - ], - "translationNotes": [ - "isTranslated" - ] - }, "if": { "properties": { - "isTranslated": { - "const": true - } + "isTranslated": { "const": "Yes" } } }, "then": { - "required": [ - "originalLanguage" - ] - } + "required": ["originalLanguage", "originalDocumentLink"], + "properties": { + "originalLanguage": { + "description": "Original language is required when the proposal is translated" + }, + "originalDocumentLink": { + "description": "Link to the original document is required when the proposal is translated" + } + } + }, + "else": { + "properties": { + "originalLanguage": { + "not": {} + }, + "originalDocumentLink": { + "not": {} + } + } + }, + "required": ["isTranslated"] }, "problem": { "$ref": "#/definitions/section", From 9a02d58a2a8b67c17cfd35171b46ada0b8e448bb Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Sun, 15 Dec 2024 21:14:34 +0300 Subject: [PATCH 17/25] fix: milestones updated, added x-note to definitions, minor structure fixes --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 151 +++++++++++++----- 1 file changed, 113 insertions(+), 38 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 780593af2ba..7e28a70e995 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -13,49 +13,58 @@ "segment": { "$comment": "UI - Logical Document Section Break.", "type": "object", - "additionalProperties": false + "additionalProperties": false, + "x-note": "Major sections of the proposal. Each segment contains sections of information grouped together." }, "section": { "$comment": "UI - Logical Document Sub-Section Break.", "type": "object", - "additionalProperties": false + "additionalProperties": false, + "x-note": "Subsections containing specific details about the proposal." }, "singleLineTextEntry": { "$comment": "UI - Single Line text entry without any markup or rich text capability.", "type": "string", "contentMediaType": "text/plain", - "pattern": "^.*$" + "pattern": "^.*$", + "x-note": "Enter a single line of text. No formatting, line breaks, or special characters are allowed." }, "singleLineHttpsURLEntry": { "$comment": "UI - Single Line text entry for HTTPS Urls.", "type": "string", "format": "uri", - "pattern": "^https:.*" + "pattern": "^https:.*", + "x-note": "Enter a valid HTTPS URL. Must start with 'https://' and be a complete, working web address." }, "multiLineTextEntry": { "$comment": "UI - Multiline text entry without any markup or rich text capability.", "type": "string", "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]*$" + "pattern": "^[\\S\\s]*$", + "x-note": "Enter multiple lines of plain text. You can use line breaks but no special formatting." + }, "multiLineTextEntryMarkdown": { "$comment": "UI - Multiline text entry with Markdown content.", "type": "string", "contentMediaType": "text/markdown", - "pattern": "^[\\S\\s]*$" + "pattern": "^[\\S\\s]*$", + "x-note": "Use Markdown formatting for rich text. Available formatting:\n- Headers: # for h1, ## for h2, etc.\n- Lists: * or - for bullets, 1. for numbered\n- Emphasis: *italic* or **bold**\n- Links: [text](url)\n- Code: `inline` or ```block```" }, "dropDownSingleSelect": { "$comment": "UI - Drop Down Selection of a single entry from the defined enum.", "type": "string", "contentMediaType": "text/plain", "pattern": "^.*$", - "format": "dropDownSingleSelect" + "format": "dropDownSingleSelect", + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." }, "multiSelect": { "$comment": "UI - Multiselect from the given items.", "type": "array", "uniqueItems": true, - "format": "multiSelect" + "format": "multiSelect", + "x-note": "Select multiple options from the dropdown menu. Multiple choices are allowed." }, "singleLineTextEntryList": { "$comment": "UI - A Growable List of single line text (no markup or richtext).", @@ -66,7 +75,8 @@ "items": { "$ref": "#/definitions/singleLineTextEntry", "maxLength": 1024 - } + }, + "x-note": "Add multiple single-line text entries. Each entry should be unique and under 1024 characters." }, "multiLineTextEntryListMarkdown": { "$comment": "UI - A Growable List of markdown formatted text fields.", @@ -77,7 +87,8 @@ "items": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "maxLength": 10240 - } + }, + "x-note": "Add multiple markdown-formatted text entries. Each entry can include rich formatting and should be unique." }, "singleLineHttpsURLEntryList": { "$comment": "UI - A Growable List of HTTPS URLs.", @@ -88,68 +99,79 @@ "items": { "$ref": "#/definitions/singleLineHttpsURLEntry", "maxLength": 1024 - } + }, + "x-note": "Enter multiple HTTPS URLs. Each URL should be unique and under 1024 characters." }, "nestedQuestionsList": { "$comment": "UI - A Growable List of Questions. The contents are an object, that can have any UI elements within.", "type": "array", "format": "nestedQuestionsList", "uniqueItems": true, - "default": [] + "default": [], + "x-note": "Add multiple questions. Each question should be unique." }, "nestedQuestions": { "$comment": "UI - The container for a nested question set.", "type": "object", "format": "nestedQuestions", - "additionalProperties": false + "additionalProperties": false, + "x-note": "Add multiple questions. Each question should be unique." }, "singleGroupedTagSelector": { "$comment": "UI - A selector where a top level selection, gives a single choice from a list of tags.", "type": "object", "format": "singleGroupedTagSelector", - "additionalProperties": false + "additionalProperties": false, + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." }, "tagGroup": { "$comment": "UI - An individual group within a singleGroupedTagSelector.", "type": "string", "format": "tagGroup", - "pattern": "^.*$" + "pattern": "^.*$", + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." }, "tagSelection": { "$comment": "UI - An individual tag within the group of a singleGroupedTagSelector.", "type": "string", "format": "tagSelection", - "pattern": "^.*$" + "pattern": "^.*$", + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." }, "tokenValueCardanoADA": { "$comment": "UI - A Token Value denominated in Cardano ADA.", "type": "integer", - "format": "token:cardano:ada" + "format": "token:cardano:ada", + "x-note": "Enter the amount of Cardano ADA to be used in the proposal." }, "durationInMonths": { "$comment": "UI - A Duration represented in total months.", "type": "integer", - "format": "datetime:duration:months" + "format": "datetime:duration:months", + "x-note": "Enter the duration of the proposal in months." }, "yesNoChoice": { "$comment": "UI - A Boolean choice, represented as a Yes/No selection. Yes = true.", "type": "boolean", "format": "yesNoChoice", - "default": false + "default": false, + "x-note": "Select Yes or No." }, "agreementConfirmation": { "$comment": "UI - A Boolean choice, defaults to `false` but its invalid if its not set to `true`.", "type": "boolean", "format": "agreementConfirmation", "default": false, - "const": true + "const": true, + "x-note": "Select Yes or No." }, "spdxLicenseOrURL": { "$comment": "UI - Drop Down Selection of any valid SPDX Identifier. This is a complex type, it should let the user select one of the valid SPDX licenses, or enter a URL of the license if its proprietary. In the form its just a string.", "type": "string", "contentMediaType": "text/plain", "pattern": "^.*$", - "format": "spdxLicenseOrURL" + "format": "spdxLicenseOrURL", + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." } }, "type": "object", @@ -489,7 +511,7 @@ "open_source": { "$ref": "#/definitions/section", "title": "Project Open Source", - "description": "Will your project's output/be s fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", + "description": "Will your project's output be fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", "x-guidance": "

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

", "properties": { "source_code": { @@ -503,7 +525,7 @@ "title": "More Information", "description": "Please provide here more information on the open source status of your project outputs", "maxLength": 500, - "x-guidance": "

If you did not answer PROPRIETARY to the above questions, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository. Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs If only certain elements of your code will be open source please clarify which elements will be open source here. If you answered NO to the above question, please give further details as to why your projects outputs will not be open source METADATA

" + "x-guidance": "

If you did not answer PROPRIETARY to the above questions, the project should be open source available throughout the entire lifecycle of the project with a declared open-source repository. Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs If only certain elements of your code will be open source please clarify which elements will be open source here. If you answered NO to the above question, please give further details as to why your projects outputs will not be open source METADATA

" } }, "required": [ @@ -830,25 +852,78 @@ }, "x-order": ["solution", "impact", "feasibility"] }, +"milestones": { + "$ref": "#/definitions/segment", + "title": "Milestones", + "properties": { "milestones": { - "$ref": "#/definitions/segment", - "title": "Milestones", + "$ref": "#/definitions/section", + "title": "Project Milestones", + "description": "

Each milestone must declare:

  • A: Milestone outputs
  • B: Acceptance criteria
  • C: Evidence of completion

Requirements:

  • For Grant Amounts up to 75k ada: minimum 3 milestones (2 + final)
  • For Grant Amounts 75k-150k ada: minimum 4 milestones (3 + final)
  • The final milestone must include Project Close-out Report and Video
", "properties": { - "milestones": { - "$ref": "#/definitions/section", - "description": "

A clear set of milestones and acceptance criteria will demonstrate your capability to deliver the project as proposed. More guidance on submitting milestones as part of your project proposal can be found here.


Milestones guidance


  • For Grant Amounts of up to 75k ada, at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)
  • For Grant Amounts over 75k ada up to 150k ada, at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)
  • For Grant Amounts over 150k ada up to 300k ada, at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)
  • For Grant Amounts exceeding 300k ada, at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)
", - "properties": { - "declared": { - "$ref": "#/definitions/multiLineTextEntryListMarkdown", - "minItems": 3, - "maxItems": 6 + "milestone_list": { + "type": "array", + "title": "Milestones", + "description": "What are the key milestones you need to achieve in order to complete your project successfully?", + "x-guidance": "

Milestone Requirements:

  • For Grant Amounts of up to 75k ada: at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)
  • For Grant Amounts over 75k ada up to 150k ada: at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)
  • For Grant Amounts over 150k ada up to 300k ada: at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)
  • For Grant Amounts exceeding 300k ada: at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)
", + "minItems": 3, + "maxItems": 6, + "items": { + "type": "object", + "required": ["title", "outputs", "acceptance_criteria", "evidence", "delivery_month", "cost"], + "properties": { + "title": { + "$ref": "#/definitions/singleLineTextEntry", + "title": "Milestone Title", + "description": "A clear, concise title for this milestone", + "maxLength": 100 + }, + "outputs": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Milestone Outputs", + "description": "What will be delivered in this milestone", + "maxLength": 1000 + }, + "acceptance_criteria": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Acceptance Criteria", + "description": "Specific conditions that must be met", + "maxLength": 1000 + }, + "evidence": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Evidence of Completion", + "description": "How you will demonstrate achievement", + "maxLength": 1000 + }, + "delivery_month": { + "$ref": "#/definitions/durationInMonths", + "title": "Delivery Month", + "description": "The month when this milestone will be delivered", + "minimum": 1, + "maximum": 12 + }, + "cost": { + "$ref": "#/definitions/tokenValueCardanoADA", + "title": "Cost in ADA", + "description": "The cost of this milestone in ADA" + }, + "progress": { + "$ref": "#/definitions/dropDownSingleSelect", + "title": "Progress Status", + "description": "Current status of the milestone", + "enum": ["Not Started", "In Progress", "Completed", "Delayed"], + "default": "Not Started" + } } - }, - "x-order": ["declared"] + } } }, - "x-order": ["milestones"] - }, + "required": ["milestone_list"] + } + }, + "x-order": ["milestones"] +}, "pitch": { "$ref": "#/definitions/segment", "title": "Final Pitch", @@ -860,7 +935,7 @@ "who": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "title": "Who is in the project team and what are their roles?", - "description": "p>List your team, their Linkedin profiles (or similar) and state what aspect of the proposal’s work each team member will undertake.


If you are planning to recruit additional team members, please state what specific skills you will be looking for in the people you recruit, so readers can see that you understand what skills will be needed to complete the project.


You are expected to have already engaged the relevant members of the organizations referenced so you understand if they are willing and/or have capacity to support the project. If you have not taken any steps to engage with your team yet, it is likely that the resources will not be available if you are approved for funding, which can jeopardize the project before it has even begun. The Catalyst team cannot help with this, meaning you are expected to have understood the requirements and engaged the necessary people before submitting a proposal.


Have you engaged anyone on any of the technical group channels (eg Discord or Telegram), or do you have a direct line of communications with the people and resources required?


Important: Catalyst funding is not anonymous, and some level of ‘proof of life’ verifications will take place before initial funding is released. Also remember that your proposal will be publicly available, so make sure to obtain any consent required before including confidential or third party information.


All Project Participants must disclose their role and scope of services across any submitted proposals, even if they are not in the lead or co-proposer role, such as an implementer, vendor, service provider, etc. Failure to disclose this information may lead to disqualification from the current grant round.

", + "description": "

List your team, their Linkedin profiles (or similar) and state what aspect of the proposal’s work each team member will undertake.


If you are planning to recruit additional team members, please state what specific skills you will be looking for in the people you recruit, so readers can see that you understand what skills will be needed to complete the project.


You are expected to have already engaged the relevant members of the organizations referenced so you understand if they are willing and/or have capacity to support the project. If you have not taken any steps to engage with your team yet, it is likely that the resources will not be available if you are approved for funding, which can jeopardize the project before it has even begun. The Catalyst team cannot help with this, meaning you are expected to have understood the requirements and engaged the necessary people before submitting a proposal.


Have you engaged anyone on any of the technical group channels (eg Discord or Telegram), or do you have a direct line of communications with the people and resources required?


Important: Catalyst funding is not anonymous, and some level of ‘proof of life’ verifications will take place before initial funding is released. Also remember that your proposal will be publicly available, so make sure to obtain any consent required before including confidential or third party information.


All Project Participants must disclose their role and scope of services across any submitted proposals, even if they are not in the lead or co-proposer role, such as an implementer, vendor, service provider, etc. Failure to disclose this information may lead to disqualification from the current grant round.

", "minLength": 1, "maxLength": 10240 } From 3d81d9abf52fa5c051f6ec930aa9b52d9d9646cb Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Mon, 16 Dec 2024 17:41:19 +0300 Subject: [PATCH 18/25] fix: updated the example, translation example failing --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 2 +- .../F14-Generic/example.proposal.json | 68 ++++++++++++++----- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 7e28a70e995..52d0592bb77 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -329,7 +329,7 @@ }, "if": { "properties": { - "isTranslated": { "const": "Yes" } + "isTranslated": { "const": true } } }, "then": { diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json index 17aafe4cd15..01710279147 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json @@ -5,9 +5,12 @@ "title": "Example Catalyst Proposal" }, "proposer": { - "applicant": "John Doe", + "applicant": "John Smith", "type": "Individual", - "coproposers": [] + "coproposers": [ + "Jane Doe", + "Bob Wilson" + ] } }, "summary": { @@ -19,8 +22,8 @@ }, "translation": { "isTranslated": true, - "originalLanguage": "Spanish", - "translationNotes": "Translated using DeepL with manual review" + "originalLanguage":"German", + "originalDocumentLink": "https://example.com/original-doc" }, "problem": { "statement": "Current challenge in the Cardano ecosystem...", @@ -42,11 +45,19 @@ "mainRepository": "https://github.com/example/project", "documentation": "https://docs.example.com", "other": [ - "https://github.com/example/project" + "https://example.com/whitepaper", + "https://example.com/roadmap" ] }, "dependencies": { - "details": [] + "details": [ + { + "name": "External API Service", + "type": "Technical", + "description": "Integration with third-party API service", + "mitigationPlan": "Build fallback mechanisms and maintain alternative providers" + } + ] }, "open_source": { "source_code": "MIT", @@ -64,34 +75,57 @@ }, "details": { "solution": { - "solution": "Our solution provides a comprehensive toolkit..." + "solution": "Our solution involves developing a comprehensive toolkit that will enhance the Cardano developer experience..." }, "impact": { - "impact": "The project will significantly impact developer productivity..." + "impact": "The project will significantly impact developer productivity by reducing development time and improving code quality..." }, "feasibility": { - "feasibility": "Our team has extensive experience in blockchain development..." + "feasibility": "Our team has extensive experience in blockchain development and has successfully delivered similar projects..." } }, "milestones": { "milestones": { - "declared": [ - "# Initial Setup and Planning\n\nProject setup and infrastructure...", - "# Core Development\n\nImplementation of main features...", - "# Testing and Documentation\n\nComprehensive testing and documentation...", - "# Final Release\n\nProject completion and deployment with Project Close-out Report and Video..." + "milestone_list": [ + { + "title": "Initial Setup and Planning", + "outputs": "Project infrastructure setup and detailed planning documents", + "acceptance_criteria": "- Development environment configured\n- Detailed project plan approved", + "evidence": "- GitHub repository setup\n- Documentation of infrastructure\n- Project planning documents", + "delivery_month": 1, + "cost": 30000, + "progress": "Not Started" + }, + { + "title": "Core Development", + "outputs": "Implementation of main features", + "acceptance_criteria": "- Core features implemented\n- Unit tests passing", + "evidence": "- Code repository\n- Test results\n- Technical documentation", + "delivery_month": 3, + "cost": 60000, + "progress": "Not Started" + }, + { + "title": "Final Release and Documentation", + "outputs": "Project completion, documentation, and Project Close-out Report and Video", + "acceptance_criteria": "- All features implemented and tested\n- Documentation complete\n- Close-out report and video delivered", + "evidence": "- Final release\n- Complete documentation\n- Close-out report and video", + "delivery_month": 6, + "cost": 60000, + "progress": "Not Started" + } ] } }, "pitch": { "team": { - "who": "Our team consists of experienced blockchain developers..." + "who": "Our team consists of experienced blockchain developers with proven track records..." }, "budget": { - "costs": "Budget breakdown:\n- Development: 70%\n- Testing: 15%\n- Documentation: 15%" + "costs": "Budget breakdown:\n- Development (70%): 105,000 ADA\n- Testing (15%): 22,500 ADA\n- Documentation (15%): 22,500 ADA" }, "value": { - "note": "This project provides excellent value for money..." + "note": "This project provides excellent value for money by delivering essential developer tools..." } }, "agreements": { From 21f366b0808d1f1377f65bb9ea28a309c57376de Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Mon, 16 Dec 2024 19:38:11 +0300 Subject: [PATCH 19/25] fix: horizons grouping issue fixed --- .../0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 52d0592bb77..afa71e375b5 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -121,7 +121,7 @@ "$comment": "UI - A selector where a top level selection, gives a single choice from a list of tags.", "type": "object", "format": "singleGroupedTagSelector", - "additionalProperties": false, + "additionalProperties": true, "x-note": "Select one option from the dropdown menu. Only one choice is allowed." }, "tagGroup": { From d5e5a692327ddc649fb1c67dfc2963395a39adc0 Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Mon, 30 Dec 2024 21:47:44 +0300 Subject: [PATCH 20/25] Fix: converted all html formatted content to markdown --- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 273 ++++++------------ .../F14-Generic/example.proposal.json | 19 +- 2 files changed, 99 insertions(+), 193 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index afa71e375b5..746d52f9f0c 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -122,7 +122,7 @@ "type": "object", "format": "singleGroupedTagSelector", "additionalProperties": true, - "x-note": "Select one option from the dropdown menu. Only one choice is allowed." + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." }, "tagGroup": { "$comment": "UI - An individual group within a singleGroupedTagSelector.", @@ -195,10 +195,10 @@ "title": { "$ref": "#/definitions/singleLineTextEntry", "title": "Proposal Title", - "description": "

Proposal title

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.

", + "description": "**Proposal title**\n\nPlease note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.", "minLength": 1, "maxLength": 60, - "x-guidance": "

The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

" + "x-guidance": "The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important." } }, "required": [ @@ -212,7 +212,7 @@ "$ref": "#/definitions/singleLineTextEntry", "title": "Name and surname of main applicant", "description": "Name and surname of main applicant", - "x-guidance": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", + "x-guidance": "Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).", "minLength": 2, "maxLength": 100 }, @@ -220,7 +220,7 @@ "$ref": "#/definitions/dropDownSingleSelect", "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", - "x-guidance": "

Please select from one of the following:

  1. Individual
  2. Entity (Incorporated)
  3. Entity (Not Incorporated)
", + "x-guidance": "Please select from one of the following:\n\n1. Individual\n2. Entity (Incorporated)\n3. Entity (Not Incorporated)", "enum": [ "Individual", "Entity (Incorporated)", @@ -232,7 +232,7 @@ "$ref": "#/definitions/singleLineTextEntryList", "title": "Co-proposers and additional applicants", "description": "Co-proposers and additional applicants", - "x-guidance": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 14 rules for added detail.

", + "x-guidance": "List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. **IMPORTANT** A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 14 rules for added detail.", "maxItems": 5, "minItems": 0 } @@ -263,7 +263,7 @@ "$ref": "#/definitions/tokenValueCardanoADA", "title": "Requested funds in ADA", "description": "The amount of funding requested for your proposal", - "x-guidance": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:

Minimum Funding Amount per proposal:

Cardano Open: A15,000

Cardano Uses Cases: A15,000

Cardano Partners: A500,000

Maximum Funding Amount per proposal:

Cardano Open:

  • Developers (technical): A200,000
  • Ecosystem (non-technical): A100,000

Cardano Uses Cases:

  • Concept A150,000
  • Product: A500,000

Cardano Partners:

  • Enterprise R&D A2,000,000
  • Growth & Acceleration: A2,000,000
", + "x-guidance": "There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:\n\nMinimum Funding Amount per proposal:\n\nCardano Open: A15,000\nCardano Uses Cases: A15,000\nCardano Partners: A500,000\n\nMaximum Funding Amount per proposal:\n\nCardano Open:\n- Developers (technical): A200,000\n- Ecosystem (non-technical): A100,000\n\nCardano Uses Cases:\n- Concept A150,000\n- Product: A500,000\n\nCardano Partners:\n- Enterprise R&D A2,000,000\n- Growth & Acceleration: A2,000,000", "minimum": 15000, "maximum": 2000000 } @@ -280,7 +280,7 @@ "$ref": "#/definitions/durationInMonths", "title": "Project Duration in Months", "description": "Specify the expected duration of your project. Projects must be completable within 2-12 months.", - "x-guidance": "

Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", + "x-guidance": "Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months. If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months. If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.", "minimum": 2, "maximum": 12 } @@ -298,7 +298,7 @@ "$ref": "#/definitions/yesNoChoice", "title": "Auto-translated Status", "description": "Indicate if your proposal has been auto-translated into English from another language", - "x-guidance": "

Tick YES if your proposal has been auto-translated into English from another language so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections. Tick NO if your proposal has not been auto-translated into English from another language

" + "x-guidance": "Tick YES if your proposal has been auto-translated into English from another language so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections. Tick NO if your proposal has not been auto-translated into English from another language" }, "originalLanguage": { "$ref": "#/definitions/singleLineTextEntry", @@ -366,7 +366,7 @@ "description": "Clearly define the problem you aim to solve. This will be visible in the Catalyst voting app.", "minLength": 10, "maxLength": 200, - "x-guidance": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly. This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

" + "x-guidance": "Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly. This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail." }, "impact": { "$ref": "#/definitions/multiSelect", @@ -407,7 +407,7 @@ "description": "Briefly describe your solution. Focus on what you will do or create to solve the problem.", "minLength": 10, "maxLength": 200, - "x-guidance": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how' This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

" + "x-guidance": "Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a...' Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how'. This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail." }, "approach": { "$ref": "#/definitions/multiLineTextEntry", @@ -432,107 +432,19 @@ "$ref": "#/definitions/section", "title": "Supporting Documentation", "description": "Additional resources and documentation for your proposal", - "x-guidance": "

Here, provide links to yours or your partner organization's website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal. Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere. If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include https. Without these steps, the form will not be submittable and show errors

", - "properties": { - "mainRepository": { - "$ref": "#/definitions/singleLineHttpsURLEntry", - "title": "Main Code Repository", - "description": "Primary repository where the project's code will be hosted" - }, - "documentation": { - "$ref": "#/definitions/singleLineHttpsURLEntry", - "title": "Documentation URL", - "description": "Main documentation site or resource for the project" - }, - "other": { - "$ref": "#/definitions/singleLineHttpsURLEntryList", - "title": "Resource Links", - "description": "Links to any other relevant documentation, code repositories, or marketing materials. All links must use HTTPS.", - "minItems": 0, - "maxItems": 5 - } - } + "x-guidance": "Here, provide links to yours or your partner organization's website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal. Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere. If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include https. Without these steps, the form will not be submittable and show errors" }, "dependencies": { "$ref": "#/definitions/section", "title": "Project Dependencies", "description": "External dependencies and requirements for project success", - "x-guidance": "

If your project has any dependencies and prerequisites for your project's success, list them here. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", - "properties": { - "details": { - "$ref": "#/definitions/nestedQuestionsList", - "title": "Dependency Details", - "description": "List and describe each dependency", - "items": { - "$ref": "#/definitions/nestedQuestions", - "properties": { - "name": { - "$ref": "#/definitions/singleLineTextEntry", - "title": "Dependency Name", - "description": "Name of the organization, technology, or resource", - "maxLength": 100 - }, - "type": { - "$ref": "#/definitions/dropDownSingleSelect", - "title": "Dependency Type", - "description": "Type of dependency", - "enum": [ - "Technical", - "Organizational", - "Legal", - "Financial", - "Other" - ] - }, - "description": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "Description", - "description": "Explain why this dependency is essential and how it affects your project", - "maxLength": 500 - }, - "mitigationPlan": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "Mitigation Plan", - "description": "How will you handle potential issues with this dependency", - "maxLength": 300 - } - }, - "required": [ - "name", - "type", - "description" - ] - }, - "minItems": 0, - "maxItems": 10 - } - } + "x-guidance": "If your project has any dependencies and prerequisites for your project's success, list them here. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software." }, "open_source": { "$ref": "#/definitions/section", "title": "Project Open Source", "description": "Will your project's output be fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", - "x-guidance": "

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

", - "properties": { - "source_code": { - "$ref": "#/definitions/spdxLicenseOrURL" - }, - "documentation": { - "$ref": "#/definitions/spdxLicenseOrURL" - }, - "note": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "More Information", - "description": "Please provide here more information on the open source status of your project outputs", - "maxLength": 500, - "x-guidance": "

If you did not answer PROPRIETARY to the above questions, the project should be open source available throughout the entire lifecycle of the project with a declared open-source repository. Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs If only certain elements of your code will be open source please clarify which elements will be open source here. If you answered NO to the above question, please give further details as to why your projects outputs will not be open source METADATA

" - } - }, - "required": [ - "source_code", - "documentation" - ], - "x-order": ["source_code", "documentation", "note"] + "x-guidance": "Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software" } }, "x-order": [ @@ -557,8 +469,7 @@ "properties": { "grouped_tag": { "$ref": "#/definitions/singleGroupedTagSelector", - "oneOf": [ - { + "oneOf": [{ "properties": { "group": { "$ref": "#/definitions/tagGroup", @@ -813,22 +724,20 @@ "solution": { "$ref": "#/definitions/section", "title": "Solution", - "description": "

How you write this section will depend on what type of proposal you are writing. You might want to include details on:


  • How do you perceive the problem you are solving?
  • What are your reasons for approaching it in the way that you have?
  • Who will your project engage?
  • How will you demonstrate or prove your impact?


Explain what is unique about your solution, who will benefit, and why this is important to Cardano.

", + "description": "How you write this section will depend on what type of proposal you are writing. You might want to include details on:\n\n- How do you perceive the problem you are solving?\n- What are your reasons for approaching it in the way that you have?\n- Who will your project engage?\n- How will you demonstrate or prove your impact?\n\nExplain what is unique about your solution, who will benefit, and why this is important to Cardano.", "properties": { "solution": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "minLength": 1, "maxLength": 10240, - "examples": [ - "Our solution involves developing a decentralized education platform that will..." - ] + "x-guidance": "Our solution involves developing a decentralized education platform that will..." } } }, "impact": { "$ref": "#/definitions/section", "title": "Impact", - "description": "

Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:


  • In what way will the success of your project bring value to the Cardano Community? 
  • How will you measure this impact? 
  • How will you share the outputs and opportunities that result from your project?
", + "description": "Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:\n\n- In what way will the success of your project bring value to the Cardano Community?\n- How will you measure this impact?\n- How will you share the outputs and opportunities that result from your project?", "properties": { "impact": { "$ref": "#/definitions/multiLineTextEntryMarkdown", @@ -840,7 +749,7 @@ "feasibility": { "$ref": "#/definitions/section", "title": "Capabilities & Feasibility", - "description": "

Please describe your existing capabilities that demonstrate how and why you believe you’re best suited to deliver this project?

Please include the steps or processes that demonstrate that you can be trusted to manage funds properly.

", + "description": "Please describe your existing capabilities that demonstrate how and why you believe you're best suited to deliver this project?\n\nPlease include the steps or processes that demonstrate that you can be trusted to manage funds properly.", "properties": { "feasibility": { "$ref": "#/definitions/multiLineTextEntryMarkdown", @@ -852,78 +761,78 @@ }, "x-order": ["solution", "impact", "feasibility"] }, -"milestones": { - "$ref": "#/definitions/segment", - "title": "Milestones", - "properties": { "milestones": { - "$ref": "#/definitions/section", - "title": "Project Milestones", - "description": "

Each milestone must declare:

  • A: Milestone outputs
  • B: Acceptance criteria
  • C: Evidence of completion

Requirements:

  • For Grant Amounts up to 75k ada: minimum 3 milestones (2 + final)
  • For Grant Amounts 75k-150k ada: minimum 4 milestones (3 + final)
  • The final milestone must include Project Close-out Report and Video
", + "$ref": "#/definitions/segment", + "title": "Milestones", "properties": { - "milestone_list": { - "type": "array", - "title": "Milestones", - "description": "What are the key milestones you need to achieve in order to complete your project successfully?", - "x-guidance": "

Milestone Requirements:

  • For Grant Amounts of up to 75k ada: at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)
  • For Grant Amounts over 75k ada up to 150k ada: at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)
  • For Grant Amounts over 150k ada up to 300k ada: at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)
  • For Grant Amounts exceeding 300k ada: at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)
", - "minItems": 3, - "maxItems": 6, - "items": { - "type": "object", - "required": ["title", "outputs", "acceptance_criteria", "evidence", "delivery_month", "cost"], - "properties": { - "title": { - "$ref": "#/definitions/singleLineTextEntry", - "title": "Milestone Title", - "description": "A clear, concise title for this milestone", - "maxLength": 100 - }, - "outputs": { - "$ref": "#/definitions/multiLineTextEntryMarkdown", - "title": "Milestone Outputs", - "description": "What will be delivered in this milestone", - "maxLength": 1000 - }, - "acceptance_criteria": { - "$ref": "#/definitions/multiLineTextEntryMarkdown", - "title": "Acceptance Criteria", - "description": "Specific conditions that must be met", - "maxLength": 1000 - }, - "evidence": { - "$ref": "#/definitions/multiLineTextEntryMarkdown", - "title": "Evidence of Completion", - "description": "How you will demonstrate achievement", - "maxLength": 1000 - }, - "delivery_month": { - "$ref": "#/definitions/durationInMonths", - "title": "Delivery Month", - "description": "The month when this milestone will be delivered", - "minimum": 1, - "maximum": 12 - }, - "cost": { - "$ref": "#/definitions/tokenValueCardanoADA", - "title": "Cost in ADA", - "description": "The cost of this milestone in ADA" - }, - "progress": { - "$ref": "#/definitions/dropDownSingleSelect", - "title": "Progress Status", - "description": "Current status of the milestone", - "enum": ["Not Started", "In Progress", "Completed", "Delayed"], - "default": "Not Started" + "milestones": { + "$ref": "#/definitions/section", + "title": "Project Milestones", + "description": "Each milestone must declare:\n\n- A: Milestone outputs\n- B: Acceptance criteria\n- C: Evidence of completion\n\n**Requirements:**\n\n- For Grant Amounts up to 75k ada: minimum 3 milestones (2 + final)\n- For Grant Amounts 75k-150k ada: minimum 4 milestones (3 + final)\n- The final milestone must include Project Close-out Report and Video", + "properties": { + "milestone_list": { + "type": "array", + "title": "Milestones", + "description": "What are the key milestones you need to achieve in order to complete your project successfully?", + "x-guidance": "**Milestone Requirements:**\n\n- For Grant Amounts of up to 75k ada: at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (**3 milestones in total**)\n- For Grant Amounts over 75k ada up to 150k ada: at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (**4 milestones in total**)\n- For Grant Amounts over 150k ada up to 300k ada: at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (**5 milestones in total**)\n- For Grant Amounts exceeding 300k ada: at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (**6 milestones in total**)", + "minItems": 3, + "maxItems": 6, + "items": { + "type": "object", + "required": ["title", "outputs", "acceptance_criteria", "evidence", "delivery_month", "cost"], + "properties": { + "title": { + "$ref": "#/definitions/singleLineTextEntry", + "title": "Milestone Title", + "description": "A clear, concise title for this milestone", + "maxLength": 100 + }, + "outputs": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Milestone Outputs", + "description": "What will be delivered in this milestone", + "maxLength": 1000 + }, + "acceptance_criteria": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Acceptance Criteria", + "description": "Specific conditions that must be met", + "maxLength": 1000 + }, + "evidence": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Evidence of Completion", + "description": "How you will demonstrate achievement", + "maxLength": 1000 + }, + "delivery_month": { + "$ref": "#/definitions/durationInMonths", + "title": "Delivery Month", + "description": "The month when this milestone will be delivered", + "minimum": 1, + "maximum": 12 + }, + "cost": { + "$ref": "#/definitions/tokenValueCardanoADA", + "title": "Cost in ADA", + "description": "The cost of this milestone in ADA" + }, + "progress": { + "$ref": "#/definitions/dropDownSingleSelect", + "title": "Progress Status", + "description": "Current status of the milestone", + "enum": ["Not Started", "In Progress", "Completed", "Delayed"], + "default": "Not Started" + } + } } } - } + }, + "required": ["milestone_list"] } }, - "required": ["milestone_list"] - } - }, - "x-order": ["milestones"] -}, + "x-order": ["milestones"] + }, "pitch": { "$ref": "#/definitions/segment", "title": "Final Pitch", @@ -935,7 +844,7 @@ "who": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "title": "Who is in the project team and what are their roles?", - "description": "

List your team, their Linkedin profiles (or similar) and state what aspect of the proposal’s work each team member will undertake.


If you are planning to recruit additional team members, please state what specific skills you will be looking for in the people you recruit, so readers can see that you understand what skills will be needed to complete the project.


You are expected to have already engaged the relevant members of the organizations referenced so you understand if they are willing and/or have capacity to support the project. If you have not taken any steps to engage with your team yet, it is likely that the resources will not be available if you are approved for funding, which can jeopardize the project before it has even begun. The Catalyst team cannot help with this, meaning you are expected to have understood the requirements and engaged the necessary people before submitting a proposal.


Have you engaged anyone on any of the technical group channels (eg Discord or Telegram), or do you have a direct line of communications with the people and resources required?


Important: Catalyst funding is not anonymous, and some level of ‘proof of life’ verifications will take place before initial funding is released. Also remember that your proposal will be publicly available, so make sure to obtain any consent required before including confidential or third party information.


All Project Participants must disclose their role and scope of services across any submitted proposals, even if they are not in the lead or co-proposer role, such as an implementer, vendor, service provider, etc. Failure to disclose this information may lead to disqualification from the current grant round.

", + "description": "List your team, their Linkedin profiles (or similar) and state what aspect of the proposal's work each team member will undertake.\n\nIf you are planning to recruit additional team members, please state what specific skills you will be looking for in the people you recruit, so readers can see that you understand what skills will be needed to complete the project.\n\nYou are expected to have already engaged the relevant members of the organizations referenced so you understand if they are willing and/or have capacity to support the project. If you have not taken any steps to engage with your team yet, it is likely that the resources will not be available if you are approved for funding, which can jeopardize the project before it has even begun. The Catalyst team cannot help with this, meaning you are expected to have understood the requirements and engaged the necessary people before submitting a proposal.\n\nHave you engaged anyone on any of the technical group channels (eg Discord or Telegram), or do you have a direct line of communications with the people and resources required?\n\nImportant: Catalyst funding is not anonymous, and some level of 'proof of life' verifications will take place before initial funding is released. Also remember that your proposal will be publicly available, so make sure to obtain any consent required before including confidential or third party information.\n\nAll Project Participants must disclose their role and scope of services across any submitted proposals, even if they are not in the lead or co-proposer role, such as an implementer, vendor, service provider, etc. Failure to disclose this information may lead to disqualification from the current grant round.", "minLength": 1, "maxLength": 10240 } @@ -948,7 +857,7 @@ "costs": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "title": "Please provide a cost breakdown of the proposed work and resources", - "description": "

Make sure every element mentioned in your plan reflects its cost. It may be helpful to refer to your plan and timeline, list all the resources you will need at each stage, and what they cost.


Here, provide a clear description of any third party product or service you will be using. This could be hardware, software licenses, professional services (legal, accounting, code auditing, etc) but does not need to include the use of contracted programmers and developers.


The exact budget elements you include will depend on what type of work you are doing, and you might need to give less detail for a small, low-budget proposal. If the cost of the project will exceed the funding request, please provide information about alternative sources of funding.


Consider including budget elements for publicity / marketing / promotion / community engagement; project management; documentation; and reporting back to the community. Most proposals need these, but many proposers forget to include them.


It is the project team’s responsibility to properly manage the funds provided. Make sure to reference Fund Rules to understand eligibility around costs.

", + "description": "Make sure every element mentioned in your plan reflects its cost. It may be helpful to refer to your plan and timeline, list all the resources you will need at each stage, and what they cost.\n\nHere, provide a clear description of any third party product or service you will be using. This could be hardware, software licenses, professional services (legal, accounting, code auditing, etc) but does not need to include the use of contracted programmers and developers.\n\nThe exact budget elements you include will depend on what type of work you are doing, and you might need to give less detail for a small, low-budget proposal. If the cost of the project will exceed the funding request, please provide information about alternative sources of funding.\n\nConsider including budget elements for publicity / marketing / promotion / community engagement; project management; documentation; and reporting back to the community. Most proposals need these, but many proposers forget to include them.\n\nIt is the project team's responsibility to properly manage the funds provided. Make sure to reference [Fund Rules](https://docs.projectcatalyst.io/current-fund/fund-basics/fund-rules) to understand eligibility around costs.", "minLength": 1, "maxLength": 10240 } @@ -961,7 +870,7 @@ "note": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "title": "How does the cost of the project represent value for money for the Cardano ecosystem?", - "description": "

Use the response to provide the context about the costs you listed previously, particularly if they are high.


It may be helpful to include some brief information on how you have decided on the costs of the project. 


For instance, can you justify with supporting evidence that costs are proportional to the average wage in your country, or typical freelance rates in your industry? Is there anything else that helps to support how the project represents value for money?

", + "description": "Use the response to provide the context about the costs you listed previously, particularly if they are high.\n\nIt may be helpful to include some brief information on how you have decided on the costs of the project.\n\nFor instance, can you justify with supporting evidence that costs are proportional to the average wage in your country, or typical freelance rates in your industry? Is there anything else that helps to support how the project represents value for money?", "minLength": 1, "maxLength": 10240 } @@ -981,17 +890,17 @@ "fund_rules": { "$ref": "#/definitions/agreementConfirmation", "title": "Fund Rules:", - "description": "

By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the Fund Rules.

" + "description": "By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the [Fund Rules](https://docs.projectcatalyst.io/current-fund/fund-basics/fund-rules)." }, "terms_and_conditions": { "$ref": "#/definitions/agreementConfirmation", "title": "Terms and Conditions:", - "description": "

By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions.

" + "description": "By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the [Project Catalyst Terms and Conditions](https://docs.projectcatalyst.io/current-fund/fund-basics/project-catalyst-terms-and-conditions)." }, "privacy_policy": { "$ref": "#/definitions/agreementConfirmation", "title": "Privacy Policy: ", - "description": "

I acknowledge and agree that any data I share in connection with my participation in Project Catalyst Fund14 will be collected, stored, used and processed in accordance with the Catalyst FC’s Privacy Policy.

" + "description": "I acknowledge and agree that any data I share in connection with my participation in Project Catalyst Fund14 will be collected, stored, used and processed in accordance with the Catalyst FC's [Privacy Policy](https://docs.projectcatalyst.io/current-fund/fund-basics/project-catalyst-terms-and-conditions/catalyst-fc-privacy-policy)." } }, "required": [ diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json index 01710279147..7aa31deb128 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/example.proposal.json @@ -22,7 +22,7 @@ }, "translation": { "isTranslated": true, - "originalLanguage":"German", + "originalLanguage": "German", "originalDocumentLink": "https://example.com/original-doc" }, "problem": { @@ -50,14 +50,12 @@ ] }, "dependencies": { - "details": [ - { - "name": "External API Service", - "type": "Technical", - "description": "Integration with third-party API service", - "mitigationPlan": "Build fallback mechanisms and maintain alternative providers" - } - ] + "details": [{ + "name": "External API Service", + "type": "Technical", + "description": "Integration with third-party API service", + "mitigationPlan": "Build fallback mechanisms and maintain alternative providers" + }] }, "open_source": { "source_code": "MIT", @@ -86,8 +84,7 @@ }, "milestones": { "milestones": { - "milestone_list": [ - { + "milestone_list": [{ "title": "Initial Setup and Planning", "outputs": "Project infrastructure setup and detailed planning documents", "acceptance_criteria": "- Development environment configured\n- Detailed project plan approved", From 04d1155a9be57d894247c0c253f2513c8439536d Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Mon, 30 Dec 2024 21:50:26 +0300 Subject: [PATCH 21/25] Fix: min and max stabilized in all topic --- .../0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index 746d52f9f0c..505f6445818 100644 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -304,6 +304,8 @@ "$ref": "#/definitions/singleLineTextEntry", "title": "Original Language", "description": "If auto-translated, specify the original language of your proposal", + "minLength": 2, + "maxLength": 50, "enum": [ "Arabic", "Chinese", @@ -413,14 +415,19 @@ "$ref": "#/definitions/multiLineTextEntry", "title": "Technical Approach", "description": "Outline the technical approach or methodology you will use", - "maxLength": 500 + "maxLength": 500, + "minLength": 10 }, "innovationAspects": { "$ref": "#/definitions/singleLineTextEntryList", "title": "Innovation Aspects", "description": "Key innovative aspects of your solution", "minItems": 1, - "maxItems": 5 + "maxItems": 5, + "items": { + "maxLength": 200, + "minLength": 10 + } } }, "required": [ From e20e2d5e920d5fc0dcb75430d2184b38ff4be88c Mon Sep 17 00:00:00 2001 From: nathanbogale Date: Tue, 31 Dec 2024 15:21:51 +0300 Subject: [PATCH 22/25] fix: merge cleanup and alignment with main --- .config/dictionaries/project.dic | 15 +- .github/CODEOWNERS | 14 +- catalyst-gateway/Earthfile | 16 + catalyst-gateway/bin/Cargo.toml | 5 +- catalyst-gateway/bin/src/db/event/mod.rs | 5 +- .../bin/src/db/event/signed_docs/mod.rs | 32 + .../sql/insert_signed_documents.sql | 12 + .../bin/src/db/event/signed_docs/tests/mod.rs | 50 + catalyst-gateway/bin/src/settings/mod.rs | 4 - catalyst-gateway/blueprint.cue | 5 + catalyst-gateway/docker-compose.yml | 12 +- catalyst-gateway/event-db/Earthfile | 38 +- catalyst-gateway/event-db/blueprint.cue | 9 +- .../migrations/V2__signed_documents.sql | 8 +- catalyst-gateway/event-db/poetry.lock | 311 ++++ catalyst-gateway/event-db/pyproject.toml | 23 + .../queries/insert_signed_documents.sql | 22 + .../queries/select_signed_documents.sql.jinja | 12 + .../select_signed_documents_2.sql.jinja | 12 + .../event-db/tests/docker-compose.yml | 30 + .../tests/test_signed_docs_queries.py | 147 ++ catalyst_voices/.gitignore | 1 + catalyst_voices/Earthfile | 20 +- .../apps/voices/integration_test/Earthfile | 2 +- .../voices/integration_test/all_test.dart | 17 + .../voices/integration_test/app_test.dart | 137 +- .../integration_test/onboarding_test.dart | 52 + .../pageobject/app_bar_page.dart | 8 + .../pageobject/common_page.dart | 10 + .../pageobject/onboarding_page.dart | 159 ++ .../pageobject/overall_spaces_page.dart | 15 + .../pageobject/spaces_drawer_page.dart | 159 ++ .../types/registration_state.dart | 15 + .../utils/selector_utils.dart | 22 + .../utils/translations_utils.dart | 8 + .../apps/voices/lib/app/view/app.dart | 24 +- .../app/view/app_active_state_listener.dart | 49 + .../apps/voices/lib/app/view/app_content.dart | 13 +- .../lib/common/codecs/markdown_codec.dart | 60 + .../voices/lib/common/ext/guidance_ext.dart | 2 +- .../apps/voices/lib/common/ext/map_ext.dart | 7 + .../lib/common/ext/time_of_day_ext.dart | 10 + .../lib/common/formatters/date_formatter.dart | 68 + .../apps/voices/lib/configs/bootstrap.dart | 44 +- .../voices/lib/dependency/dependencies.dart | 123 +- .../pages/account/unlock_keychain_dialog.dart | 1 + .../campaign_admin_tools_dialog.dart | 348 +++++ .../campaign_admin_tools_events.dart | 354 +++++ .../campaign_admin_tools_views.dart | 60 + .../details/campaign_details_dialog.dart | 69 + .../widgets/campaign_categories_tile.dart | 165 +++ .../widgets/campaign_details_tile.dart | 210 +++ .../details/widgets/campaign_header.dart | 22 + .../details/widgets/campaign_management.dart | 137 ++ .../widgets/campaign_management_dialog.dart | 127 ++ .../widgets/campaign_sections_list_view.dart | 41 + .../pages/discovery/current_status_text.dart | 10 +- .../lib/pages/discovery/discovery_page.dart | 428 +++++- .../pages/discovery/toggle_state_text.dart | 65 +- .../funded_projects/funded_projects_page.dart | 8 +- .../apps/voices/lib/pages/login/login.dart | 3 - .../voices/lib/pages/login/login_button.dart | 34 - .../pages/login/login_email_text_filed.dart | 30 - .../voices/lib/pages/login/login_form.dart | 72 - .../voices/lib/pages/login/login_page.dart | 13 - .../login/login_password_text_field.dart | 38 - .../space/treasury_overview.dart | 6 +- .../spaces_overview_list_view.dart | 53 +- .../proposal_builder/proposal_builder.dart | 1 + .../proposal_builder_body.dart} | 8 +- .../proposal_builder_guidance_view.dart} | 16 +- .../proposal_builder_navigation_panel.dart} | 4 +- .../proposal_builder_page.dart | 108 ++ .../proposal_builder_rich_text_step.dart} | 37 +- .../proposal_builder_setup_panel.dart | 63 + .../get_started/get_started_panel.dart | 4 +- .../registration_details_panel.dart | 1 + .../registration/registration_info_panel.dart | 1 + .../widgets/information_panel.dart | 8 +- .../widgets/registration_stage_message.dart | 2 + .../pages/spaces/drawer/discovery_menu.dart | 5 + .../lib/pages/spaces/drawer/guest_menu.dart | 1 + .../drawer/individual_private_campaigns.dart | 1 + .../spaces/drawer/my_private_proposals.dart | 26 +- .../lib/pages/spaces/drawer/space_header.dart | 1 + .../pages/spaces/drawer/spaces_drawer.dart | 40 +- .../pages/spaces/drawer/voting_rounds.dart | 1 + .../lib/pages/spaces/spaces_shell_page.dart | 192 ++- .../treasury_campaign_categories_step.dart | 34 + .../treasury_campaign_details_step.dart} | 8 +- .../treasury_campaign_stages_edit_step.dart | 162 ++ .../treasury_campaign_stages_view_step.dart | 151 ++ .../steps/treasury_campaign_widgets.dart | 106 ++ .../treasury_proposal_template_step.dart | 34 + .../lib/pages/treasury/treasury_body.dart | 35 +- .../lib/pages/treasury/treasury_page.dart | 20 +- .../voices/lib/pages/voting/voting_page.dart | 8 +- .../lib/pages/workspace/rich_text/answer.dart | 5 - .../workspace/rich_text/bonus_mark_up.dart | 78 - .../delivery_and_accountability.dart | 53 - .../rich_text/feasibility_checks.dart | 12 - .../rich_text/problem_statement.dart | 8 - .../rich_text/public_description.dart | 121 -- .../rich_text/solution_statement.dart | 8 - .../lib/pages/workspace/rich_text/title.dart | 8 - .../workspace/rich_text/value_for_money.dart | 16 - .../workspace/workspace_empty_state.dart | 36 + .../lib/pages/workspace/workspace_error.dart | 50 + .../lib/pages/workspace/workspace_header.dart | 230 +++ .../pages/workspace/workspace_loading.dart | 39 + .../lib/pages/workspace/workspace_page.dart | 171 +-- .../pages/workspace/workspace_proposals.dart | 79 + .../workspace/workspace_setup_panel.dart | 100 -- .../apps/voices/lib/routes/app_router.dart | 3 +- .../lib/routes/guards/user_access_guard.dart | 46 + .../lib/routes/routing/login_route.dart | 15 - .../routes/routing/overall_spaces_route.dart | 8 +- .../voices/lib/routes/routing/routes.dart | 2 - .../voices/lib/routes/routing/routing.dart | 1 - .../lib/routes/routing/spaces_route.dart | 77 +- .../session/session_action_header.dart | 1 + .../lib/widgets/buttons/voices_buttons.dart | 2 + .../widgets/cards/campaign_stage_card.dart | 126 ++ .../widgets/cards/funded_proposal_card.dart | 10 +- .../lib/widgets/cards/guidance_card.dart | 14 +- .../widgets/cards/pending_proposal_card.dart | 84 +- .../lib/widgets/cards/proposal_card.dart | 56 + .../lib/widgets/common/affix_decorator.dart | 7 +- .../common/proposal_status_container.dart | 12 +- .../containers/grey_out_container.dart | 23 + .../containers/workspace_tile_container.dart | 2 +- .../document_builder/document_property.dart | 34 + .../lib/widgets/drawer/voices_drawer.dart | 4 + .../drawer/voices_drawer_space_chooser.dart | 26 +- .../lib/widgets/empty_state/empty_state.dart | 76 + .../lib/widgets/headers/brand_header.dart | 1 + .../lib/widgets/headers/segment_header.dart | 4 +- .../widgets/images/voices_image_scheme.dart | 23 + .../widgets/indicators/voices_indicator.dart | 2 +- .../lib/widgets/menu/voices_modal_menu.dart | 179 +++ .../lib/widgets/menu/voices_node_menu.dart | 6 +- .../details/voices_align_title_header.dart | 54 + .../modals/details/voices_details_dialog.dart | 39 + .../details/voices_details_dialog_header.dart | 161 ++ .../widgets/modals/voices_desktop_dialog.dart | 52 +- .../modals/voices_upload_file_dialog.dart | 17 +- .../navigation/sections_controller.dart | 38 +- .../navigation/sections_list_view.dart | 62 +- .../lib/widgets/navigation/sections_menu.dart | 4 +- .../pickers/voices_calendar_picker.dart | 93 ++ .../widgets/pickers/voices_time_picker.dart | 141 ++ .../widgets/rich_text/voices_rich_text.dart | 10 +- .../widgets/text_field/voices_date_field.dart | 223 +++ .../text_field/voices_date_time_field.dart | 328 ++++ .../voices_date_time_text_field.dart | 96 ++ .../voices_password_text_field.dart | 5 + .../widgets/text_field/voices_text_field.dart | 87 +- .../widgets/text_field/voices_time_field.dart | 175 +++ .../widgets/tiles/voices_expansion_tile.dart | 107 ++ .../lib/widgets/tiles/voices_nav_tile.dart | 1 + .../toggles/voices_theme_mode_switch.dart | 1 + .../apps/voices/lib/widgets/widgets.dart | 4 + .../Flutter/GeneratedPluginRegistrant.swift | 2 + catalyst_voices/apps/voices/pubspec.yaml | 6 + .../common/codecs/markdown_codec_test.dart | 89 ++ .../cards/campaign_stage_card_test.dart | 154 ++ .../text_field/voices_text_field_test.dart | 4 +- catalyst_voices/justfile | 7 +- catalyst_voices/melos.yaml | 33 +- .../assets/images/no_proposal_foreground.svg | 41 + .../lib/src/admin_tools/admin_tools.dart | 2 + .../src/admin_tools/admin_tools_cubit.dart | 35 + .../src/admin_tools/admin_tools_state.dart | 30 + .../src/authentication/authentication.dart | 1 - .../authentication/authentication_bloc.dart | 67 - .../authentication/authentication_event.dart | 13 - .../authentication/authentication_state.dart | 28 - .../campaign_builder/campaign_builder.dart | 1 + .../campaign_builder_cubit.dart | 50 + .../campaign_builder_state.dart | 32 + .../campaign/details/campaign_details.dart | 3 + .../details/campaign_details_bloc.dart | 56 + .../details/campaign_details_event.dart | 16 + .../details/campaign_details_state.dart | 29 + .../lib/src/campaign/info/campaign_info.dart | 2 + .../campaign/info/campaign_info_cubit.dart | 72 + .../campaign/info/campaign_info_state.dart | 16 + .../lib/src/catalyst_voices_blocs.dart | 9 +- .../lib/src/login/login.dart | 1 - .../lib/src/login/login_bloc.dart | 87 -- .../lib/src/login/login_event.dart | 30 - .../lib/src/login/login_state.dart | 35 - .../proposal_builder/proposal_builder.dart | 3 + .../proposal_builder_bloc.dart | 117 ++ .../proposal_builder_event.dart | 40 + .../proposal_builder_state.dart | 47 + .../lib/src/proposals/proposals.dart | 2 + .../lib/src/proposals/proposals_cubit.dart | 146 ++ .../lib/src/proposals/proposals_state.dart | 29 + .../registration/cubits/recover_cubit.dart | 12 +- .../lib/src/session/session_cubit.dart | 158 +- .../lib/src/session/session_state.dart | 43 + .../lib/src/workspace/workspace.dart | 3 + .../lib/src/workspace/workspace_bloc.dart | 99 ++ .../lib/src/workspace/workspace_event.dart | 38 + .../lib/src/workspace/workspace_state.dart | 60 + .../catalyst_voices_blocs/pubspec.yaml | 2 + .../admin_tools/admin_tools_cubit_test.dart | 54 + .../info/campaign_info_cubit_test.dart | 112 ++ .../test/proposals/proposals_cubit_test.dart | 203 +++ .../test/session/session_cubit_test.dart | 127 +- .../lib/l10n/intl_en.arb | 252 +++- .../analysis_options.yaml | 2 +- .../catalyst_voices_models/build.yaml | 6 + .../lib/src/app_config.dart | 56 + .../lib/src/auth/authentication_status.dart | 5 - .../lib/src/campaign/campaign.dart | 67 + .../lib/src/campaign/campaign_category.dart | 14 + .../lib/src/campaign/campaign_publish.dart | 9 + .../lib/src/campaign/campaign_section.dart | 24 + .../lib/src/catalyst_voices_models.dart | 20 +- .../lib/src/crypto/keychain_metadata.dart | 46 - .../lib/src/document/document_json.dart | 1 - .../document_builder/document_builder.dart | 82 + .../document_definitions.dart | 567 +++++++ .../src/document_builder/document_schema.dart | 121 ++ .../lib/src/markdown_data.dart | 1 + .../lib/src/proposal/funded_proposal.dart | 37 - .../lib/src/proposal}/guidance.dart | 26 +- .../lib/src/proposal/pending_proposal.dart | 43 - .../lib/src/proposal/proposal.dart | 65 + .../lib/src/proposal/proposal_section.dart | 78 + .../lib/src/proposal/proposal_status.dart | 9 - .../lib/src/proposal/proposal_template.dart | 15 + .../lib/src/session_data.dart | 43 - .../lib/src/user/account.dart | 74 +- .../lib/src/user/user.dart | 47 +- .../catalyst_voices_models/pubspec.yaml | 7 +- .../test/crypto/keychain_metadata_test.dart | 52 - .../catalyst_voices_repositories/README.md | 31 + .../analysis_options.yaml | 2 +- .../catalyst_voices_repositories/build.yaml | 26 + .../lib/generated/api/client_index.dart | 2 + .../lib/generated/api}/client_mapping.dart | 0 .../lib/src/api_models/overriden_models.dart | 100 ++ .../lib/src/authentication_repository.dart | 66 - .../lib/src/campaign/campaign_repository.dart | 153 ++ .../lib/src/catalyst_voices_repositories.dart | 7 +- .../lib/src/config/config_repository.dart | 23 + .../src/credentials_storage_repository.dart | 51 - .../lib/src/dto/app_config_dto.dart | 121 ++ .../lib/src/dto/document_builder_dto.dart | 227 +++ .../lib/src/dto/document_definitions_dto.dart | 928 ++++++++++++ .../lib/src/dto/document_schema_dto.dart | 353 +++++ .../lib/src/dto/user_dto.dart | 159 ++ .../lib/src/proposal/proposal_repository.dart | 102 ++ .../lib/src/user/user_repository.dart | 46 + .../lib/src/user/user_storage.dart | 43 + .../lib/src/utils/json_converters.dart | 33 + .../openapi-filters.json | 0 .../openapi/vit.yaml | 1313 +++++++++++++++++ .../catalyst_voices_repositories/pubspec.yaml | 13 +- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 1067 ++++++++++++++ .../test/assets/generic_proposal.json | 138 ++ .../test/helpers/read_json.dart | 10 + .../document_builder_test.dart | 51 + .../document_definitions_test.dart | 54 + .../document_schema_test.dart | 60 + .../catalyst_voices_services/README.md | 36 - .../catalyst_voices_services/build.yaml | 15 - .../catalyst_gateway/client_index.dart | 1 - .../lib/src/campaign/campaign_service.dart | 36 + .../lib/src/catalyst_voices_services.dart | 15 +- .../lib/src/config/config_service.dart | 22 + .../lib/src/keychain/keychain.dart | 17 - .../src/keychain/keychain_transformers.dart | 43 - .../lib/src/keychain/vault_keychain.dart | 130 -- .../lib/src/proposal/proposal_service.dart | 32 + .../registration/registration_service.dart | 48 +- .../registration_transaction_builder.dart | 62 +- .../lib/src/storage/dummy_auth_storage.dart | 47 - .../lib/src/user/user_service.dart | 228 +-- .../lib/src/user/user_storage.dart | 25 - .../catalyst_voices_services/pubspec.yaml | 15 +- .../test/src/user/user_service_test.dart | 123 +- .../lib/src/cache/cache.dart | 16 + .../lib/src/cache/local_tll_cache.dart | 92 ++ .../lib/src/cache/ttl_cache.dart | 19 + .../lib/src/catalyst_voices_shared.dart | 24 + .../lib/src/crypto/crypto_service.dart | 0 .../lib/src/crypto/key_derivation.dart | 0 .../lib/src/crypto/local_crypto_service.dart} | 14 +- .../src/dependency/dependency_provider.dart | 26 +- .../lib/src/document/document_manager.dart | 83 ++ .../src/document/document_manager_impl.dart | 129 ++ .../extension/document_list_sort_ext.dart | 13 + .../extension/document_map_to_list_ext.dart | 28 + .../lib/src/document/identifiable.dart | 3 + .../lib/src/keychain/keychain.dart | 14 + .../lib/src/keychain/keychain_provider.dart | 2 +- .../src/keychain/keychain_transformers.dart | 84 ++ .../lib/src/keychain/vault_keychain.dart | 67 + .../src/keychain/vault_keychain_provider.dart | 43 +- .../lib/src/range/range.dart | 18 + .../lib/src/storage/local_storage.dart | 62 + .../lib/src/storage/memory_storage.dart | 37 + .../lib/src/storage/secure_storage.dart | 18 +- .../lib/src/storage/storage.dart | 0 .../lib/src/storage/storage_string_mixin.dart | 5 +- .../storage/vault/secure_storage_vault.dart | 180 ++- .../vault/secure_storage_vault_cache.dart | 49 + .../lib/src/storage/vault/vault.dart | 6 +- .../lib/src/utils/active_aware.dart | 11 + .../lib/src/utils}/lockable.dart | 4 + .../catalyst_voices_shared/pubspec.yaml | 15 + .../test/src/cache/local_tll_cache_test.dart | 79 + .../test/src/crypto/key_derivation_test.dart | 2 +- .../crypto/local_crypto_service_test.dart} | 4 +- .../src/document/document_manager_test.dart | 172 +++ .../keychain/keychain_transformers_test.dart | 18 +- .../vault_keychain_provider_test.dart | 23 +- .../src/keychain/vault_keychain_test.dart | 75 +- .../test/src/storage/secure_storage_test.dart | 2 +- .../storage/storage_string_mixin_test.dart | 4 +- .../vault/secure_storage_vault_test.dart | 19 +- .../src/authentication/access_control.dart | 93 ++ .../src/authentication/authentication.dart | 1 + .../campaign/campaign_category_section.dart | 41 + .../lib/src/campaign/campaign_info.dart | 67 + .../lib/src/campaign/campaign_list_item.dart | 42 + .../lib/src/campaign/campaign_stage.dart | 45 + .../lib/src/catalyst_voices_view_models.dart | 12 +- .../lib/src/menu/menu_item.dart | 31 + .../lib/src/menu/popup_menu_item.dart | 32 + .../src/navigation/sections_navigation.dart | 19 +- .../src/proposal/guidance/guidance_type.dart | 11 - .../lib/src/proposal/proposal_view_model.dart | 151 ++ .../lib/src/session/session_status.dart | 20 + .../lib/src/treasury/campaign_setup.dart | 30 - .../lib/src/treasury/treasury_sections.dart | 66 +- .../workspace/capability_and_feasibility.dart | 51 - .../lib/src/workspace/proposal_impact.dart | 41 - .../lib/src/workspace/proposal_setup.dart | 27 - .../lib/src/workspace/proposal_solution.dart | 73 - .../lib/src/workspace/proposal_summary.dart | 58 - .../workspace_proposal_list_item.dart | 14 + .../lib/src/workspace/workspace_sections.dart | 42 +- .../lib/src/workspace/workspace_tab_type.dart | 17 + .../catalyst_voices_view_models/pubspec.yaml | 3 + .../test/campaign/campaign_stage_test.dart | 69 + .../wallet-automation/pages/homePage.ts | 3 +- .../wallet-automation/pages/modal.ts | 45 +- .../wallet-automation/pages/walletListPage.ts | 22 +- .../wallet-automation/setup.ts | 3 +- .../wallet-automation/tests/wallets.spec.ts | 67 +- .../utils/extensionDownloader.ts | 35 +- .../wallet-automation/utils/extensions.ts | 12 + .../wallet-automation/utils/walletConfigs.ts | 171 ++- .../utils/wallets/nufiUtils.ts | 43 + .../utils/wallets/walletUtils.ts | 47 +- .../utils/wallets/yoroiUtils.ts | 49 + .../lib/src/rbac/auth_token.dart | 19 +- .../lib/src/utils/cbor.dart | 3 + .../lib/src/utils/uuid.dart | 14 + .../pubspec.yaml | 2 +- .../test/rbac/auth_token_test.dart | 13 +- .../packages/libs/catalyst_cose/README.md | 89 +- .../libs/catalyst_cose/example/main.dart | 89 +- .../libs/catalyst_cose/lib/catalyst_cose.dart | 7 +- .../catalyst_cose/lib/src/catalyst_cose.dart | 149 -- .../catalyst_cose/lib/src/cose_constants.dart | 98 ++ .../libs/catalyst_cose/lib/src/cose_sign.dart | 261 ++++ .../catalyst_cose/lib/src/cose_sign1.dart | 162 ++ .../lib/src/types/cose_headers.dart | 229 +++ .../lib/src/types/string_or_int.dart | 61 + .../catalyst_cose/lib/src/types/uuid.dart | 74 + .../lib/src/utils/cbor_utils.dart | 84 ++ .../packages/libs/catalyst_cose/pubspec.yaml | 3 + .../test/catalyst_cose_test.dart | 157 -- .../catalyst_cose/test/cose_sign1_test.dart | 101 ++ .../catalyst_cose/test/cose_sign_test.dart | 117 ++ .../lib/examples/voices_menu_example.dart | 8 +- .../lib/examples/voices_modals_example.dart | 3 - .../voices_proposal_card_example.dart | 13 +- .../examples/voices_text_field_example.dart | 36 +- .../utilities/uikit_example/pubspec.yaml | 2 + cspell.json | 1 + .../signed_document_metadata/.pages | 3 - .../signed_document_metadata/metadata.md | 296 ---- .../0009-uuid7-vs-ulid.md | 82 + 390 files changed, 20116 insertions(+), 3897 deletions(-) create mode 100644 catalyst-gateway/bin/src/db/event/signed_docs/mod.rs create mode 100644 catalyst-gateway/bin/src/db/event/signed_docs/sql/insert_signed_documents.sql create mode 100644 catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs create mode 100644 catalyst-gateway/event-db/poetry.lock create mode 100644 catalyst-gateway/event-db/pyproject.toml create mode 100644 catalyst-gateway/event-db/queries/insert_signed_documents.sql create mode 100644 catalyst-gateway/event-db/queries/select_signed_documents.sql.jinja create mode 100644 catalyst-gateway/event-db/queries/select_signed_documents_2.sql.jinja create mode 100644 catalyst-gateway/event-db/tests/docker-compose.yml create mode 100644 catalyst-gateway/event-db/tests/test_signed_docs_queries.py create mode 100644 catalyst_voices/apps/voices/integration_test/all_test.dart create mode 100644 catalyst_voices/apps/voices/integration_test/onboarding_test.dart create mode 100644 catalyst_voices/apps/voices/integration_test/pageobject/app_bar_page.dart create mode 100644 catalyst_voices/apps/voices/integration_test/pageobject/common_page.dart create mode 100644 catalyst_voices/apps/voices/integration_test/pageobject/onboarding_page.dart create mode 100644 catalyst_voices/apps/voices/integration_test/pageobject/overall_spaces_page.dart create mode 100644 catalyst_voices/apps/voices/integration_test/pageobject/spaces_drawer_page.dart create mode 100644 catalyst_voices/apps/voices/integration_test/types/registration_state.dart create mode 100644 catalyst_voices/apps/voices/integration_test/utils/selector_utils.dart create mode 100644 catalyst_voices/apps/voices/integration_test/utils/translations_utils.dart create mode 100644 catalyst_voices/apps/voices/lib/app/view/app_active_state_listener.dart create mode 100644 catalyst_voices/apps/voices/lib/common/codecs/markdown_codec.dart create mode 100644 catalyst_voices/apps/voices/lib/common/ext/map_ext.dart create mode 100644 catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_dialog.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_events.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_views.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/details/campaign_details_dialog.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_categories_tile.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_details_tile.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_header.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_management.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_management_dialog.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_sections_list_view.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/login/login.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/login/login_button.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/login/login_email_text_filed.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/login/login_form.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/login/login_page.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/login/login_password_text_field.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder.dart rename catalyst_voices/apps/voices/lib/pages/{workspace/workspace_body.dart => proposal_builder/proposal_builder_body.dart} (79%) rename catalyst_voices/apps/voices/lib/pages/{workspace/workspace_guidance_view.dart => proposal_builder/proposal_builder_guidance_view.dart} (82%) rename catalyst_voices/apps/voices/lib/pages/{workspace/workspace_navigation_panel.dart => proposal_builder/proposal_builder_navigation_panel.dart} (84%) create mode 100644 catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart rename catalyst_voices/apps/voices/lib/pages/{workspace/workspace_rich_text_step.dart => proposal_builder/proposal_builder_rich_text_step.dart} (69%) create mode 100644 catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_setup_panel.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_categories_step.dart rename catalyst_voices/apps/voices/lib/pages/treasury/{treasury_dummy_topic_step.dart => steps/treasury_campaign_details_step.dart} (83%) create mode 100644 catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_stages_edit_step.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_stages_view_step.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_widgets.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_proposal_template_step.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/answer.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/bonus_mark_up.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/delivery_and_accountability.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/feasibility_checks.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/problem_statement.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/public_description.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/solution_statement.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/title.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/rich_text/value_for_money.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/workspace_empty_state.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/workspace_error.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/workspace_header.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/workspace_loading.dart create mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/workspace_proposals.dart delete mode 100644 catalyst_voices/apps/voices/lib/pages/workspace/workspace_setup_panel.dart create mode 100644 catalyst_voices/apps/voices/lib/routes/guards/user_access_guard.dart delete mode 100644 catalyst_voices/apps/voices/lib/routes/routing/login_route.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/cards/proposal_card.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/containers/grey_out_container.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/document_builder/document_property.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/empty_state/empty_state.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/images/voices_image_scheme.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/menu/voices_modal_menu.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/modals/details/voices_align_title_header.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/modals/details/voices_details_dialog.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/modals/details/voices_details_dialog_header.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/pickers/voices_calendar_picker.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart create mode 100644 catalyst_voices/apps/voices/lib/widgets/tiles/voices_expansion_tile.dart create mode 100644 catalyst_voices/apps/voices/test/common/codecs/markdown_codec_test.dart create mode 100644 catalyst_voices/apps/voices/test/widgets/cards/campaign_stage_card_test.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_assets/assets/images/no_proposal_foreground.svg create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools_cubit.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools_state.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_bloc.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_event.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_state.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_cubit.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_state.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_bloc.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_event.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_state.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info_cubit.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info_state.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_bloc.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_event.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_state.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_event.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_cubit.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_state.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_event.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_state.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/test/admin_tools/admin_tools_cubit_test.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/test/campaign/info/campaign_info_cubit_test.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/build.yaml create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app_config.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/auth/authentication_status.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_publish.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_section.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/crypto/keychain_metadata.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_json.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_builder.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_definitions.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_schema.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/markdown_data.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/funded_proposal.dart rename catalyst_voices/packages/internal/{catalyst_voices_view_models/lib/src/proposal/guidance => catalyst_voices_models/lib/src/proposal}/guidance.dart (71%) delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/pending_proposal.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_section.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_status.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/lib/src/session_data.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_models/test/crypto/keychain_metadata_test.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/build.yaml create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/generated/api/client_index.dart rename catalyst_voices/packages/internal/{catalyst_voices_services/lib/generated/catalyst_gateway => catalyst_voices_repositories/lib/generated/api}/client_mapping.dart (100%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/api_models/overriden_models.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/authentication_repository.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/config/config_repository.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/credentials_storage_repository.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_config_dto.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_builder_dto.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_definitions_dto.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_schema_dto.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/user_dto.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_repository.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_storage.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_repositories}/openapi-filters.json (100%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/openapi/vit.yaml create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/test/helpers/read_json.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_builder_test.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_definitions_test.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_schema_test.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/build.yaml delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway/client_index.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/src/config/config_service.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_transformers.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/dummy_auth_storage.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_storage.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/cache.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/local_tll_cache.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/ttl_cache.dart rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/crypto/crypto_service.dart (100%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/crypto/key_derivation.dart (100%) rename catalyst_voices/packages/internal/{catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart => catalyst_voices_shared/lib/src/crypto/local_crypto_service.dart} (92%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/extension/document_list_sort_ext.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/extension/document_map_to_list_ext.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/identifiable.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain.dart rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/keychain/keychain_provider.dart (72%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_transformers.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain.dart rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/keychain/vault_keychain_provider.dart (67%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/range/range.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/local_storage.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/memory_storage.dart rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/storage/secure_storage.dart (75%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/storage/storage.dart (100%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/storage/storage_string_mixin.dart (89%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/storage/vault/secure_storage_vault.dart (52%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault_cache.dart rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/lib/src/storage/vault/vault.dart (64%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/active_aware.dart rename catalyst_voices/packages/internal/{catalyst_voices_services/lib/src => catalyst_voices_shared/lib/src/utils}/lockable.dart (83%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/test/src/cache/local_tll_cache_test.dart rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/test/src/crypto/key_derivation_test.dart (97%) rename catalyst_voices/packages/internal/{catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart => catalyst_voices_shared/test/src/crypto/local_crypto_service_test.dart} (96%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/test/src/keychain/keychain_transformers_test.dart (66%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/test/src/keychain/vault_keychain_provider_test.dart (79%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/test/src/keychain/vault_keychain_test.dart (60%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/test/src/storage/secure_storage_test.dart (96%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/test/src/storage/storage_string_mixin_test.dart (95%) rename catalyst_voices/packages/internal/{catalyst_voices_services => catalyst_voices_shared}/test/src/storage/vault/secure_storage_vault_test.dart (87%) create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/access_control.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_category_section.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_info.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_list_item.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_stage.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/menu/menu_item.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/menu/popup_menu_item.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance_type.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/proposal_view_model.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_status.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/treasury/campaign_setup.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/capability_and_feasibility.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_impact.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_setup.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_solution.dart delete mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_summary.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_proposal_list_item.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_tab_type.dart create mode 100644 catalyst_voices/packages/internal/catalyst_voices_view_models/test/campaign/campaign_stage_test.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/nufiUtils.ts create mode 100644 catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/yoroiUtils.ts delete mode 100644 catalyst_voices/packages/libs/catalyst_cose/lib/src/catalyst_cose.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/lib/src/types/string_or_int.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/lib/src/utils/cbor_utils.dart delete mode 100644 catalyst_voices/packages/libs/catalyst_cose/test/catalyst_cose_test.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart create mode 100644 catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart delete mode 100644 docs/src/architecture/08_concepts/signed_document_metadata/.pages delete mode 100644 docs/src/architecture/08_concepts/signed_document_metadata/metadata.md create mode 100644 docs/src/architecture/09_architecture_decisions/0009-uuid7-vs-ulid.md diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index f1b9a1c93f7..6fb00d63c76 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -22,6 +22,7 @@ auditability Autolayout autorecalculates autoresizing +autovalidate backendpython backgrounding bech @@ -59,6 +60,7 @@ collabs commitlog concatcp coproposers +COSE coti coverallsapp CQLSH @@ -77,6 +79,7 @@ delegators devnet DIND dockerhub +domcontentloaded Dominik dotenv dotenvy @@ -168,6 +171,7 @@ loguru lovelace lovelaces LTRB +Lynxx mdlint metadatum metadatums @@ -187,11 +191,13 @@ Multiplatform myproject Nami nanos +nathanbogale NDEBUG netifas netkey nextest Nodetool +NuFi oapi OCSP Oleksandr @@ -200,6 +206,7 @@ oneshot openapi opentelemetry overprovisioned +pageobject Pbkdf2 pbxproj Pdart @@ -219,6 +226,7 @@ projectcatalyst Prokhorenko proptest psql +psycopg Ptarget pubkey PUBLICKEY @@ -283,7 +291,9 @@ testplan testunit thiserror thollander +Timelapse timelike +tkach Toastify todos toggleable @@ -292,6 +302,7 @@ tomjs Traceback traefik trailings +tstr TXNZD txos Typer @@ -299,6 +310,7 @@ unawaited unchunk Unlogged unmanaged +Unmarks Unstaked upskilling UTXO @@ -321,6 +333,7 @@ Wireframes Wmissing Wnullable Woverlength +Writedown xcassets xcconfig xcfilelist @@ -333,4 +346,4 @@ xprv xpub xpublic xvfb -yoroi +yoroi \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6be1963f41c..73619052f97 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,9 +1,13 @@ -/athena/ @stevenj @Mr-Leshiy @FelipeRosa @cong-or @saibatizoku +/athena/ @stevenj @Mr-Leshiy @cong-or @saibatizoku @bkioshn @stanislav-tkach -/catalyst_voices/ @minikin +/catalyst_voices/ @dtscalac @damian-molinski @LynxLynxx -/catalyst_voices/packages/libs/ @minikin @stevenj +/catalyst_voices_packages/ @dtscalac @damian-molinski @LynxLynxx -/catalyst-gateway/ @stevenj @Mr-Leshiy @FelipeRosa @cong-or @saibatizoku @minikin +/catalyst-gateway/ @stevenj @Mr-Leshiy @cong-or @saibatizoku @bkioshn @stanislav-tkach -.md @stevenj @minikin +/catalyst-gateway-crates/ @stevenj @Mr-Leshiy @cong-or @saibatizoku @bkioshn @stanislav-tkach + +/docs/ @stevenj @neil-iohk @nathanbogale @minikin + +.md @stevenj @minikin \ No newline at end of file diff --git a/catalyst-gateway/Earthfile b/catalyst-gateway/Earthfile index 1cde02848f7..bde5adb213f 100644 --- a/catalyst-gateway/Earthfile +++ b/catalyst-gateway/Earthfile @@ -124,3 +124,19 @@ check-builder-src-cache: RUN diff ../src_fingerprint.txt ../src_fingerprint_uncached.txt \ || (echo "ERROR: Source fingerprints do not match. Caching Error Detected!!" && exit 1) \ && echo "Source fingerprints match. Caching OK." + +test: + FROM +builder-src + + COPY docker-compose.yml . + + ENV EVENT_DB_URL "postgres://catalyst-event-dev:CHANGE_ME@localhost/CatalystEventDev" + + WITH DOCKER \ + --compose "./docker-compose.yml" \ + --load ./event-db+build \ + --pull alpine:3.20.3 \ + --service event-db-is-running + RUN --mount=$EARTHLY_RUST_CARGO_HOME_CACHE --mount=$EARTHLY_RUST_TARGET_CACHE \ + cargo nextest run --release --run-ignored=only signed_docs + END \ No newline at end of file diff --git a/catalyst-gateway/bin/Cargo.toml b/catalyst-gateway/bin/Cargo.toml index fbba8109c1b..75c578f6d92 100644 --- a/catalyst-gateway/bin/Cargo.toml +++ b/catalyst-gateway/bin/Cargo.toml @@ -15,7 +15,7 @@ repository.workspace = true workspace = true [dependencies] -cardano-chain-follower = { version = "0.0.5", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.9" } +cardano-chain-follower = { version = "0.0.6", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.10" } c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.3" } rbac-registration = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.8" } @@ -43,6 +43,7 @@ tokio-postgres = { version = "0.7.12", features = [ "with-chrono-0_4", "with-serde_json-1", "with-time-0_3", + "with-uuid-1" ] } tokio = { version = "1.41.0", features = ["rt", "macros", "rt-multi-thread"] } dotenvy = "0.15.7" @@ -77,7 +78,7 @@ poem-openapi = { version = "5.1.2", features = [ "url", "chrono", ] } -uuid = { version = "1.11.0", features = ["v4", "serde"] } +uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] } ulid = { version = "1.1.3", features = ["serde", "uuid"] } blake2b_simd = "1.0.2" url = "2.5.3" diff --git a/catalyst-gateway/bin/src/db/event/mod.rs b/catalyst-gateway/bin/src/db/event/mod.rs index d0914f3a888..1bc4bd1d476 100644 --- a/catalyst-gateway/bin/src/db/event/mod.rs +++ b/catalyst-gateway/bin/src/db/event/mod.rs @@ -18,10 +18,11 @@ pub(crate) mod config; pub(crate) mod error; pub(crate) mod legacy; pub(crate) mod schema_check; +pub(crate) mod signed_docs; /// Database version this crate matches. /// Must equal the last Migrations Version Number from `event-db/migrations`. -pub(crate) const DATABASE_SCHEMA_VERSION: i32 = 9; +pub(crate) const DATABASE_SCHEMA_VERSION: i32 = 2; /// Postgres Connection Manager DB Pool type SqlDbPool = Arc>>; @@ -212,7 +213,7 @@ impl EventDB { /// /// The env var "`DATABASE_URL`" can be set directly as an anv var, or in a /// `.env` file. -pub(crate) fn establish_connection() { +pub fn establish_connection() { let (url, user, pass) = Settings::event_db_settings(); // This was pre-validated and can't fail, but provide default in the impossible case it diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/mod.rs b/catalyst-gateway/bin/src/db/event/signed_docs/mod.rs new file mode 100644 index 00000000000..19ae3a854b3 --- /dev/null +++ b/catalyst-gateway/bin/src/db/event/signed_docs/mod.rs @@ -0,0 +1,32 @@ +//! Signed docs queries + +#[cfg(test)] +mod tests; + +use super::EventDB; + +/// Insert sql query +const INSERT_SIGNED_DOCS: &str = include_str!("./sql/insert_signed_documents.sql"); + +/// Make an insert query into the `event-db` by adding data into the `signed_docs` table +/// +/// * IF the record primary key (id,ver) does not exist, then add the new record. Return +/// success. +/// * IF the record does exist, but all values are the same as stored, return Success. +/// * Otherwise return an error. (Can not over-write an existing record with new data). +/// +/// # Arguments: +/// - `id` is a UUID v7 +/// - `ver` is a UUID v7 +/// - `doc_type` is a UUID v4 +#[allow(dead_code)] +pub(crate) async fn insert_signed_docs( + id: &uuid::Uuid, ver: &uuid::Uuid, doc_type: &uuid::Uuid, author: &String, + metadata: &Option, payload: &Option, raw: &Vec, +) -> anyhow::Result<()> { + EventDB::modify(INSERT_SIGNED_DOCS, &[ + id, ver, doc_type, author, metadata, payload, raw, + ]) + .await?; + Ok(()) +} diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/sql/insert_signed_documents.sql b/catalyst-gateway/bin/src/db/event/signed_docs/sql/insert_signed_documents.sql new file mode 100644 index 00000000000..61183ea693a --- /dev/null +++ b/catalyst-gateway/bin/src/db/event/signed_docs/sql/insert_signed_documents.sql @@ -0,0 +1,12 @@ +INSERT INTO signed_docs +( + id, + ver, + type, + author, + metadata, + payload, + raw +) +VALUES +($1, $2, $3, $4, $5, $6, $7) diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs b/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs new file mode 100644 index 00000000000..a65c9206adc --- /dev/null +++ b/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs @@ -0,0 +1,50 @@ +//! Integration tests of the `signed docs` queries + +use super::*; +use crate::db::event::establish_connection; + +#[ignore = "An integration test which requires a running EventDB instance, disabled from `testunit` CI run"] +#[tokio::test] +async fn some_test() { + establish_connection(); + + let docs = [ + ( + uuid::Uuid::now_v7(), + uuid::Uuid::now_v7(), + uuid::Uuid::new_v4(), + "Alex".to_string(), + &Some(serde_json::Value::Null), + &Some(serde_json::Value::Null), + vec![1, 2, 3, 4], + ), + ( + uuid::Uuid::now_v7(), + uuid::Uuid::now_v7(), + uuid::Uuid::new_v4(), + "Steven".to_string(), + &Some(serde_json::Value::Null), + &Some(serde_json::Value::Null), + vec![5, 6, 7, 8], + ), + ( + uuid::Uuid::now_v7(), + uuid::Uuid::now_v7(), + uuid::Uuid::new_v4(), + "Sasha".to_string(), + &None, + &None, + vec![9, 10, 11, 12], + ), + ]; + + for (id, ver, doc_type, author, metadata, payload, raw) in &docs { + insert_signed_docs(id, ver, doc_type, author, metadata, payload, raw) + .await + .unwrap(); + // // try to insert the same data again + // insert_signed_docs(id, ver, doc_type, author, metadata, payload, raw) + // .await + // .unwrap(); + } +} diff --git a/catalyst-gateway/bin/src/settings/mod.rs b/catalyst-gateway/bin/src/settings/mod.rs index 28726514680..6be03945050 100644 --- a/catalyst-gateway/bin/src/settings/mod.rs +++ b/catalyst-gateway/bin/src/settings/mod.rs @@ -73,10 +73,6 @@ fn calculate_service_uuid() -> String { #[derive(Args, Clone)] #[clap(version = BUILD_INFO)] pub(crate) struct ServiceSettings { - /// Url to the postgres event db - #[clap(long, env)] - pub(crate) event_db_url: Option, - /// Logging level #[clap(long, default_value = LOG_LEVEL_DEFAULT)] pub(crate) log_level: LogLevel, diff --git a/catalyst-gateway/blueprint.cue b/catalyst-gateway/blueprint.cue index c81c19d56c6..9e36433f3f4 100644 --- a/catalyst-gateway/blueprint.cue +++ b/catalyst-gateway/blueprint.cue @@ -12,4 +12,9 @@ project: { } } } + ci: { + targets: { + test: privileged: true + } + } } diff --git a/catalyst-gateway/docker-compose.yml b/catalyst-gateway/docker-compose.yml index 4b2d833735b..00b2a348764 100644 --- a/catalyst-gateway/docker-compose.yml +++ b/catalyst-gateway/docker-compose.yml @@ -1,10 +1,7 @@ -version: "3" - services: event-db: image: event-db:latest environment: - # Required environment variables for migrations - DB_HOST=localhost - DB_PORT=5432 - DB_NAME=CatalystEventDev @@ -16,7 +13,6 @@ services: - INIT_AND_DROP_DB=true - WITH_MIGRATIONS=true - - WITH_SEED_DATA=true ports: - 5432:5432 healthcheck: @@ -25,6 +21,14 @@ services: timeout: 5s retries: 10 +# it is a helper service to wait until the event-db will be ready +# mainly its a trick for Earthly how to wait until service will be fully functional + event-db-is-running: + image: alpine:3.20.3 + depends_on: + event-db: + condition: service_healthy + cat-gateway: image: cat-gateway:latest environment: diff --git a/catalyst-gateway/event-db/Earthfile b/catalyst-gateway/event-db/Earthfile index dac2e2647ca..4b004d1ea62 100644 --- a/catalyst-gateway/event-db/Earthfile +++ b/catalyst-gateway/event-db/Earthfile @@ -4,6 +4,7 @@ VERSION 0.8 IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.24 AS postgresql-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/python:v3.2.24 AS python-ci # cspell: words @@ -41,24 +42,19 @@ local: SAVE IMAGE --push --insecure $local_registry/event-db:latest END - -# test the event db database schema -# CI target : true -#test: -# FROM github.com/input-output-hk/catalyst-ci/earthly/postgresql:v2.9.2+postgres-base - -# COPY github.com/input-output-hk/catalyst-ci/earthly/utils:v2.9.2+shell-assert/assert.sh . - -# COPY ./docker-compose.yml . -# WITH DOCKER \ -# --compose docker-compose.yml \ -# --load event-db:latest=(+build --with_historic_data=false) \ -# --service event-db \ -# --allow-privileged -# RUN sleep 65;\ -# res=$(psql postgresql://catalyst-event-dev:CHANGE_ME@0.0.0.0:5432/CatalystEventDev -c "SELECT COUNT(*) FROM event");\ - -# source assert.sh;\ -# expected=$(printf " count \n-------\n 5\n(1 row)");\ -# assert_eq "$expected" "$res" -# END +# Run the queries_tests.py +test: + FROM python-ci+python-base + + DO python-ci+BUILDER + COPY --dir tests . + COPY --dir queries . + + WITH DOCKER \ + --compose "./tests/docker-compose.yml" \ + --load event-db:latest=+build \ + --pull alpine:3.20.3 \ + --service event-db-is-running \ + --allow-privileged + RUN poetry run pytest -s -m ci + END diff --git a/catalyst-gateway/event-db/blueprint.cue b/catalyst-gateway/event-db/blueprint.cue index 1dad7a98b08..0d9ef5b2bff 100644 --- a/catalyst-gateway/event-db/blueprint.cue +++ b/catalyst-gateway/event-db/blueprint.cue @@ -1,2 +1,9 @@ version: "1.0.0" -project: name: "gateway-event-db" +project: { + name: "gateway-event-db" + ci: { + targets: { + test: privileged: true + } + } +} diff --git a/catalyst-gateway/event-db/migrations/V2__signed_documents.sql b/catalyst-gateway/event-db/migrations/V2__signed_documents.sql index 523680ef9f2..28ff361d4a3 100644 --- a/catalyst-gateway/event-db/migrations/V2__signed_documents.sql +++ b/catalyst-gateway/event-db/migrations/V2__signed_documents.sql @@ -12,11 +12,11 @@ -- Signed Documents Storage Repository defintion. CREATE TABLE IF NOT EXISTS signed_docs ( - id UUID NOT NULL, -- Actually a ULID - ver UUID NOT NULL, -- Actually a ULID - type UUID NOT NULL, -- Yes its a UUID this time + id UUID NOT NULL, -- UUID v7 + ver UUID NOT NULL, -- UUID v7 + type UUID NOT NULL, -- UUID v4 author TEXT NOT NULL, - metadata JSONB NOT NULL, + metadata JSONB NULL, payload JSONB NULL, raw BYTEA NOT NULL, diff --git a/catalyst-gateway/event-db/poetry.lock b/catalyst-gateway/event-db/poetry.lock new file mode 100644 index 00000000000..9194944001f --- /dev/null +++ b/catalyst-gateway/event-db/poetry.lock @@ -0,0 +1,311 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "psycopg" +version = "3.2.3" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg-3.2.3-py3-none-any.whl", hash = "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907"}, + {file = "psycopg-3.2.3.tar.gz", hash = "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.3)"] +c = ["psycopg-c (==3.2.3)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.11)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.2.3" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea"}, + {file = "psycopg_binary-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585"}, + {file = "psycopg_binary-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9"}, + {file = "psycopg_binary-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd"}, + {file = "psycopg_binary-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b"}, + {file = "psycopg_binary-3.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f"}, + {file = "psycopg_binary-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749"}, +] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "f1cfae8e203124329bc62902795c307371fa78930a3aabc9c8cd41a889b5298e" diff --git a/catalyst-gateway/event-db/pyproject.toml b/catalyst-gateway/event-db/pyproject.toml new file mode 100644 index 00000000000..d7447218181 --- /dev/null +++ b/catalyst-gateway/event-db/pyproject.toml @@ -0,0 +1,23 @@ +# cspell: words bitcoinlib + +[tool.poetry] +name = "tests" +version = "0.1.0" +description = "" +authors = [] +readme = "Readme.md" +license = "MIT or Apache-2.0" + +[tool.poetry.dependencies] +python = "^3.11" +loguru = "^0.7.2" +pytest = "^8.0.0" +psycopg = "3.2.3" +psycopg-binary = "^3.2.3" +jinja2 = "^3.1.4" + +[tool.pytest.ini_options] +markers = [ + "ci: marks tests to be run in ci", + "nightly: marks tests to be run nightly", +] \ No newline at end of file diff --git a/catalyst-gateway/event-db/queries/insert_signed_documents.sql b/catalyst-gateway/event-db/queries/insert_signed_documents.sql new file mode 100644 index 00000000000..b5d79222259 --- /dev/null +++ b/catalyst-gateway/event-db/queries/insert_signed_documents.sql @@ -0,0 +1,22 @@ +INSERT INTO signed_docs +( + id, + ver, + type, + author, + metadata, + payload, + raw +) +VALUES +($1, $2, $3, $4, $5, $6, $7) +ON CONFLICT (id, ver) DO UPDATE +SET +type = signed_docs.type + +WHERE +signed_docs.type = excluded.type +AND signed_docs.author = excluded.author +AND signed_docs.metadata = excluded.metadata +AND signed_docs.payload = excluded.payload +AND signed_docs.raw = excluded.raw diff --git a/catalyst-gateway/event-db/queries/select_signed_documents.sql.jinja b/catalyst-gateway/event-db/queries/select_signed_documents.sql.jinja new file mode 100644 index 00000000000..fb4effa7fb3 --- /dev/null +++ b/catalyst-gateway/event-db/queries/select_signed_documents.sql.jinja @@ -0,0 +1,12 @@ +SELECT + signed_docs.type, + signed_docs.author, + signed_docs.metadata, + signed_docs.payload, + signed_docs.raw +FROM signed_docs +WHERE + signed_docs.id = '{{ id }}' + {% if ver %} AND signed_docs.ver = '{{ ver }}' {% endif %} +ORDER BY signed_docs.ver DESC +LIMIT 1 diff --git a/catalyst-gateway/event-db/queries/select_signed_documents_2.sql.jinja b/catalyst-gateway/event-db/queries/select_signed_documents_2.sql.jinja new file mode 100644 index 00000000000..fda56391dbf --- /dev/null +++ b/catalyst-gateway/event-db/queries/select_signed_documents_2.sql.jinja @@ -0,0 +1,12 @@ +SELECT + signed_docs.id, + signed_docs.ver, + signed_docs.type, + signed_docs.author, + signed_docs.metadata +FROM signed_docs +WHERE + {{ conditions }} +ORDER BY signed_docs.type DESC, signed_docs.id DESC, signed_docs.ver DESC +{% if limit %} LIMIT {{ limit }} {% endif %} +{% if offset %} OFFSET {{ offset }} {% endif %} diff --git a/catalyst-gateway/event-db/tests/docker-compose.yml b/catalyst-gateway/event-db/tests/docker-compose.yml new file mode 100644 index 00000000000..4c9801de859 --- /dev/null +++ b/catalyst-gateway/event-db/tests/docker-compose.yml @@ -0,0 +1,30 @@ +services: + event-db: + image: event-db:latest + environment: + - DB_HOST=localhost + - DB_PORT=5432 + - DB_NAME=CatalystEventDev + - DB_DESCRIPTION="Catalyst Event DB" + - DB_SUPERUSER=postgres + - DB_SUPERUSER_PASSWORD=postgres + - DB_USER=catalyst-event-dev + - DB_USER_PASSWORD=CHANGE_ME + + - INIT_AND_DROP_DB=true + - WITH_MIGRATIONS=true + ports: + - 5432:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${DB_SUPERUSER} -d $${DB_SUPERUSER_PASSWORD}"] + interval: 10s + timeout: 5s + retries: 10 + +# it is a helper service to wait until the event-db will be ready +# mainly its a trick for Earthly how to wait until service will be fully functional + event-db-is-running: + image: alpine:3.20.3 + depends_on: + event-db: + condition: service_healthy diff --git a/catalyst-gateway/event-db/tests/test_signed_docs_queries.py b/catalyst-gateway/event-db/tests/test_signed_docs_queries.py new file mode 100644 index 00000000000..3eaad1af525 --- /dev/null +++ b/catalyst-gateway/event-db/tests/test_signed_docs_queries.py @@ -0,0 +1,147 @@ +import psycopg +import pytest +import jinja2 + + +jinja_env = jinja2.Environment( + loader=jinja2.FileSystemLoader("./queries/"), +) +EVENT_DB_URL = "postgres://catalyst-event-dev:CHANGE_ME@localhost/CatalystEventDev" + + +class SignedData: + def __init__( + self, + id: str, + ver: str, + doc_type: str, + author: str, + metadata: str, + payload: str, + raw: bytes, + ): + self.id = id + self.ver = ver + self.doc_type = doc_type + self.author = author + self.metadata = metadata + self.payload = payload + self.raw = raw + + +@pytest.mark.ci +def test_signed_docs_queries(): + with psycopg.connect(EVENT_DB_URL) as conn: + docs = [ + SignedData( + id="3764e30b-9bb5-4a34-906e-f4de1845e8bf", + ver="299255cb-9f53-46ec-8e24-4360b9d374bd", + doc_type="c17f59b2-1304-4a75-923e-00795add70af", + author="Alex", + metadata="{}", + payload="{}", + raw=b"bytes1", + ), + SignedData( + id="4ee05138-e85b-48b4-91c2-2a45a74a0a82", + ver="3cd78d6c-9388-47ab-9a65-f3a84c76190f", + doc_type="fec1b996-ad89-4fab-a787-6568f42b00b1", + author="Steven", + metadata="{}", + payload="{}", + raw=b"bytes2", + ), + ] + + insert_signed_documents_query(conn, docs) + # try insert the same values + insert_signed_documents_query(conn, docs) + select_signed_documents_query(conn, docs) + select_signed_documents_2_query(conn, docs) + + # try insert the same id and ver, but with the different other data + docs[0].author = "Sasha" + docs[1].author = "Sasha" + should_panic( + lambda: insert_signed_documents_query(conn, docs), + "insert_signed_documents_query should fail", + ) + should_panic( + lambda: select_signed_documents_query(conn, docs), + "select_signed_documents_query should fail", + ) + should_panic( + lambda: select_signed_documents_2_query(conn, docs), + "select_signed_documents_2_query should fail", + ) + + +def insert_signed_documents_query(conn, docs: [SignedData]): + sql_stmt = open("./queries/insert_signed_documents.sql", "r").read() + sql_stmt = ( + sql_stmt.replace("$1", "%s") + .replace("$2", "%s") + .replace("$3", "%s") + .replace("$4", "%s") + .replace("$5", "%s") + .replace("$6", "%s") + .replace("$7", "%s") + ) + for doc in docs: + conn.execute( + sql_stmt, + ( + doc.id, + doc.ver, + doc.doc_type, + doc.author, + doc.metadata, + doc.payload, + doc.raw, + ), + ) + + +def select_signed_documents_query(conn, docs: [SignedData]): + template = jinja_env.get_template("select_signed_documents.sql.jinja") + for doc in docs: + sql_stmt = template.render( + { + "id": doc.id, + "ver": doc.ver, + } + ) + cur = conn.execute(sql_stmt) + (doc_type, author, metadata, payload, raw) = cur.fetchone() + assert str(doc_type) == doc.doc_type + assert author == doc.author + assert str(metadata) == doc.metadata + assert str(payload) == doc.payload + assert raw == doc.raw + + +def select_signed_documents_2_query(conn, docs: [SignedData]): + template = jinja_env.get_template("select_signed_documents_2.sql.jinja") + for doc in docs: + sql_stmt = template.render( + { + "conditions": f"signed_docs.id = '{doc.id}' AND signed_docs.ver = '{doc.ver}'", + "limit": 1, + "offset": 0, + } + ) + cur = conn.execute(sql_stmt) + (id, ver, doc_type, author, metadata) = cur.fetchone() + assert str(id) == doc.id + assert str(ver) == doc.ver + assert str(doc_type) == doc.doc_type + assert author == doc.author + assert str(metadata) == doc.metadata + + +def should_panic(func, msg: str): + try: + func() + assert False, msg + except: + pass diff --git a/catalyst_voices/.gitignore b/catalyst_voices/.gitignore index 870c49c28a0..361c63c808d 100644 --- a/catalyst_voices/.gitignore +++ b/catalyst_voices/.gitignore @@ -1,5 +1,6 @@ ### Dart ### # See https://www.dartlang.org/guides/libraries/private-files +devtools_options.yaml # Generated files from code generation tools diff --git a/catalyst_voices/Earthfile b/catalyst_voices/Earthfile index 8cbe449c2ee..15cd041e617 100644 --- a/catalyst_voices/Earthfile +++ b/catalyst_voices/Earthfile @@ -19,13 +19,13 @@ builder: DO flutter-ci+BOOTSTRAP # Creates filtered OpenAPI spec -# Takes json file from openapi-filter from /packages/internal/catalyst_voices_services +# Takes json file from openapi-filter from /packages/internal/catalyst_voices_repositories filter-openapi: FROM node:18 - WORKDIR /packages/internal/catalyst_voices_services + WORKDIR /packages/internal/catalyst_voices_repositories COPY catalyst-gateway+build/doc/cat-gateway-api.json openapi/cat-gateway-api.json - COPY packages/internal/catalyst_voices_services/openapi-filters.json openapi-filters.json + COPY packages/internal/catalyst_voices_repositories/openapi-filters.json openapi-filters.json RUN npm install -g openapi-format RUN openapi-format openapi/cat-gateway-api.json -o openapi/filtered-openapi.json --filterFile openapi-filters.json --verbose @@ -40,19 +40,19 @@ filter-openapi: # It accepts [save_locally] ARG that when true place the artifacts in the # proper folders # It accepts [filter_openapi] ARG that when true filter the openapi spec -# using filters from /packages/internal/catalyst_voices_services/openapi-filters.json +# using filters from /packages/internal/catalyst_voices_repositories/openapi-filters.json code-generator: ARG save_locally=false ARG filter_openapi=true FROM +builder - LET gen_code_path = lib/generated/catalyst_gateway - LET local_gen_code_path = packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway/ + LET gen_code_path = lib/generated/api + LET local_gen_code_path = packages/internal/catalyst_voices_repositories/lib/generated/api/ RUN melos l10n RUN melos build_runner IF [ $save_locally = true ] - RUN find . \( -name "*.g.dart" -o -name "*.freezed.dart" -o -name "*.chopper.dart" -o -name "*.swagger.dart" -o -name "*.openapi.dart" -o -name "*.gen.dart" -o -name "catalyst_voices_localizations*.dart" -o -name "cat_gateway_api.*.swagger.*" \) + RUN find . \( -name "*.g.dart" -o -name "*.freezed.dart" -o -name "*.chopper.dart" -o -name "*.swagger.dart" -o -name "*.openapi.dart" -o -name "*.gen.dart" -o -name "catalyst_voices_localizations*.dart" -o -name "cat_gateway.*.swagger.*" \) FOR generated_file IN $(find . \( -name "*.g.dart" -o -name "*.freezed.dart" -o -name "*.chopper.dart" -o -name "*.swagger.dart" -o -name "*.openapi.dart" -o -name "*.gen.dart" -o -name "catalyst_voices_localizations*.dart" -o -name "cat_gateway_api.*.swagger.*" \)) SAVE ARTIFACT $generated_file AS LOCAL $generated_file @@ -60,12 +60,12 @@ code-generator: ELSE SAVE ARTIFACT . END - WORKDIR packages/internal/catalyst_voices_services + WORKDIR packages/internal/catalyst_voices_repositories IF [ $filter_openapi = true ] - COPY +filter-openapi/filtered-openapi.json openapi/cat-gateway-api.json + COPY +filter-openapi/filtered-openapi.json openapi/cat-gateway.json ELSE - COPY catalyst-gateway+build/doc/cat-gateway-api.json openapi/cat-gateway-api.json + COPY catalyst-gateway+build/doc/cat-gateway-api.json openapi/cat-gateway.json END DO flutter-ci+OPENAPI_CODE_GEN \ diff --git a/catalyst_voices/apps/voices/integration_test/Earthfile b/catalyst_voices/apps/voices/integration_test/Earthfile index 75f0f3de6c8..84470172b61 100644 --- a/catalyst_voices/apps/voices/integration_test/Earthfile +++ b/catalyst_voices/apps/voices/integration_test/Earthfile @@ -21,7 +21,7 @@ integration-test-web: # IF [ $browser = "edge" && $TARGETARCH = "amd64" ]] # LET driver = "msedgedriver" # END - + WORKDIR /frontend/apps/voices RUN ($driver --port=$driver_port > $driver.log &) && \ diff --git a/catalyst_voices/apps/voices/integration_test/all_test.dart b/catalyst_voices/apps/voices/integration_test/all_test.dart new file mode 100644 index 00000000000..550f5872a25 --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/all_test.dart @@ -0,0 +1,17 @@ +import 'package:catalyst_voices/configs/bootstrap.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'app_test.dart' as app_test; +import 'onboarding_test.dart' as onboarding_test; + +void main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await bootstrap(router: buildAppRouter()); + }); + + app_test.main(); + onboarding_test.main(); +} diff --git a/catalyst_voices/apps/voices/integration_test/app_test.dart b/catalyst_voices/apps/voices/integration_test/app_test.dart index 9c9bad35d45..e2d34b294dd 100644 --- a/catalyst_voices/apps/voices/integration_test/app_test.dart +++ b/catalyst_voices/apps/voices/integration_test/app_test.dart @@ -1,20 +1,127 @@ import 'package:catalyst_voices/app/view/app.dart'; import 'package:catalyst_voices/configs/bootstrap.dart'; +import 'package:catalyst_voices/routes/routes.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('End to end tests', () { - testWidgets('run app', (tester) async { - final args = await bootstrap(); - await tester.pumpWidget(App(routerConfig: args.routerConfig)); - // let the application load - await tester.pump(const Duration(seconds: 5)); - // pump and settle every 100ms to simulate almost production-like FPS - await tester.pumpAndSettle(const Duration(milliseconds: 100)); - expect(find.text('Coming'), findsOneWidget); - }); +import 'package:go_router/go_router.dart'; +import 'package:patrol_finders/patrol_finders.dart'; + +import 'pageobject/app_bar_page.dart'; +import 'pageobject/overall_spaces_page.dart'; +import 'pageobject/spaces_drawer_page.dart'; +import 'utils/selector_utils.dart'; + +void main() async { + late final GoRouter router; + + setUpAll(() async { + router = buildAppRouter(); + }); + + setUp(() async { + await registerDependencies(config: const AppConfig()); + router.go(const DiscoveryRoute().location); + }); + + tearDown(() async { + await restartDependencies(); }); + + group( + 'Spaces drawer -', + () { + patrolWidgetTest( + 'visitor - no drawer button', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(OverallSpacesPage.visitorShortcutBtn) + .tap(settleTimeout: const Duration(seconds: 10)); + expect($(AppBarPage.spacesDrawerButton).exists, false); + }, + ); + + patrolWidgetTest( + 'guest - chooser - clicking on icons works correctly', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(OverallSpacesPage.guestShortcutBtn) + .tap(settleTimeout: const Duration(seconds: 10)); + await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); + SpacesDrawerPage.commonElementsLookAsExpected($); + + // iterate thru spaces by clicking on spaces icons directly + for (final space in Space.values) { + await $(SpacesDrawerPage.chooserItem(space)).tap(); + await SpacesDrawerPage.guestLooksAsExpected($, space); + } + SelectorUtils.isDisabled($, $(SpacesDrawerPage.chooserNextBtn)); + }, + ); + + patrolWidgetTest( + 'guest - chooser - next,previous buttons work correctly', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(OverallSpacesPage.guestShortcutBtn) + .tap(settleTimeout: const Duration(seconds: 10)); + await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); + + // iterate thru spaces by clicking next + for (final space in Space.values) { + await SpacesDrawerPage.guestLooksAsExpected($, space); + await $(SpacesDrawerPage.chooserNextBtn).tap(); + SelectorUtils.isEnabled($, $(SpacesDrawerPage.chooserPrevBtn)); + } + SelectorUtils.isDisabled($, $(SpacesDrawerPage.chooserNextBtn)); + + // iterate thru spaces by clicking previous + for (final space in Space.values.reversed) { + await SpacesDrawerPage.guestLooksAsExpected($, space); + await $(SpacesDrawerPage.chooserPrevBtn).tap(); + SelectorUtils.isEnabled($, $(SpacesDrawerPage.chooserNextBtn)); + } + SelectorUtils.isDisabled($, $(SpacesDrawerPage.chooserPrevBtn)); + }, + ); + + patrolWidgetTest( + 'user - chooser - clicking on icons works correctly', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(OverallSpacesPage.userShortcutBtn) + .tap(settleTimeout: const Duration(seconds: 10)); + await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); + SpacesDrawerPage.commonElementsLookAsExpected($); + for (final space in Space.values) { + await $(SpacesDrawerPage.chooserItem(space)).tap(); + await SpacesDrawerPage.userLooksAsExpected($, space); + } + }, + ); + + patrolWidgetTest( + 'guest - chooser - all spaces button works', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(OverallSpacesPage.guestShortcutBtn) + .tap(settleTimeout: const Duration(seconds: 10)); + await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); + await $(SpacesDrawerPage.allSpacesBtn).tap(); + expect($(OverallSpacesPage.spacesListView), findsOneWidget); + }, + ); + + patrolWidgetTest( + 'user - chooser - all spaces button works', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(OverallSpacesPage.userShortcutBtn) + .tap(settleTimeout: const Duration(seconds: 10)); + await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); + await $(SpacesDrawerPage.allSpacesBtn).tap(); + expect($(OverallSpacesPage.spacesListView), findsOneWidget); + }, + ); + }, + ); } diff --git a/catalyst_voices/apps/voices/integration_test/onboarding_test.dart b/catalyst_voices/apps/voices/integration_test/onboarding_test.dart new file mode 100644 index 00000000000..af666e9739c --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/onboarding_test.dart @@ -0,0 +1,52 @@ +import 'package:catalyst_voices/app/view/app.dart'; +import 'package:catalyst_voices/configs/bootstrap.dart'; +import 'package:catalyst_voices/routes/routes.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:patrol_finders/patrol_finders.dart'; + +import 'pageobject/app_bar_page.dart'; +import 'pageobject/onboarding_page.dart'; +import 'pageobject/overall_spaces_page.dart'; + +void main() async { + late final GoRouter router; + + setUpAll(() async { + router = buildAppRouter(); + }); + + setUp(() async { + await registerDependencies(config: const AppConfig()); + router.go(const DiscoveryRoute().location); + }); + + tearDown(() async { + await restartDependencies(); + }); + + group('Onboarding -', () { + patrolWidgetTest( + 'visitor - get started button works', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(OverallSpacesPage.visitorShortcutBtn) + .tap(settleTimeout: const Duration(seconds: 10)); + await $(AppBarPage.getStartedBtn).tap(); + expect($(OnboardingPage.registrationInfoPanel), findsOneWidget); + expect($(OnboardingPage.registrationDetailsPanel), findsOneWidget); + }, + ); + + patrolWidgetTest( + 'visitor - get started screen looks as expected', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: const Duration(seconds: 10)); + await OnboardingPage.getStartedScreenLooksAsExpected($); + }, + ); + }); +} diff --git a/catalyst_voices/apps/voices/integration_test/pageobject/app_bar_page.dart b/catalyst_voices/apps/voices/integration_test/pageobject/app_bar_page.dart new file mode 100644 index 00000000000..ab5528f9136 --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/pageobject/app_bar_page.dart @@ -0,0 +1,8 @@ +library dashboard_page; + +import 'package:flutter/material.dart'; + +class AppBarPage { + static const spacesDrawerButton = Key('DrawerButton'); + static const getStartedBtn = Key('GetStartedButton'); +} diff --git a/catalyst_voices/apps/voices/integration_test/pageobject/common_page.dart b/catalyst_voices/apps/voices/integration_test/pageobject/common_page.dart new file mode 100644 index 00000000000..8581d62e1a4 --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/pageobject/common_page.dart @@ -0,0 +1,10 @@ +library dashboard_page; + +import 'package:flutter/material.dart'; + +class CommonPage { + static const decoratorData = Key('DecoratorData'); + static const decoratorIconBefore = Key('DecoratorIconBefore'); + static const decoratorIconAfter = Key('DecoratorIconAfter'); + static const dialogCloseButton = Key('DialogCloseButton'); +} diff --git a/catalyst_voices/apps/voices/integration_test/pageobject/onboarding_page.dart b/catalyst_voices/apps/voices/integration_test/pageobject/onboarding_page.dart new file mode 100644 index 00000000000..8b27bbfe509 --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/pageobject/onboarding_page.dart @@ -0,0 +1,159 @@ +library dashboard_page; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol_finders/patrol_finders.dart'; + +import '../types/registration_state.dart'; +import '../utils/translations_utils.dart'; +import 'common_page.dart'; + +class OnboardingPage { + static const registrationInfoPanel = Key('RegistrationInfoPanel'); + static const registrationDetailsPanel = Key('RegistrationDetailsPanel'); + static const registrationInfoLearnMoreButton = Key('LearnMoreButton'); + static const headerTitle = Key('HeaderTitle'); + static const headerSubtitle = Key('HeaderSubtitle'); + static const headerBody = Key('HeaderBody'); + static const registrationInfoPictureContainer = Key('PictureContainer'); + static const registrationInfoTaskPicture = Key('TaskPictureIconBox'); + static const registrationDetailsTitle = Key('RegistrationDetailsTitle'); + static const registrationDetailsBody = Key('RegistrationDetailsBody'); + + static Future infoPartHeaderTitleText(PatrolTester $) async { + return $(registrationInfoPanel).$(headerTitle).text; + } + + static Future infoPartHeaderSubtitleText(PatrolTester $) async { + return $(registrationInfoPanel).$(headerSubtitle).text; + } + + static Future infoPartHeaderBodyText(PatrolTester $) async { + return $(registrationInfoPanel).$(headerBody).text; + } + + static Future infoPartLearnMoreButtonText(PatrolTester $) async { + final child = find.descendant( + of: $(registrationInfoPanel).$(CommonPage.decoratorData), + matching: find.byType(Text), + ); + return $(child).text; + } + + static Finder infoPartTaskPicture(PatrolTester $) { + final child = find.descendant( + of: $(registrationInfoPanel).$(registrationInfoPictureContainer), + matching: find.byType(IconTheme), + ); + return child; + } + + static String? detailsPartGetStartedTitle(PatrolTester $) { + final child = find.descendant( + of: $(registrationDetailsPanel).$(registrationDetailsTitle), + matching: find.byType(Text), + ); + return $(child).text; + } + + static String? detailsPartGetStartedBody(PatrolTester $) { + final child = find.descendant( + of: $(registrationDetailsPanel).$(registrationDetailsBody), + matching: find.byType(Text), + ); + return $(child).text; + } + + static String? detailsPartGetStartedQuestionText(PatrolTester $) { + return $(registrationDetailsPanel).$(const Key('GetStartedQuestion')).text; + } + + static Future detailsPartGetStartedCreateNewBtn( + PatrolTester $, + ) async { + return $(registrationDetailsPanel) + .$(const Key('CreateAccountType.createNew')); + } + + static Future detailsPartGetStartedRecoverBtn( + PatrolTester $, + ) async { + return $(registrationDetailsPanel) + .$(const Key('CreateAccountType.recover')); + } + + static Future getStartedScreenLooksAsExpected(PatrolTester $) async { + await registrationInfoPanelLooksAsExpected($, RegistrationState.getStarted); + await registrationDetailsPanelLooksAsExpected( + $, + RegistrationState.getStarted, + ); + } + + static Future registrationInfoPanelLooksAsExpected( + PatrolTester $, + RegistrationState step, + ) async { + switch (step) { + case RegistrationState.getStarted: + expect(await infoPartHeaderTitleText($), T.get('Get Started')); + expect(await infoPartLearnMoreButtonText($), T.get('Learn More')); + expect(infoPartTaskPicture($), findsOneWidget); + break; + case RegistrationState.checkYourKeychain: + throw UnimplementedError(); + case RegistrationState.createKeychain: + throw UnimplementedError(); + case RegistrationState.keychainCreated: + throw UnimplementedError(); + case RegistrationState.keychainRestoreInfo: + throw UnimplementedError(); + case RegistrationState.keychainRestoreInput: + throw UnimplementedError(); + case RegistrationState.keychainRestoreStart: + throw UnimplementedError(); + case RegistrationState.keychainRestoreSuccess: + throw UnimplementedError(); + case RegistrationState.mnemonicInput: + throw UnimplementedError(); + case RegistrationState.mnemonicVerified: + throw UnimplementedError(); + case RegistrationState.mnemonicWritedown: + throw UnimplementedError(); + case RegistrationState.passwordInfo: + throw UnimplementedError(); + case RegistrationState.passwordInput: + throw UnimplementedError(); + } + } + + static Future registrationDetailsPanelLooksAsExpected( + PatrolTester $, + RegistrationState getStarted, + ) async { + expect( + detailsPartGetStartedTitle($), + T.get('Welcome to Catalyst'), + ); + expect( + detailsPartGetStartedBody($), + isNotEmpty, + ); + expect( + detailsPartGetStartedQuestionText($), + T.get('What do you want to do?'), + ); + expect( + await detailsPartGetStartedCreateNewBtn($), + findsOneWidget, + ); + expect( + await detailsPartGetStartedRecoverBtn($), + findsOneWidget, + ); + expect( + $(CommonPage.dialogCloseButton), + findsOneWidget, + ); + } +} diff --git a/catalyst_voices/apps/voices/integration_test/pageobject/overall_spaces_page.dart b/catalyst_voices/apps/voices/integration_test/pageobject/overall_spaces_page.dart new file mode 100644 index 00000000000..6ee344a019c --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/pageobject/overall_spaces_page.dart @@ -0,0 +1,15 @@ +library dashboard_page; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +class OverallSpacesPage { + static const guestShortcutBtn = Key('GuestShortcut'); + static const visitorShortcutBtn = Key('VisitorShortcut'); + static const userShortcutBtn = Key('UserShortcut'); + static const spacesListView = Key('SpacesListView'); + + static Key spaceOverview(Space space) { + return Key('SpaceOverview.${space.name}'); + } +} diff --git a/catalyst_voices/apps/voices/integration_test/pageobject/spaces_drawer_page.dart b/catalyst_voices/apps/voices/integration_test/pageobject/spaces_drawer_page.dart new file mode 100644 index 00000000000..056d611b171 --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/pageobject/spaces_drawer_page.dart @@ -0,0 +1,159 @@ +library spaces_drawer_page; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol_finders/patrol_finders.dart'; + +class SpacesDrawerPage { + static const closeBtn = Key('MenuCloseButton'); + static const guestMenuItems = Key('GuestMenuItems'); + static const allSpacesBtn = Key('DrawerChooserAllSpacesButton'); + static const chooserPrevBtn = Key('DrawerChooserPreviousButton'); + static const chooserNextBtn = Key('DrawerChooserNextButton'); + static const chooserItemContainer = Key('DrawerChooserItem'); + static const userDiscoveryDashboardTile = Key('DiscoveryDashboardTile'); + static const userRolesTile = Key('RolesTile'); + static const userFeedbackTile = Key('FeedbackTile'); + static const userDocumentationTile = Key('DocumentationTile'); + static const userDrawerMenuItem = Key('UserDrawerMenuItem'); + + static Key chooserItem(Space space) { + return Key('DrawerChooser$space'); + } + + static Key userHeader(Space space) { + return Key('SpaceHeader.${space.name}'); + } + + static Key chooserIcon(Space space) { + return Key('DrawerChooser${space}AvatarKey'); + } + + static Key userMenuContainer(Space space) { + return Key('Drawer${space}MenuKey'); + } + + static Key userSectionHeader(Space space) { + return Key('Header.${space.name}'); + } + + static void commonElementsLookAsExpected(PatrolTester $) { + expect($(closeBtn), findsOneWidget); + expect($(allSpacesBtn), findsOneWidget); + expect($(chooserPrevBtn), findsOneWidget); + expect($(chooserNextBtn), findsOneWidget); + expect($(chooserItemContainer), findsExactly(5)); + } + + static Future guestLooksAsExpected(PatrolTester $, Space space) async { + expect( + $(SpacesDrawerPage.chooserIcon(space)), + findsOneWidget, + ); + final children = find.descendant( + of: $(guestMenuItems), + matching: find.byWidgetPredicate((widget) => true), + ); + expect($(children), findsAtLeast(1)); + } + + static Future userLooksAsExpected(PatrolTester $, Space space) async { + switch (space) { + case Space.discovery: + userDiscoveryLooksAsExpected($); + break; + case Space.workspace: + userWorkspaceLooksAsExpected($); + break; + case Space.voting: + userVotingLooksAsExpected($); + break; + case Space.fundedProjects: + userFundedProjectsLooksAsExpected($); + break; + case Space.treasury: + userTreasuryLooksAsExpected($); + break; + } + } + + static void userDiscoveryLooksAsExpected(PatrolTester $) { + expect( + $(userMenuContainer(Space.discovery)).$(userHeader(Space.discovery)), + findsOneWidget, + ); + expect( + $(userMenuContainer(Space.discovery)).$(userDiscoveryDashboardTile), + findsOneWidget, + ); + expect( + $(userMenuContainer(Space.discovery)).$(userRolesTile), + findsOneWidget, + ); + expect( + $(userMenuContainer(Space.discovery)).$(userFeedbackTile), + findsOneWidget, + ); + expect( + $(userMenuContainer(Space.discovery)).$(userDocumentationTile), + findsOneWidget, + ); + } + + static void userWorkspaceLooksAsExpected(PatrolTester $) { + expect( + $(userMenuContainer(Space.workspace)).$(userHeader(Space.workspace)), + findsOneWidget, + ); + expect( + $(userMenuContainer(Space.workspace)) + .$(userSectionHeader(Space.workspace)), + findsOneWidget, + ); + final children = find.descendant( + of: $(userMenuContainer(Space.workspace)), + matching: $(userDrawerMenuItem), + ); + expect($(children), findsAtLeast(1)); + } + + static void userVotingLooksAsExpected(PatrolTester $) { + expect( + $(userMenuContainer(Space.voting)).$(userHeader(Space.voting)), + findsOneWidget, + ); + expect( + $(userMenuContainer(Space.voting)).$(userSectionHeader(Space.voting)), + findsOneWidget, + ); + final children = find.descendant( + of: $(userMenuContainer(Space.voting)), + matching: $(userDrawerMenuItem), + ); + expect($(children), findsAtLeast(1)); + } + + static void userFundedProjectsLooksAsExpected(PatrolTester $) { + expect( + $(userMenuContainer(Space.fundedProjects)), + findsOneWidget, + ); + } + + static void userTreasuryLooksAsExpected(PatrolTester $) { + expect( + $(userMenuContainer(Space.treasury)).$(userHeader(Space.treasury)), + findsOneWidget, + ); + expect( + $(userMenuContainer(Space.treasury)).$(userSectionHeader(Space.treasury)), + findsOneWidget, + ); + final children = find.descendant( + of: $(userMenuContainer(Space.treasury)), + matching: $(userDrawerMenuItem), + ); + expect($(children), findsAtLeast(1)); + } +} diff --git a/catalyst_voices/apps/voices/integration_test/types/registration_state.dart b/catalyst_voices/apps/voices/integration_test/types/registration_state.dart new file mode 100644 index 00000000000..c40c7ddd5b6 --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/types/registration_state.dart @@ -0,0 +1,15 @@ +enum RegistrationState { + checkYourKeychain, + createKeychain, + getStarted, + keychainCreated, + keychainRestoreInfo, + keychainRestoreInput, + keychainRestoreStart, + keychainRestoreSuccess, + mnemonicInput, + mnemonicVerified, + mnemonicWritedown, + passwordInfo, + passwordInput; +} diff --git a/catalyst_voices/apps/voices/integration_test/utils/selector_utils.dart b/catalyst_voices/apps/voices/integration_test/utils/selector_utils.dart new file mode 100644 index 00000000000..67a0c980078 --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/utils/selector_utils.dart @@ -0,0 +1,22 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol_finders/patrol_finders.dart'; + +class SelectorUtils { + static void isDisabled( + PatrolTester $, + PatrolFinder widget, { + bool? reverse = false, + }) { + final widgetProps = $.tester.widget(widget).toString().split('(').last; + final expectedState = reverse! ? 'enabled' : 'disabled'; + expect( + widgetProps.contains('disabled'), + !reverse, + reason: 'Expected $expectedState (${widget.description})', + ); + } + + static void isEnabled(PatrolTester $, PatrolFinder widget) { + isDisabled($, widget, reverse: true); + } +} diff --git a/catalyst_voices/apps/voices/integration_test/utils/translations_utils.dart b/catalyst_voices/apps/voices/integration_test/utils/translations_utils.dart new file mode 100644 index 00000000000..3e409608a63 --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/utils/translations_utils.dart @@ -0,0 +1,8 @@ +//wrapper that we should adapt to read actual i18n translations we use in app +//it will also support different locales once we have it +//now this is here so we can easily replace this implementation and know where +class T { + static String get(String key, {String? locale}) { + return key; + } +} diff --git a/catalyst_voices/apps/voices/lib/app/view/app.dart b/catalyst_voices/apps/voices/lib/app/view/app.dart index cadb91549c8..9b5cc5a8f89 100644 --- a/catalyst_voices/apps/voices/lib/app/view/app.dart +++ b/catalyst_voices/apps/voices/lib/app/view/app.dart @@ -38,14 +38,26 @@ class _AppState extends State { List _multiBlocProviders() { return [ - BlocProvider( - create: (_) => Dependencies.instance.get(), - ), - BlocProvider( - create: (_) => Dependencies.instance.get(), + BlocProvider( + create: (_) => Dependencies.instance.get(), ), BlocProvider( - create: (_) => Dependencies.instance.get(), + create: (_) => Dependencies.instance.get(), + ), + BlocProvider( + create: (_) => Dependencies.instance.get(), + ), + BlocProvider( + create: (_) => Dependencies.instance.get(), + ), + BlocProvider( + create: (_) => Dependencies.instance.get(), + ), + BlocProvider( + create: (context) => Dependencies.instance.get(), + ), + BlocProvider( + create: (context) => Dependencies.instance.get(), ), ]; } diff --git a/catalyst_voices/apps/voices/lib/app/view/app_active_state_listener.dart b/catalyst_voices/apps/voices/lib/app/view/app_active_state_listener.dart new file mode 100644 index 00000000000..7a4ada05151 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/app/view/app_active_state_listener.dart @@ -0,0 +1,49 @@ +import 'package:catalyst_voices/dependency/dependencies.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:flutter/material.dart'; + +/// Observes application lifecycle and updates any dependencies that are +/// interested in app state. +class AppActiveStateListener extends StatefulWidget { + final Widget child; + + const AppActiveStateListener({ + super.key, + required this.child, + }); + + @override + State createState() => _AppActiveStateListenerState(); +} + +class _AppActiveStateListenerState extends State { + late final AppLifecycleListener _listener; + + @override + void initState() { + super.initState(); + _listener = AppLifecycleListener( + onResume: _handleResumed, + onInactive: _handleInactive, + ); + } + + @override + void dispose() { + _listener.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } + + Future _handleResumed() async { + Dependencies.instance.get().isActive = true; + } + + Future _handleInactive() async { + Dependencies.instance.get().isActive = false; + } +} diff --git a/catalyst_voices/apps/voices/lib/app/view/app_content.dart b/catalyst_voices/apps/voices/lib/app/view/app_content.dart index be743c669a2..d27d3ca5844 100644 --- a/catalyst_voices/apps/voices/lib/app/view/app_content.dart +++ b/catalyst_voices/apps/voices/lib/app/view/app_content.dart @@ -1,3 +1,4 @@ +import 'package:catalyst_voices/app/view/app_active_state_listener.dart'; import 'package:catalyst_voices/app/view/app_precache_image_assets.dart'; import 'package:catalyst_voices/app/view/app_session_listener.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; @@ -51,9 +52,15 @@ class AppContentState extends State { brightness: Brightness.dark, ), builder: (context, child) { - return GlobalPrecacheImages( - child: GlobalSessionListener( - child: child ?? const SizedBox.shrink(), + return Scaffold( + primary: false, + backgroundColor: Colors.transparent, + body: AppActiveStateListener( + child: GlobalPrecacheImages( + child: GlobalSessionListener( + child: child ?? const SizedBox.shrink(), + ), + ), ), ); }, diff --git a/catalyst_voices/apps/voices/lib/common/codecs/markdown_codec.dart b/catalyst_voices/apps/voices/lib/common/codecs/markdown_codec.dart new file mode 100644 index 00000000000..08081267f70 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/common/codecs/markdown_codec.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter_quill/quill_delta.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:markdown_quill/markdown_quill.dart'; + +// Note. +// This codec is here because it depends on flutter_quill which is heavy +// package with lots different dependencies which we don't want to have in +// other packages. +// +// If we could have just Delta package it would be preferred to live in +// models/shared package +const markdown = MarkdownCodec(); + +final _mdDocument = md.Document(); +final _mdToDelta = MarkdownToDelta(markdownDocument: _mdDocument); +final _deltaToMd = DeltaToMarkdown( + customContentHandler: DeltaToMarkdown.escapeSpecialCharactersRelaxed, +); + +final class MarkdownCodec extends Codec { + const MarkdownCodec(); + + @override + Converter get decoder => const MarkdownEncoder(); + + @override + Converter get encoder => const MarkdownDecoder(); +} + +class MarkdownDecoder extends Converter { + const MarkdownDecoder(); + + @override + Delta convert(MarkdownData input) { + if (input.data.isEmpty) { + return Delta(); + } + + return _mdToDelta.convert(input.data); + } +} + +class MarkdownEncoder extends Converter { + const MarkdownEncoder(); + + @override + MarkdownData convert(Delta input) { + if (input.isEmpty) { + return const MarkdownData(''); + } + + final data = _deltaToMd.convert(input); + final trimmed = data.trim(); + + return MarkdownData(trimmed); + } +} diff --git a/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart index e4ab1f54554..1a59ca68baa 100644 --- a/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart +++ b/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart @@ -1,6 +1,6 @@ import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_localization/generated/catalyst_voices_localizations.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; extension GuidanceExt on GuidanceType { String localizedType(VoicesLocalizations localizations) => switch (this) { diff --git a/catalyst_voices/apps/voices/lib/common/ext/map_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/map_ext.dart new file mode 100644 index 00000000000..12e141efde7 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/common/ext/map_ext.dart @@ -0,0 +1,7 @@ +extension MapFilterExtension on Map { + Map useKeys(List keys) { + return Map.fromEntries( + entries.where((entry) => keys.contains(entry.key)), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart new file mode 100644 index 00000000000..8bd65d9a602 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +extension TimeOfDayExt on TimeOfDay { + String get formatted { + final hour = this.hour.toString().padLeft(2, '0'); + final minute = this.minute.toString().padLeft(2, '0'); + + return '$hour:$minute'; + } +} diff --git a/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart b/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart index 0829da4ce11..74ee6f28bb5 100644 --- a/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart +++ b/catalyst_voices/apps/voices/lib/common/formatters/date_formatter.dart @@ -27,4 +27,72 @@ abstract class DateFormatter { return DateFormat.yMMMMd().format(dateTime); } + + static String formatInDays( + VoicesLocalizations l10n, + DateTime dateTime, { + DateTime? from, + }) { + from ??= DateTimeExt.now(); + + final days = dateTime.isAfter(from) ? dateTime.difference(from).inDays : 0; + + return l10n.inXDays(days); + } + + static (String date, String time) formatDateTimeParts( + DateTime date, + ) { + final dayMonthFormatter = DateFormat('d MMMM').format(date); + final timeFormatter = DateFormat('HH:mm').format(date); + + return (dayMonthFormatter, timeFormatter); + } + + static String formatShortMonth( + VoicesLocalizations l10n, + DateTime dateTime, + ) { + return DateFormat.MMM().format(dateTime); + } + + /// Formats full date and time. + /// If [timeOnNewline] is true then the time will be placed on a new line. + /// + /// Example: + /// - Thu, 6 June 2024 10:00 am + static String formatFullDateTime( + DateTime dateTime, { + bool timeOnNewline = false, + }) { + final format = + timeOnNewline ? 'EEE, d MMMM yyyy\nh:mm a' : 'EEE, d MMMM yyyy h:mm a'; + return DateFormat(format).format(dateTime); + } + + /// Formats the timezone info extracted from the [dateTime]. + /// + /// Example: + /// - GMT+01:00 Central European Standard Time + static String formatTimezone(DateTime dateTime) { + final offset = _formatTimezoneOffset(dateTime.timeZoneOffset); + final timezone = dateTime.timeZoneName; + return 'GMT$offset $timezone'; + } + + static String _formatTimezoneOffset(Duration offset) { + if (offset.isNegative) { + return '-${_formatDurationHHmm(offset)}'; + } else { + return '+${_formatDurationHHmm(offset)}'; + } + } + + static String _formatDurationHHmm(Duration offset) { + final nf = NumberFormat('00'); + final hours = offset.inHours; + final minutes = offset.inMinutes - hours * Duration.minutesPerHour; + + return '${nf.format(hours)}:${nf.format(minutes)}'; + } } diff --git a/catalyst_voices/apps/voices/lib/configs/bootstrap.dart b/catalyst_voices/apps/voices/lib/configs/bootstrap.dart index 764087cb9dc..05578a7c649 100644 --- a/catalyst_voices/apps/voices/lib/configs/bootstrap.dart +++ b/catalyst_voices/apps/voices/lib/configs/bootstrap.dart @@ -7,6 +7,9 @@ import 'package:catalyst_voices/configs/sentry_service.dart'; import 'package:catalyst_voices/dependency/dependencies.dart'; import 'package:catalyst_voices/routes/guards/milestone_guard.dart'; import 'package:catalyst_voices/routes/routes.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -74,6 +77,30 @@ Future _doBootstrapAndRun(BootstrapWidgetBuilder builder) async { await _runApp(app); } +@visibleForTesting +GoRouter buildAppRouter({ + String? initialLocation, +}) { + return AppRouter.init( + initialLocation: initialLocation, + guards: const [ + MilestoneGuard(), + ], + ); +} + +@visibleForTesting +Future registerDependencies({required AppConfig config}) async { + if (!Dependencies.instance.isInitialized) { + await Dependencies.instance.init(config: config); + } +} + +@visibleForTesting +Future restartDependencies() async { + await Dependencies.instance.reset; +} + /// Initializes the application before it can be run. Should setup all /// the things which are necessary before the actual app is run, /// either via [runApp] or injected into a test environment during @@ -81,7 +108,9 @@ Future _doBootstrapAndRun(BootstrapWidgetBuilder builder) async { /// /// Initialization logic that is relevant for [runApp] scenario /// only should be added to [_doBootstrapAndRun], not here. -Future bootstrap() async { +Future bootstrap({ + GoRouter? router, +}) async { _loggingService ..level = kDebugMode ? Level.FINER : Level.OFF ..printLogs = kDebugMode; @@ -89,16 +118,17 @@ Future bootstrap() async { GoRouter.optionURLReflectsImperativeAPIs = true; setPathUrlStrategy(); - await Dependencies.instance.init(); + final configService = ConfigService(ConfigRepository()); + final config = await configService + .getAppConfig() + .onError((error, stackTrace) => const AppConfig()); + + await registerDependencies(config: config); // Key derivation needs to be initialized before it can be used await CatalystKeyDerivation.init(); - final router = AppRouter.init( - guards: const [ - MilestoneGuard(), - ], - ); + router ??= buildAppRouter(); Bloc.observer = AppBlocObserver(); diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart index e93ed705f77..9bfd7421253 100644 --- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart @@ -3,33 +3,52 @@ import 'dart:async'; import 'package:catalyst_cardano/catalyst_cardano.dart'; import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; final class Dependencies extends DependencyProvider { static final Dependencies instance = Dependencies._(); + bool _isInitialized = false; + Dependencies._(); - Future init() async { + Future init({ + required AppConfig config, + }) async { DependencyProvider.instance = this; + + registerSingleton(config); + + _registerStorages(); _registerServices(); _registerRepositories(); _registerBlocsWithDependencies(); + + _isInitialized = true; + } + + bool get isInitialized => _isInitialized; + + @override + Future get reset { + return super.reset.whenComplete(() { + _isInitialized = false; + }); } void _registerBlocsWithDependencies() { this - ..registerSingleton( - AuthenticationBloc( - authenticationRepository: get(), - ), + ..registerLazySingleton( + AdminToolsCubit.new, ) - ..registerLazySingleton( - () => LoginBloc( - authenticationRepository: get(), - ), + ..registerLazySingleton( + () => get(), ) ..registerLazySingleton( () { @@ -37,6 +56,8 @@ final class Dependencies extends DependencyProvider { get(), get(), get(), + get(), + get(), ); }, dispose: (cubit) async => cubit.close(), @@ -49,31 +70,70 @@ final class Dependencies extends DependencyProvider { registrationService: get(), progressNotifier: get(), ); + }) + ..registerLazySingleton( + () => ProposalsCubit( + get(), + get(), + get(), + ), + ) + ..registerFactory(() { + return CampaignDetailsBloc( + get(), + ); + }) + ..registerLazySingleton(() { + return CampaignInfoCubit( + get(), + get(), + ); + }) + // TODO(ryszard-schossler): add repository for campaign management + ..registerLazySingleton( + CampaignBuilderCubit.new, + ) + ..registerFactory(() { + return WorkspaceBloc( + get(), + ); + }) + ..registerFactory(() { + return ProposalBuilderBloc( + get(), + ); }); } void _registerRepositories() { this - ..registerLazySingleton( - () => CredentialsStorageRepository(storage: get()), - ) - ..registerLazySingleton( - () => AuthenticationRepository(credentialsStorageRepository: get()), - ) ..registerLazySingleton( TransactionConfigRepository.new, - ); + ) + ..registerLazySingleton(ProposalRepository.new) + ..registerLazySingleton(CampaignRepository.new) + ..registerLazySingleton(ConfigRepository.new) + ..registerLazySingleton(() { + return UserRepository( + get(), + get(), + ); + }); } void _registerServices() { registerLazySingleton(() => const SecureStorage()); registerLazySingleton(CatalystKeyDerivation.new); registerLazySingleton(() => KeyDerivation(get())); - registerLazySingleton(VaultKeychainProvider.new); - registerLazySingleton(SecureDummyAuthStorage.new); + registerLazySingleton(() { + return VaultKeychainProvider( + secureStorage: get(), + sharedPreferences: get(), + cacheConfig: get().cache, + ); + }); registerLazySingleton(Downloader.new); registerLazySingleton(() => CatalystCardano.instance); - registerLazySingleton(SecureUserStorage.new); registerLazySingleton( RegistrationProgressNotifier.new, ); @@ -88,11 +148,32 @@ final class Dependencies extends DependencyProvider { registerLazySingleton( () { return UserService( - keychainProvider: get(), - userStorage: get(), + userRepository: get(), ); }, dispose: (service) => unawaited(service.dispose()), ); + registerLazySingleton(AccessControl.new); + registerLazySingleton(() { + return CampaignService( + get(), + ); + }); + registerLazySingleton(() { + return ProposalService( + get(), + ); + }); + registerLazySingleton(() { + return ConfigService( + get(), + ); + }); + } + + void _registerStorages() { + registerLazySingleton(FlutterSecureStorage.new); + registerLazySingleton(SharedPreferencesAsync.new); + registerLazySingleton(SecureUserStorage.new); } } diff --git a/catalyst_voices/apps/voices/lib/pages/account/unlock_keychain_dialog.dart b/catalyst_voices/apps/voices/lib/pages/account/unlock_keychain_dialog.dart index 2c3b3385e45..deb0a32412e 100644 --- a/catalyst_voices/apps/voices/lib/pages/account/unlock_keychain_dialog.dart +++ b/catalyst_voices/apps/voices/lib/pages/account/unlock_keychain_dialog.dart @@ -155,6 +155,7 @@ class _UnlockPassword extends StatelessWidget { Widget build(BuildContext context) { return VoicesPasswordTextField( controller: controller, + autofocus: true, decoration: VoicesTextFieldDecoration( labelText: context.l10n.unlockDialogHint, errorText: error?.message(context), diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_dialog.dart b/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_dialog.dart new file mode 100644 index 00000000000..c83dd1977db --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_dialog.dart @@ -0,0 +1,348 @@ +import 'package:catalyst_voices/common/ext/space_ext.dart'; +import 'package:catalyst_voices/pages/campaign/admin_tools/campaign_admin_tools_events.dart'; +import 'package:catalyst_voices/pages/campaign/admin_tools/campaign_admin_tools_views.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// A draggable [CampaignAdminToolsDialog], +/// should be used as a child of a [Stack]. +/// +/// Initially shown at bottom-left corner with [initialOffset] offset. +class DraggableCampaignAdminToolsDialog extends StatefulWidget { + /// The key for the [CampaignAdminToolsDialog] to make sure it's state + /// is kept when a user keeps dragging it. + /// + /// The state might include currently open tab from [TabBar], + /// scroll position, etc. + final GlobalKey dialogKey; + + /// See [CampaignAdminToolsDialog.selectedSpace]. + final Space selectedSpace; + + /// See [CampaignAdminToolsDialog.onSpaceSelected]. + final ValueChanged onSpaceSelected; + + /// The initial offset from bottom-left for the dialog. + final Offset initialOffset; + + const DraggableCampaignAdminToolsDialog({ + super.key, + required this.dialogKey, + required this.selectedSpace, + required this.onSpaceSelected, + this.initialOffset = const Offset(32, 32), + }); + + @override + State createState() => + _DraggableCampaignAdminToolsDialogState(); +} + +class _DraggableCampaignAdminToolsDialogState + extends State { + Offset _position = Offset.infinite; + late Size _screenSize; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + _screenSize = MediaQuery.sizeOf(context); + + if (_position.isInfinite) { + // initialize it for the first time + _position = Offset( + widget.initialOffset.dx, + _screenSize.height - + CampaignAdminToolsDialog._height - + widget.initialOffset.dy, + ); + } else { + // clamp it so that it fits into the screen + // in case user shrinks the app window + + _position = _clampIntoScreenBounds(_position); + } + } + + @override + Widget build(BuildContext context) { + final Widget child = CampaignAdminToolsDialog( + key: widget.dialogKey, + selectedSpace: widget.selectedSpace, + onSpaceSelected: widget.onSpaceSelected, + ); + + return Positioned( + left: _position.dx, + top: _position.dy, + child: Draggable( + onDragUpdate: _onDragUpdate, + childWhenDragging: const Offstage(), + feedback: child, + child: child, + ), + ); + } + + void _onDragUpdate(DragUpdateDetails details) { + final newPosition = _position + details.delta; + final clampedPosition = _clampIntoScreenBounds(newPosition); + + if (_position != clampedPosition) { + setState(() { + _position = clampedPosition; + }); + } + } + + /// Makes sure the dialog would fit into a screen window + /// even if the window gets shrunk, etc. + Offset _clampIntoScreenBounds(Offset offset) { + return Offset( + offset.dx.clamp( + 0, + _screenSize.width - CampaignAdminToolsDialog._width, + ), + offset.dy.clamp( + 0, + _screenSize.height - CampaignAdminToolsDialog._height, + ), + ); + } +} + +/// The campaign admin tools dialog which supports +/// mocking different campaign and user states. +/// +/// With it you can mock the keychain, mock the +/// campaign state or quickly switch between states. +class CampaignAdminToolsDialog extends StatelessWidget { + /// The keyboard shortcut to activate the admin tools. + /// + /// You must handle the shortcut in the parent widget, + /// it is added here for convenience. + static final ShortcutActivator shortcut = LogicalKeySet( + LogicalKeyboardKey.control, + LogicalKeyboardKey.shift, + LogicalKeyboardKey.keyA, + ); + + static const double _width = 360; + static const double _height = 480; + + /// The current selected [Space]. + /// Admin tools reuse the currently selected space from the navigation drawer. + final Space selectedSpace; + + /// Callback to notify when [Space] changes. + /// In response to this event the app should navigate to given space. + final ValueChanged onSpaceSelected; + + const CampaignAdminToolsDialog({ + super.key, + required this.selectedSpace, + required this.onSpaceSelected, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: _width, + height: _height, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colors.onSurfaceNeutral012!, + width: 1, + ), + ), + child: Material( + type: MaterialType.transparency, + child: Column( + children: [ + const _Header(), + const Expanded(child: _Tabs()), + _BlocFooter( + selectedSpace: selectedSpace, + onSpaceSelected: onSpaceSelected, + ), + ], + ), + ), + ); + } +} + +class _Header extends StatelessWidget { + const _Header(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(24, 12, 12, 12), + child: Row( + children: [ + Expanded( + child: Text( + context.l10n.campaignPreviewTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + XButton( + onTap: () => context.read().disable(), + ), + ], + ), + ); + } +} + +class _Tabs extends StatelessWidget { + const _Tabs(); + + @override + Widget build(BuildContext context) { + return const DefaultTabController( + length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _TabBar(), + Expanded( + child: TabBarStackView( + children: [ + CampaignAdminToolsEventsTab(), + CampaignAdminToolsViewsTab(), + ], + ), + ), + ], + ), + ); + } +} + +class _TabBar extends StatelessWidget { + const _TabBar(); + + @override + Widget build(BuildContext context) { + return TabBar( + tabAlignment: TabAlignment.fill, + indicatorSize: TabBarIndicatorSize.tab, + tabs: [ + Tab( + text: context.l10n.campaignPreviewEvents, + ), + Tab( + text: context.l10n.campaignPreviewViews, + ), + ], + ); + } +} + +class _BlocFooter extends StatelessWidget { + final Space selectedSpace; + final ValueChanged onSpaceSelected; + + const _BlocFooter({ + required this.selectedSpace, + required this.onSpaceSelected, + }); + + @override + Widget build(BuildContext context) { + return BlocSelector>( + selector: (state) => state.spaces, + builder: (context, spaces) { + return Offstage( + // don't show footer with spaces if there's only once, + // since the user can't change it to anything else + offstage: spaces.length <= 1, + child: _Footer( + selectedSpace: selectedSpace, + onSpaceSelected: onSpaceSelected, + ), + ); + }, + ); + } +} + +class _Footer extends StatelessWidget { + final Space selectedSpace; + final ValueChanged onSpaceSelected; + + const _Footer({ + required this.selectedSpace, + required this.onSpaceSelected, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider( + height: 0, + indent: 0, + endIndent: 0, + ), + Padding( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final space in Space.values) + _SpaceItem( + space: space, + isActive: space == selectedSpace, + onTap: () => onSpaceSelected(space), + ), + ].separatedBy(const SizedBox(width: 8)).toList(), + ), + ), + ], + ); + } +} + +class _SpaceItem extends StatelessWidget { + final Space space; + final bool isActive; + final VoidCallback onTap; + + const _SpaceItem({ + required this.space, + required this.isActive, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return VoicesAvatar( + icon: space.icon.buildIcon(), + backgroundColor: + isActive ? space.backgroundColor(context) : Colors.transparent, + foregroundColor: isActive + ? space.foregroundColor(context) + : Theme.of(context).colors.iconsForeground, + padding: const EdgeInsets.all(8), + radius: 20, + onTap: onTap, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_events.dart b/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_events.dart new file mode 100644 index 00000000000..f6ccff21016 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_events.dart @@ -0,0 +1,354 @@ +import 'dart:async'; + +import 'package:catalyst_voices/pages/campaign/admin_tools/campaign_admin_tools_dialog.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// The "events" tab of the [CampaignAdminToolsDialog]. +class CampaignAdminToolsEventsTab extends StatefulWidget { + const CampaignAdminToolsEventsTab({super.key}); + + @override + State createState() => + _CampaignAdminToolsEventsTabState(); +} + +class _CampaignAdminToolsEventsTabState + extends State { + static const _defaultStageTransitionDelay = Duration(seconds: 5); + + Timer? _stageTimer; + DateTime? _stageTransitionAt; + Duration _stageTransitionDelay = _defaultStageTransitionDelay; + + @override + void dispose() { + _stageTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.campaignStage, + builder: (context, stage) { + return Column( + children: [ + Expanded( + child: _CampaignStatusChooser( + selectedStage: stage, + onStageSelected: _onStageSelected, + ), + ), + _EventTimelapseControls( + nextStageTransitionAt: _stageTransitionAt, + stageTransitionDelay: _stageTransitionDelay, + onPreviousStage: _canSelectPreviousStage(stage) + ? () => _onPreviousStage(stage) + : null, + onNextStage: + _canSelectNextStage(stage) ? () => _onNextStage(stage) : null, + onTransitionDelayChanged: _onTransitionDelayChanged, + ), + ], + ); + }, + ); + } + + void _onStageSelected(CampaignStage stage) { + setState(() { + _stageTimer?.cancel(); + _stageTimer = Timer( + _stageTransitionDelay, + () => _updateStage(stage), + ); + _stageTransitionAt = DateTimeExt.now().add(_stageTransitionDelay); + }); + } + + bool _canSelectPreviousStage(CampaignStage currentStage) { + // draft stage is not supported + + final previousIndex = currentStage.index - 1; + return previousIndex > CampaignStage.draft.index; + } + + bool _canSelectNextStage(CampaignStage currentStage) { + final nextIndex = currentStage.index + 1; + return nextIndex < CampaignStage.values.length; + } + + void _onPreviousStage(CampaignStage currentStage) { + if (!_canSelectPreviousStage(currentStage)) return; + + _onStageSelected(CampaignStage.values[currentStage.index - 1]); + } + + void _onNextStage(CampaignStage currentStage) { + if (!_canSelectNextStage(currentStage)) return; + + _onStageSelected(CampaignStage.values[currentStage.index + 1]); + } + + void _updateStage(CampaignStage stage) { + context.read().updateCampaignStage(stage); + + setState(() { + _stageTransitionAt = null; + }); + } + + void _onTransitionDelayChanged(Duration delay) { + setState(() { + _stageTransitionDelay = delay; + }); + } +} + +class _CampaignStatusChooser extends StatelessWidget { + final CampaignStage selectedStage; + final ValueChanged onStageSelected; + + const _CampaignStatusChooser({ + required this.selectedStage, + required this.onStageSelected, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, + child: Column( + children: [ + const SizedBox(height: 8), + for (final stage in CampaignStage.values) + if (stage != CampaignStage.draft) + _EventItem( + stage: stage, + isActive: stage == selectedStage, + onTap: () => onStageSelected(stage), + ), + const SizedBox(height: 8), + ], + ), + ); + } +} + +class _EventItem extends StatelessWidget { + final CampaignStage stage; + final bool isActive; + final VoidCallback onTap; + + const _EventItem({ + required this.stage, + required this.isActive, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final color = isActive + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colors.textOnPrimary; + + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 24, 12), + child: Row( + children: [ + _icon.buildIcon( + size: 24, + color: color, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + _text(context.l10n), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: color, + ), + ), + ), + if (isActive) + Text( + context.l10n.active.toUpperCase(), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w500, + fontSize: 11, + color: color, + ), + ), + ], + ), + ), + ); + } + + SvgGenImage get _icon => switch (stage) { + CampaignStage.draft => VoicesAssets.icons.clock, + CampaignStage.scheduled => VoicesAssets.icons.clock, + CampaignStage.live => VoicesAssets.icons.flag, + CampaignStage.completed => VoicesAssets.icons.calendar, + }; + + String _text(VoicesLocalizations l10n) => switch (stage) { + CampaignStage.draft => l10n.campaignPreviewEventBefore, + CampaignStage.scheduled => l10n.campaignPreviewEventBefore, + CampaignStage.live => l10n.campaignPreviewEventDuring, + CampaignStage.completed => l10n.campaignPreviewEventAfter, + }; +} + +class _EventTimelapseControls extends StatefulWidget { + final DateTime? nextStageTransitionAt; + final Duration stageTransitionDelay; + final VoidCallback? onPreviousStage; + final VoidCallback? onNextStage; + final ValueChanged onTransitionDelayChanged; + + const _EventTimelapseControls({ + required this.nextStageTransitionAt, + required this.stageTransitionDelay, + required this.onPreviousStage, + required this.onNextStage, + required this.onTransitionDelayChanged, + }); + + @override + State<_EventTimelapseControls> createState() => + _EventTimelapseControlsState(); +} + +class _EventTimelapseControlsState extends State<_EventTimelapseControls> { + final _timerController = TextEditingController(); + Timer? _refreshTimer; + bool _enabled = true; + + @override + void initState() { + super.initState(); + + _refresh(); + _restartRefreshTimer(); + } + + @override + void didUpdateWidget(_EventTimelapseControls oldWidget) { + super.didUpdateWidget(oldWidget); + _refresh(); + _restartRefreshTimer(); + } + + @override + void dispose() { + _timerController.dispose(); + _refreshTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Expanded( + child: VoicesTextButton( + leading: VoicesAssets.icons.rewind.buildIcon(), + onTap: widget.onPreviousStage, + child: Text(context.l10n.previous), + ), + ), + Container( + width: 60, + height: 56, + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, + ), + alignment: Alignment.center, + child: TextField( + controller: _timerController, + onChanged: (_) => _onTransitionDelayChanged(), + enabled: _enabled, + decoration: null, + textAlign: TextAlign.center, + ), + ), + Expanded( + child: VoicesTextButton( + leading: VoicesAssets.icons.fastForward.buildIcon(), + onTap: widget.onNextStage, + child: Text(context.l10n.next), + ), + ), + ], + ), + ); + } + + void _restartRefreshTimer() { + _refreshTimer?.cancel(); + _refreshTimer = Timer.periodic( + const Duration(seconds: 1), + (_) => _refresh(), + ); + } + + void _refresh() { + final now = DateTimeExt.now(); + final nextStageTransitionAt = widget.nextStageTransitionAt; + + if (nextStageTransitionAt != null && nextStageTransitionAt.isAfter(now)) { + final remainingDelay = nextStageTransitionAt.difference(now); + _updateTimerController(remainingDelay, enabled: false); + } else { + _updateTimerController(widget.stageTransitionDelay, enabled: true); + } + } + + void _updateTimerController(Duration duration, {required bool enabled}) { + // only update if the duration is different, + // otherwise we might be overwriting local user edits + if (_stageTransitionDelay != duration) { + final seconds = + (duration.inMilliseconds / Duration.millisecondsPerSecond).ceil(); + final text = '${seconds}s'; + _timerController.value = TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ); + } + + if (enabled != _enabled) { + setState(() { + _enabled = enabled; + }); + } + } + + void _onTransitionDelayChanged() { + final duration = _stageTransitionDelay; + if (duration != null && duration != widget.stageTransitionDelay) { + widget.onTransitionDelayChanged(duration); + } + } + + Duration? get _stageTransitionDelay { + final cleanedString = + _timerController.text.replaceAll('s', '').replaceAll(' ', ''); + final seconds = int.tryParse(cleanedString); + return seconds != null ? Duration(seconds: seconds) : null; + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_views.dart b/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_views.dart new file mode 100644 index 00000000000..801d79e2fba --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/admin_tools/campaign_admin_tools_views.dart @@ -0,0 +1,60 @@ +import 'package:catalyst_voices/pages/campaign/admin_tools/campaign_admin_tools_dialog.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_segmented_button.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// The "views" tab of the [CampaignAdminToolsDialog]. +class CampaignAdminToolsViewsTab extends StatelessWidget { + const CampaignAdminToolsViewsTab({super.key}); + + @override + Widget build(BuildContext context) { + return Material( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.l10n.userAuthenticationState, + style: Theme.of(context).textTheme.labelLarge, + ), + const SizedBox(height: 16), + const _SessionStatus(), + ], + ), + ), + ); + } +} + +class _SessionStatus extends StatelessWidget { + const _SessionStatus(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.sessionStatus, + builder: (context, sessionStatus) { + return VoicesSegmentedButton( + segments: [ + for (final userState in SessionStatus.values) + ButtonSegment( + value: userState, + label: Text(userState.name(context.l10n)), + ), + ], + selected: {sessionStatus}, + onChanged: (selected) { + context.read().updateSessionStatus(selected.first); + }, + ); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/details/campaign_details_dialog.dart b/catalyst_voices/apps/voices/lib/pages/campaign/details/campaign_details_dialog.dart new file mode 100644 index 00000000000..684625386cc --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/details/campaign_details_dialog.dart @@ -0,0 +1,69 @@ +import 'dart:async'; + +import 'package:catalyst_voices/dependency/dependencies.dart'; +import 'package:catalyst_voices/pages/campaign/details/widgets/campaign_header.dart'; +import 'package:catalyst_voices/pages/campaign/details/widgets/campaign_sections_list_view.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CampaignDetailsDialog extends StatefulWidget { + final String id; + + const CampaignDetailsDialog._({ + required this.id, + }); + + static Future show( + BuildContext context, { + required String id, + }) { + return VoicesDialog.show( + context: context, + routeSettings: RouteSettings(name: '/campaign/$id'), + builder: (context) => CampaignDetailsDialog._(id: id), + barrierDismissible: true, + ); + } + + @override + State createState() => _CampaignDetailsDialogState(); +} + +class _CampaignDetailsDialogState extends State { + late final CampaignDetailsBloc _bloc; + + @override + void initState() { + super.initState(); + _bloc = Dependencies.instance.get(); + _bloc.add(LoadCampaignEvent(id: widget.id)); + } + + @override + void didUpdateWidget(covariant CampaignDetailsDialog oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.id != oldWidget.id) { + _bloc.add(LoadCampaignEvent(id: widget.id)); + } + } + + @override + void dispose() { + unawaited(_bloc.close()); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _bloc, + child: const VoicesDetailsDialog( + header: CampaignHeader(), + body: CampaignSectionsListView(), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_categories_tile.dart b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_categories_tile.dart new file mode 100644 index 00000000000..2d6fb5c77d6 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_categories_tile.dart @@ -0,0 +1,165 @@ +import 'package:catalyst_voices/widgets/menu/voices_modal_menu.dart'; +import 'package:catalyst_voices/widgets/tiles/voices_expansion_tile.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:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class CampaignCategoriesTile extends StatefulWidget { + final List sections; + + const CampaignCategoriesTile({ + super.key, + required this.sections, + }); + + @override + State createState() => _CampaignCategoriesTileState(); +} + +class _CampaignCategoriesTileState extends State { + String? _selectedSectionId; + + @override + void initState() { + super.initState(); + + _selectedSectionId = widget.sections.firstOrNull?.id; + } + + @override + void didUpdateWidget(covariant CampaignCategoriesTile oldWidget) { + super.didUpdateWidget(oldWidget); + + if (!listEquals(widget.sections, oldWidget.sections)) { + if (!widget.sections.any((element) => element.id == _selectedSectionId)) { + _selectedSectionId = widget.sections.firstOrNull?.id; + } + } + } + + @override + Widget build(BuildContext context) { + final selectedSection = widget.sections + .singleWhereOrNull((element) => element.id == _selectedSectionId); + + return VoicesExpansionTile( + initiallyExpanded: true, + title: Text(context.l10n.campaignCategories), + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Menu( + selectedId: _selectedSectionId, + menuItems: widget.sections, + onTap: _updateSelection, + ), + const SizedBox(width: 32), + Expanded( + child: selectedSection != null + ? _Details(section: selectedSection) + : const SizedBox(), + ), + ], + ), + ], + ); + } + + void _updateSelection(String id) { + setState(() { + _selectedSectionId = id; + }); + } +} + +class _Menu extends StatelessWidget { + final String? selectedId; + final List menuItems; + final ValueChanged onTap; + + const _Menu({ + this.selectedId, + required this.menuItems, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colors = theme.colors; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text( + context.l10n.cardanoUseCases, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.titleSmall?.copyWith( + color: colors.textOnPrimaryLevel0, + ), + ), + const SizedBox(height: 12), + VoicesModalMenu( + selectedId: selectedId, + menuItems: menuItems, + onTap: onTap, + ), + const SizedBox(height: 16), + ], + ); + } +} + +class _Details extends StatelessWidget { + final CampaignCategorySection section; + + const _Details({ + required this.section, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colors = theme.colors; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 48), + Text( + section.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.headlineMedium?.copyWith( + color: colors.textOnPrimaryLevel1, + ), + ), + const SizedBox(height: 24), + Text( + section.title, + style: textTheme.titleLarge?.copyWith( + color: colors.textOnPrimaryLevel0, + ), + ), + const SizedBox(height: 16), + Text( + section.body, + style: textTheme.bodyLarge?.copyWith( + color: colors.textOnPrimaryLevel1, + ), + ), + const SizedBox(height: 32), + ], + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_details_tile.dart b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_details_tile.dart new file mode 100644 index 00000000000..6a0ad3138dd --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_details_tile.dart @@ -0,0 +1,210 @@ +import 'package:catalyst_voices/common/formatters/date_formatter.dart'; +import 'package:catalyst_voices/widgets/tiles/voices_expansion_tile.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/material.dart'; + +class CampaignDetailsTile extends StatelessWidget { + final String description; + final DateTime startDate; + final DateTime endDate; + final int categoriesCount; + final int proposalsCount; + + const CampaignDetailsTile({ + super.key, + required this.description, + required this.startDate, + required this.endDate, + required this.categoriesCount, + required this.proposalsCount, + }); + + @override + Widget build(BuildContext context) { + return VoicesExpansionTile( + initiallyExpanded: true, + title: Text(context.l10n.campaignDetails), + children: [ + _Body( + description: description, + ), + const SizedBox(height: 16 + 24), + _CampaignData( + startDate: startDate, + endDate: endDate, + categoriesCount: categoriesCount, + proposalsCount: proposalsCount, + ), + ], + ); + } +} + +class _Body extends StatelessWidget { + final String description; + + const _Body({ + required this.description, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colors = theme.colors; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.l10n.description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.titleSmall?.copyWith( + color: colors.textOnPrimaryLevel1, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 14), + Text( + description, + style: textTheme.bodyLarge?.copyWith( + color: colors.textOnPrimaryLevel1, + ), + ), + ], + ); + } +} + +class _CampaignData extends StatelessWidget { + final DateTime startDate; + final DateTime endDate; + final int categoriesCount; + final int proposalsCount; + + const _CampaignData({ + required this.startDate, + required this.endDate, + required this.categoriesCount, + required this.proposalsCount, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = theme.colors; + final l10n = context.l10n; + + return Container( + decoration: BoxDecoration( + color: colors.onSurfacePrimary012, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Row( + children: [ + _CampaignDataTile( + key: const ValueKey('StartDateTileKey'), + title: l10n.startDate, + subtitle: DateFormatter.formatInDays(l10n, startDate), + value: startDate.day, + valueSuffix: DateFormatter.formatShortMonth(l10n, startDate), + ), + _CampaignDataTile( + key: const ValueKey('EndDateTileKey'), + title: l10n.endDate, + subtitle: DateFormatter.formatInDays(l10n, endDate), + value: endDate.day, + valueSuffix: DateFormatter.formatShortMonth(l10n, endDate), + ), + _CampaignDataTile( + key: const ValueKey('CategoriesTileKey'), + title: l10n.categories, + subtitle: l10n.fundingCategories, + value: categoriesCount, + ), + _CampaignDataTile( + key: const ValueKey('ProposalsTileKey'), + title: l10n.proposals, + subtitle: l10n.totalSubmitted, + value: proposalsCount, + ), + ] + .map((e) => Expanded(child: e)) + .separatedBy(const SizedBox(width: 16)) + .toList(), + ), + ); + } +} + +class _CampaignDataTile extends StatelessWidget { + final String title; + final String subtitle; + final int value; + final String? valueSuffix; + + const _CampaignDataTile({ + super.key, + required this.title, + required this.subtitle, + required this.value, + this.valueSuffix, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colors = theme.colors; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + title, + style: textTheme.titleSmall?.copyWith( + color: colors.textOnPrimaryLevel1, + ), + ), + Text( + subtitle, + style: textTheme.bodySmall?.copyWith( + // TODO(damian-molinski): This color does not have property. + // Colors/sys color neutral md ref/N60 + color: const Color(0xFF7F90B3), + ), + ), + const SizedBox(height: 16), + Row( + textBaseline: TextBaseline.alphabetic, + crossAxisAlignment: valueSuffix != null + ? CrossAxisAlignment.baseline + : CrossAxisAlignment.end, + children: [ + Text( + '$value', + style: textTheme.headlineLarge?.copyWith( + color: colors.textOnPrimaryLevel1, + ), + ), + if (valueSuffix != null) ...[ + const SizedBox(width: 4), + Text( + valueSuffix!, + style: textTheme.titleMedium?.copyWith( + color: colors.textOnPrimaryLevel1, + ), + ), + ], + ], + ), + ], + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_header.dart b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_header.dart new file mode 100644 index 00000000000..f3c4ac7ca6f --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_header.dart @@ -0,0 +1,22 @@ +import 'package:catalyst_voices/widgets/modals/details/voices_details_dialog_header.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CampaignHeader extends StatelessWidget { + const CampaignHeader({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.title, + builder: (context, state) { + return VoicesDetailsDialogHeader( + title: state ?? '', + titleLabel: context.l10n.campaign, + ); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_management.dart b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_management.dart new file mode 100644 index 00000000000..0e1b9463dde --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_management.dart @@ -0,0 +1,137 @@ +import 'dart:async'; + +import 'package:catalyst_voices/pages/campaign/details/widgets/campaign_management_dialog.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CampaignManagement extends StatefulWidget { + const CampaignManagement({super.key}); + + @override + State createState() => _CampaignManagementState(); +} + +class _CampaignManagementState extends State { + @override + void initState() { + super.initState(); + context.read().getCampaignStatus(); + } + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.publish, + builder: (context, publish) { + return Row( + children: [ + VoicesOutlinedButton( + child: Text(context.l10n.campaignManagement), + onTap: () => unawaited(_showManagementDialog(publish)), + ), + _CampaignStatusIndicator( + campaignStatus: CampaignPublish.draft, + currentStatus: publish, + ), + _CampaignStatusIndicator( + campaignStatus: CampaignPublish.published, + currentStatus: publish, + ), + ], + ); + }, + ); + } + + Future _showManagementDialog(CampaignPublish? publish) async { + final result = await CampaignManagementDialog.show(context, publish); + if (mounted) { + _handleDialogResult(result); + } + } + + void _handleDialogResult(CampaignPublish? newPublish) { + if (newPublish == null) return; + context.read().updateCampaignPublish(newPublish); + } +} + +class _CampaignStatusIndicator extends StatelessWidget { + final CampaignPublish campaignStatus; + final CampaignPublish? currentStatus; + + const _CampaignStatusIndicator({ + required this.campaignStatus, + required this.currentStatus, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: DecoratedBox( + decoration: BoxDecoration( + color: currentStatus == campaignStatus + ? theme.colors.success + : theme.colors.onSurfaceNeutral012?.withOpacity(.12), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 16, 8), + child: Row( + children: [ + VoicesAssets.icons.check.buildIcon( + color: currentStatus == campaignStatus + ? theme.colors.successContainer + : theme.colors.onSurfaceNeutral012, + ), + const SizedBox(width: 8), + switch (campaignStatus) { + CampaignPublish.draft => _Text( + context.l10n.draft, + isSelected: campaignStatus == currentStatus, + ), + CampaignPublish.published => _Text( + context.l10n.published, + isSelected: campaignStatus == currentStatus, + ), + }, + ], + ), + ), + ), + ); + } +} + +class _Text extends StatelessWidget { + final String text; + final bool isSelected; + + const _Text( + this.text, { + required this.isSelected, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Text( + text, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: isSelected + ? theme.colors.successContainer + : theme.colors.onSurfaceNeutral012, + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_management_dialog.dart b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_management_dialog.dart new file mode 100644 index 00000000000..0381912ccfe --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_management_dialog.dart @@ -0,0 +1,127 @@ +import 'package:catalyst_voices/widgets/modals/details/voices_align_title_header.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CampaignManagementDialog extends StatefulWidget { + final CampaignPublish? initialValue; + const CampaignManagementDialog._(this.initialValue); + + static Future show( + BuildContext context, + CampaignPublish? initialValue, + ) async { + final result = await VoicesDialog.show( + context: context, + builder: (context) => CampaignManagementDialog._(initialValue), + ); + + return result; + } + + @override + State createState() => _CampaignManagementDialogState(); +} + +class _CampaignManagementDialogState extends State { + late CampaignPublish _campaignPublish; + + @override + void initState() { + super.initState(); + _campaignPublish = widget.initialValue ?? CampaignPublish.draft; + } + + @override + Widget build(BuildContext context) { + return VoicesDetailsDialog( + constraints: const BoxConstraints(maxWidth: 750, maxHeight: 270), + backgroundColor: Theme.of(context).colors.elevationsOnSurfaceNeutralLv0, + header: VoicesAlignTitleHeader( + title: context.l10n.campaignManagement, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.status, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + _CampaignPublishSegmentButton( + value: _campaignPublish, + onChanged: (value) => _campaignPublish = value, + ), + const Spacer(), + Align( + alignment: Alignment.centerRight, + child: VoicesFilledButton( + child: Text(context.l10n.saveButtonText), + onTap: () { + Navigator.of(context).pop(_campaignPublish); + context + .read() + .updateCampaignPublish(_campaignPublish); + }, + ), + ), + ], + ), + ), + ); + } +} + +class _CampaignPublishSegmentButton extends StatefulWidget { + final CampaignPublish value; + final ValueChanged onChanged; + + const _CampaignPublishSegmentButton({ + required this.value, + required this.onChanged, + }); + + @override + State<_CampaignPublishSegmentButton> createState() => _SingleChoiceState(); +} + +class _SingleChoiceState extends State<_CampaignPublishSegmentButton> { + late CampaignPublish _segmentValue; + + @override + void initState() { + super.initState(); + _segmentValue = widget.value; + } + + @override + Widget build(BuildContext context) { + return VoicesSegmentedButton( + showSelectedIcon: false, + segments: >[ + ButtonSegment( + value: CampaignPublish.draft, + label: Text(context.l10n.draft), + ), + ButtonSegment( + value: CampaignPublish.published, + label: Text(context.l10n.published), + ), + ], + selected: {_segmentValue}, + onChanged: (Set newSelection) { + setState(() { + _segmentValue = newSelection.first; + }); + widget.onChanged(_segmentValue); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_sections_list_view.dart b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_sections_list_view.dart new file mode 100644 index 00000000000..697217bcf4a --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/campaign/details/widgets/campaign_sections_list_view.dart @@ -0,0 +1,41 @@ +import 'package:catalyst_voices/pages/campaign/details/widgets/campaign_categories_tile.dart'; +import 'package:catalyst_voices/pages/campaign/details/widgets/campaign_details_tile.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CampaignSectionsListView extends StatelessWidget { + const CampaignSectionsListView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector>( + selector: (state) => state.listItems, + builder: (context, state) { + return ListView.builder( + itemCount: state.length, + itemBuilder: (context, index) { + final item = state[index]; + + switch (item) { + case CampaignDetailsListItem(): + return CampaignDetailsTile( + description: item.description, + startDate: item.startDate, + endDate: item.endDate, + categoriesCount: item.categoriesCount, + proposalsCount: item.proposalsCount, + ); + case CampaignCategoriesListItem(): + return CampaignCategoriesTile( + sections: item.sections, + ); + } + }, + ); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/discovery/current_status_text.dart b/catalyst_voices/apps/voices/lib/pages/discovery/current_status_text.dart index 297e9adb42d..03fb1dcc3b0 100644 --- a/catalyst_voices/apps/voices/lib/pages/discovery/current_status_text.dart +++ b/catalyst_voices/apps/voices/lib/pages/discovery/current_status_text.dart @@ -5,9 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; // Note. This widget will be removed so its not localized class CurrentUserStatusText extends StatelessWidget { - const CurrentUserStatusText({ - super.key, - }); + const CurrentUserStatusText({super.key}); @override Widget build(BuildContext context) { @@ -15,9 +13,9 @@ class CurrentUserStatusText extends StatelessWidget { final sessionBloc = context.watch(); final stateDesc = switch (sessionBloc.state) { - VisitorSessionState() => 'visitor', - GuestSessionState() => 'guest', - ActiveAccountSessionState() => 'user', + VisitorSessionState() => 'Visitor / no key', + GuestSessionState() => 'Guest / locked', + ActiveAccountSessionState() => 'Actor / unlocked', }; return Text( diff --git a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart index 7049932a0dd..b4db9d6f1ec 100644 --- a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart @@ -1,19 +1,69 @@ import 'dart:async'; +import 'package:catalyst_voices/pages/campaign/details/campaign_details_dialog.dart'; import 'package:catalyst_voices/pages/discovery/current_status_text.dart'; import 'package:catalyst_voices/pages/discovery/toggle_state_text.dart'; +import 'package:catalyst_voices/widgets/cards/campaign_stage_card.dart'; +import 'package:catalyst_voices/widgets/cards/proposal_card.dart'; +import 'package:catalyst_voices/widgets/empty_state/empty_state.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; -class DiscoveryPage extends StatelessWidget { - const DiscoveryPage({ - super.key, - }); +class DiscoveryPage extends StatefulWidget { + const DiscoveryPage({super.key}); + + @override + State createState() => _DiscoveryPageState(); +} + +class _DiscoveryPageState extends State { + @override + void initState() { + super.initState(); + unawaited(context.read().load()); + unawaited(context.read().load()); + } @override Widget build(BuildContext context) { - return CustomScrollView( + return const CustomScrollView( + slivers: [ + _Body(), + _Footer(), + ], + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return switch (state) { + VisitorSessionState() => const _GuestVisitorBody(), + GuestSessionState() => const _GuestVisitorBody(), + ActiveAccountSessionState() => const _ActiveAccountBody(), + }; + }, + ); + } +} + +class _GuestVisitorBody extends StatelessWidget { + const _GuestVisitorBody(); + + @override + Widget build(BuildContext context) { + return SliverMainAxisGroup( slivers: [ const SliverToBoxAdapter(child: _SpacesNavigationLocation()), SliverPadding( @@ -31,15 +81,6 @@ class DiscoveryPage extends StatelessWidget { ), ), ), - const SliverFillRemaining( - hasScrollBody: false, - child: Column( - children: [ - Spacer(), - StandardLinksPageFooter(), - ], - ), - ), ], ); } @@ -60,9 +101,7 @@ class _SpacesNavigationLocation extends StatelessWidget { } class _Segment extends StatelessWidget { - const _Segment({ - super.key, - }); + const _Segment({super.key}); @override Widget build(BuildContext context) { @@ -103,3 +142,358 @@ class _Segment extends StatelessWidget { ); } } + +class _ActiveAccountBody extends StatelessWidget { + const _ActiveAccountBody(); + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 32) + .add(const EdgeInsets.only(bottom: 32)), + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + const SizedBox(height: 16), + const _Header(), + const SizedBox(height: 40), + const _Tabs(), + ], + ), + ), + ); + } +} + +class _Header extends StatelessWidget { + const _Header(); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.spaceDiscoveryName, + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 24), + const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _FundInfo()), + Expanded(child: _CampaignStage()), + ], + ), + ], + ); + } +} + +class _FundInfo extends StatelessWidget { + const _FundInfo(); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 680), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.discoverySpaceTitle, + style: Theme.of(context).textTheme.displayMedium, + ), + const SizedBox(height: 16), + Text( + context.l10n.discoverySpaceDescription, + style: Theme.of(context).textTheme.bodyLarge, + ), + const _CampaignDetailsButton(), + ], + ), + ); + } +} + +class _CampaignDetailsButton extends StatelessWidget { + const _CampaignDetailsButton(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.campaign?.id, + builder: (context, campaignId) { + if (campaignId == null) { + return const Offstage(); + } + + return Padding( + padding: const EdgeInsets.only(top: 32), + child: OutlinedButton.icon( + onPressed: () { + unawaited( + CampaignDetailsDialog.show(context, id: campaignId), + ); + }, + label: Text(context.l10n.campaignDetails), + icon: VoicesAssets.icons.arrowsExpand.buildIcon(), + ), + ); + }, + ); + } +} + +class _CampaignStage extends StatelessWidget { + const _CampaignStage(); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topRight, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: BlocBuilder( + builder: (context, state) { + final campaign = state.campaign; + return campaign != null + ? CampaignStageCard(campaign: campaign) + : const Offstage(); + }, + ), + ), + ); + } +} + +class _Tabs extends StatelessWidget { + const _Tabs(); + + @override + Widget build(BuildContext context) { + return const DefaultTabController( + length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _TabBar(), + SizedBox(height: 24), + TabBarStackView( + children: [ + _AllProposals(), + _FavoriteProposals(), + ], + ), + SizedBox(height: 12), + ], + ), + ); + } +} + +class _TabBar extends StatelessWidget { + const _TabBar(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => + state is LoadedProposalsState ? state.proposals.length : 0, + builder: (context, proposalsCount) { + return TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + tabs: [ + Tab( + text: context.l10n.noOfAllProposals(proposalsCount), + ), + Tab( + child: Row( + children: [ + VoicesAssets.icons.starOutlined.buildIcon(), + const SizedBox(width: 8), + Text(context.l10n.favorites), + ], + ), + ), + ], + ); + }, + ); + } +} + +class _AllProposals extends StatelessWidget { + const _AllProposals(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return switch (state) { + LoadingProposalsState() => const _LoadingProposals(), + LoadedProposalsState(:final proposals, :final favoriteProposals) => + proposals.isEmpty + ? const _EmptyProposals() + : _AllProposalsList( + proposals: proposals, + favoriteProposals: favoriteProposals, + ), + }; + }, + ); + } +} + +class _AllProposalsList extends StatelessWidget { + final List proposals; + final List favoriteProposals; + + const _AllProposalsList({ + required this.proposals, + required this.favoriteProposals, + }); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 16, + runSpacing: 16, + children: [ + for (final proposal in proposals) + ProposalCard( + image: _generateImageForProposal(proposal.id), + proposal: proposal, + showStatus: false, + showLastUpdate: false, + showComments: false, + showSegments: false, + isFavorite: favoriteProposals.contains(proposal), + onFavoriteChanged: (isFavorite) async { + if (isFavorite) { + await context + .read() + .onFavoriteProposal(proposal.id); + } else { + await context + .read() + .onUnfavoriteProposal(proposal.id); + } + }, + ), + ], + ); + } +} + +class _FavoriteProposals extends StatelessWidget { + const _FavoriteProposals(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return switch (state) { + LoadingProposalsState() => const _LoadingProposals(), + LoadedProposalsState(:final favoriteProposals) => + favoriteProposals.isEmpty + ? const _EmptyProposals() + : _FavoriteProposalsList( + proposals: favoriteProposals, + ), + }; + }, + ); + } +} + +class _FavoriteProposalsList extends StatelessWidget { + final List proposals; + + const _FavoriteProposalsList({required this.proposals}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 16, + runSpacing: 16, + children: [ + for (final proposal in proposals) + ProposalCard( + image: _generateImageForProposal(proposal.id), + proposal: proposal, + showStatus: false, + showLastUpdate: false, + showComments: false, + showSegments: false, + isFavorite: true, + onFavoriteChanged: (isFavorite) async { + if (isFavorite) { + await context + .read() + .onFavoriteProposal(proposal.id); + } else { + await context + .read() + .onUnfavoriteProposal(proposal.id); + } + }, + ), + ], + ); + } +} + +class _LoadingProposals extends StatelessWidget { + const _LoadingProposals(); + + @override + Widget build(BuildContext context) { + return const Center( + child: Padding( + padding: EdgeInsets.all(64), + child: VoicesCircularProgressIndicator(), + ), + ); + } +} + +class _EmptyProposals extends StatelessWidget { + const _EmptyProposals(); + + @override + Widget build(BuildContext context) { + return Center( + child: EmptyState( + description: context.l10n.discoverySpaceEmptyProposals, + ), + ); + } +} + +class _Footer extends StatelessWidget { + const _Footer(); + + @override + Widget build(BuildContext context) { + return const SliverFillRemaining( + hasScrollBody: false, + child: Column( + children: [ + Spacer(), + StandardLinksPageFooter(), + ], + ), + ); + } +} + +AssetGenImage _generateImageForProposal(String id) { + return id.codeUnits.last.isEven + ? VoicesAssets.images.proposalBackground1 + : VoicesAssets.images.proposalBackground2; +} diff --git a/catalyst_voices/apps/voices/lib/pages/discovery/toggle_state_text.dart b/catalyst_voices/apps/voices/lib/pages/discovery/toggle_state_text.dart index cc3678484a3..f5099724716 100644 --- a/catalyst_voices/apps/voices/lib/pages/discovery/toggle_state_text.dart +++ b/catalyst_voices/apps/voices/lib/pages/discovery/toggle_state_text.dart @@ -1,5 +1,5 @@ import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; -import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -41,7 +41,7 @@ class _ToggleStateTextState extends State { await sessionBloc .switchToDummyAccount() - .then((_) => sessionBloc.unlock(SessionCubit.dummyUnlockFactor)); + .then((_) => sessionBloc.unlock(Account.dummyUnlockFactor)); }; } @@ -55,35 +55,44 @@ class _ToggleStateTextState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Text.rich( - TextSpan( - children: [ - const TextSpan(text: 'Toggle between'), - const TextSpan(text: ', '), - TextSpan( - text: 'No key (visitor)', - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: _tapVisitor, + return Row( + children: [ + GestureDetector( + key: const Key('VisitorShortcut'), + onTap: _tapVisitor.onTap, + child: const Text( + 'No key (visitor)', + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.blue, + ), ), - const TextSpan(text: ', '), - TextSpan( - text: 'Key found(Guest/locked)', - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: _tapGuest, + ), + const Text(', '), + GestureDetector( + key: const Key('GuestShortcut'), + onTap: _tapGuest.onTap, + child: const Text( + 'Key found(Guest/locked)', + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.blue, + ), ), - const TextSpan(text: ', '), - TextSpan( - text: 'Key found (Active user/unlocked)', - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: _tapActiveUser, + ), + const Text(', '), + GestureDetector( + key: const Key('UserShortcut'), + onTap: _tapActiveUser.onTap, + child: const Text( + 'Key found (Active user/unlocked)', + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.blue, + ), ), - ], - ), - style: theme.textTheme.bodyLarge?.copyWith( - color: theme.colors.textOnPrimary, - ), + ), + ], ); } } diff --git a/catalyst_voices/apps/voices/lib/pages/funded_projects/funded_projects_page.dart b/catalyst_voices/apps/voices/lib/pages/funded_projects/funded_projects_page.dart index 0503fbe05da..eaaed8c0415 100644 --- a/catalyst_voices/apps/voices/lib/pages/funded_projects/funded_projects_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/funded_projects/funded_projects_page.dart @@ -2,8 +2,8 @@ import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.da import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; final _proposalDescription = """ @@ -17,7 +17,7 @@ and PRISM, but its potential is only barely exploited. final _proposals = [ FundedProposal( id: 'f14/0', - fund: 'F14', + campaignName: 'F14', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', fundedDate: DateTime.now().minusDays(2), @@ -27,7 +27,7 @@ final _proposals = [ ), FundedProposal( id: 'f14/1', - fund: 'F14', + campaignName: 'F14', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', fundedDate: DateTime.now().minusDays(2), @@ -37,7 +37,7 @@ final _proposals = [ ), FundedProposal( id: 'f14/2', - fund: 'F14', + campaignName: 'F14', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', fundedDate: DateTime.now().minusDays(2), diff --git a/catalyst_voices/apps/voices/lib/pages/login/login.dart b/catalyst_voices/apps/voices/lib/pages/login/login.dart deleted file mode 100644 index d7ad2cd0537..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/login/login.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'login_button.dart'; -export 'login_form.dart'; -export 'login_page.dart'; diff --git a/catalyst_voices/apps/voices/lib/pages/login/login_button.dart b/catalyst_voices/apps/voices/lib/pages/login/login_button.dart deleted file mode 100644 index d1852a0ce99..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/login/login_button.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:formz/formz.dart'; - -final class LoginInButton extends StatelessWidget { - static const loginButtonKey = Key('LoginInButton'); - - const LoginInButton({super.key}); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return BlocBuilder( - buildWhen: (previous, current) => previous.status != current.status, - builder: (context, state) { - return state.status.isInProgress - ? const Center(child: CircularProgressIndicator()) - : FloatingActionButton.extended( - heroTag: UniqueKey(), - label: Text( - l10n.loginButtonText, - ), - onPressed: () { - if (state.isValid) { - context.read().add(const LoginSubmitted()); - } - }, - ); - }, - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/pages/login/login_email_text_filed.dart b/catalyst_voices/apps/voices/lib/pages/login/login_email_text_filed.dart deleted file mode 100644 index 7d8e4d6d96f..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/login/login_email_text_filed.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:catalyst_voices/widgets/widgets.dart'; -import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -final class LoginEmailTextFiled extends StatelessWidget { - static const emailInputKey = Key('EmailInput'); - - const LoginEmailTextFiled({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.email != current.email, - builder: (context, state) { - return VoicesEmailTextField( - key: emailInputKey, - onChanged: (email) => _onEmailChanged(context, email), - onFieldSubmitted: (email) => _onEmailChanged(context, email), - ); - }, - ); - } - - void _onEmailChanged(BuildContext context, String email) { - context.read().add(LoginEmailChanged(email)); - } -} diff --git a/catalyst_voices/apps/voices/lib/pages/login/login_form.dart b/catalyst_voices/apps/voices/lib/pages/login/login_form.dart deleted file mode 100644 index 9c498754028..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/login/login_form.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:catalyst_voices/pages/login/login.dart'; -import 'package:catalyst_voices/pages/login/login_email_text_filed.dart'; -import 'package:catalyst_voices/pages/login/login_password_text_field.dart'; -import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:formz/formz.dart'; - -final class LoginForm extends StatelessWidget { - static const loginFormKey = Key('LoginForm'); - static const loginErrorSnackbarKey = Key('LoginErrorSnackbar'); - - const LoginForm({super.key}); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return Scaffold( - body: BlocListener( - listener: (context, state) { - if (state.status.isFailure) { - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - key: loginErrorSnackbarKey, - content: Text(l10n.loginScreenErrorMessage), - ), - ); - } - }, - child: Center( - child: SizedBox( - height: 460, - width: 480, - child: Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 20), - child: Text( - l10n.loginTitleText, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w700, - ), - ), - ), - const SizedBox(height: 30), - const LoginEmailTextFiled(), - const SizedBox(height: 20), - const LoginPasswordTextField(), - const SizedBox(height: 20), - const SizedBox( - width: double.infinity, - child: LoginInButton(), - ), - ], - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/pages/login/login_page.dart b/catalyst_voices/apps/voices/lib/pages/login/login_page.dart deleted file mode 100644 index f86c97f9925..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/login/login_page.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:catalyst_voices/pages/login/login.dart'; -import 'package:flutter/material.dart'; - -final class LoginPage extends StatelessWidget { - static const loginPage = Key('LoginInPage'); - - const LoginPage({super.key}); - - @override - Widget build(BuildContext context) { - return const LoginForm(); - } -} diff --git a/catalyst_voices/apps/voices/lib/pages/login/login_password_text_field.dart b/catalyst_voices/apps/voices/lib/pages/login/login_password_text_field.dart deleted file mode 100644 index bf4f1b7ecce..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/login/login_password_text_field.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:catalyst_voices/widgets/widgets.dart'; -import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -final class LoginPasswordTextField extends StatelessWidget { - static const passwordInputKey = Key('PasswordInput'); - - const LoginPasswordTextField({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.password != current.password, - builder: (context, state) { - final l10n = context.l10n; - - return VoicesPasswordTextField( - key: passwordInputKey, - onChanged: (password) => _onPasswordChanged(context, password), - decoration: VoicesTextFieldDecoration( - errorMaxLines: 2, - labelText: l10n.passwordLabelText, - hintText: l10n.passwordHintText, - errorText: l10n.passwordErrorText, - ), - ); - }, - ); - } - - void _onPasswordChanged(BuildContext context, String password) { - context.read().add(LoginPasswordChanged(password)); - } -} diff --git a/catalyst_voices/apps/voices/lib/pages/overall_spaces/space/treasury_overview.dart b/catalyst_voices/apps/voices/lib/pages/overall_spaces/space/treasury_overview.dart index 6dd4f804043..bed853afe40 100644 --- a/catalyst_voices/apps/voices/lib/pages/overall_spaces/space/treasury_overview.dart +++ b/catalyst_voices/apps/voices/lib/pages/overall_spaces/space/treasury_overview.dart @@ -9,9 +9,9 @@ class TreasuryOverview extends StatelessWidget { @override Widget build(BuildContext context) { - return const SpaceOverviewContainer( - child: Column( - children: [ + return SpaceOverviewContainer( + child: ListView( + children: const [ SpaceOverviewHeader(Space.treasury), SectionHeader(title: Text('Individual private campaigns')), VoicesNavTile( diff --git a/catalyst_voices/apps/voices/lib/pages/overall_spaces/spaces_overview_list_view.dart b/catalyst_voices/apps/voices/lib/pages/overall_spaces/spaces_overview_list_view.dart index 4dbe45c741e..60b0ef79e58 100644 --- a/catalyst_voices/apps/voices/lib/pages/overall_spaces/spaces_overview_list_view.dart +++ b/catalyst_voices/apps/voices/lib/pages/overall_spaces/spaces_overview_list_view.dart @@ -3,9 +3,12 @@ import 'package:catalyst_voices/pages/overall_spaces/space/funded_projects_overv import 'package:catalyst_voices/pages/overall_spaces/space/treasury_overview.dart'; import 'package:catalyst_voices/pages/overall_spaces/space/voting_overview.dart'; import 'package:catalyst_voices/pages/overall_spaces/space/workspace_overview.dart'; +import 'package:catalyst_voices/widgets/containers/grey_out_container.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SpacesListView extends StatefulWidget { const SpacesListView({ @@ -28,26 +31,44 @@ class _SpacesListViewState extends State { @override Widget build(BuildContext context) { return VoicesScrollbar( + key: const Key('SpacesListView'), controller: _scrollController, alwaysVisible: true, padding: const EdgeInsets.symmetric(horizontal: 6), - child: ListView.separated( - controller: _scrollController, - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.only(right: 16, bottom: 24), - itemBuilder: (context, index) { - final space = Space.values[index]; - return switch (space) { - Space.discovery => DiscoveryOverview(key: ObjectKey(space)), - Space.workspace => WorkspaceOverview(key: ObjectKey(space)), - Space.voting => VotingOverview(key: ObjectKey(space)), - Space.fundedProjects => - FundedProjectsOverview(key: ObjectKey(space)), - Space.treasury => TreasuryOverview(key: ObjectKey(space)), - }; + child: BlocSelector>( + selector: (state) => state.overallSpaces, + builder: (context, state) { + return ListView.separated( + controller: _scrollController, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(right: 16, bottom: 24), + itemBuilder: (context, index) { + final space = state[index]; + + return switch (space) { + Space.discovery => + DiscoveryOverview(key: Key('SpaceOverview.${space.name}')), + Space.workspace => + WorkspaceOverview(key: Key('SpaceOverview.${space.name}')), + Space.voting => GreyOutContainer( + child: VotingOverview( + key: Key('SpaceOverview.${space.name}'), + ), + ), + Space.fundedProjects => GreyOutContainer( + child: FundedProjectsOverview( + key: Key('SpaceOverview.${space.name}'), + ), + ), + Space.treasury => TreasuryOverview( + key: Key('SpaceOverview.${space.name}'), + ), + }; + }, + separatorBuilder: (context, index) => const SizedBox(width: 16), + itemCount: state.length, + ); }, - separatorBuilder: (context, index) => const SizedBox(width: 16), - itemCount: Space.values.length, ), ); } diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder.dart new file mode 100644 index 00000000000..d74034a236c --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder.dart @@ -0,0 +1 @@ +export 'proposal_builder_page.dart'; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_body.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_body.dart similarity index 79% rename from catalyst_voices/apps/voices/lib/pages/workspace/workspace_body.dart rename to catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_body.dart index 5b8b811a9fe..54b0ba5b69a 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_body.dart +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_body.dart @@ -1,14 +1,14 @@ -import 'package:catalyst_voices/pages/workspace/workspace_rich_text_step.dart'; +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_rich_text_step.dart'; import 'package:catalyst_voices/widgets/navigation/sections_list_view.dart'; import 'package:catalyst_voices/widgets/navigation/sections_list_view_builder.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -class WorkspaceBody extends StatelessWidget { +class ProposalBuilderBody extends StatelessWidget { final ItemScrollController itemScrollController; - const WorkspaceBody({ + const ProposalBuilderBody({ super.key, required this.itemScrollController, }); @@ -23,7 +23,7 @@ class WorkspaceBody extends StatelessWidget { stepBuilder: (context, step) { switch (step) { case RichTextStep(): - return WorkspaceRichTextStep(step: step); + return ProposalBuilderRichTextStep(step: step); } }, ); diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_guidance_view.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_guidance_view.dart similarity index 82% rename from catalyst_voices/apps/voices/lib/pages/workspace/workspace_guidance_view.dart rename to catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_guidance_view.dart index fd580e3ac84..545265dd89e 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_guidance_view.dart +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_guidance_view.dart @@ -2,18 +2,22 @@ import 'package:catalyst_voices/common/ext/guidance_ext.dart'; import 'package:catalyst_voices/widgets/cards/guidance_card.dart'; import 'package:catalyst_voices/widgets/dropdown/voices_dropdown.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; -class GuidanceView extends StatefulWidget { +class ProposalBuilderGuidanceView extends StatefulWidget { final List guidances; - const GuidanceView(this.guidances, {super.key}); + + const ProposalBuilderGuidanceView(this.guidances, {super.key}); @override - State createState() => _GuidanceViewState(); + State createState() { + return _ProposalBuilderGuidanceViewState(); + } } -class _GuidanceViewState extends State { +class _ProposalBuilderGuidanceViewState + extends State { final List filteredGuidances = []; GuidanceType? selectedType; @@ -27,7 +31,7 @@ class _GuidanceViewState extends State { } @override - void didUpdateWidget(GuidanceView oldWidget) { + void didUpdateWidget(ProposalBuilderGuidanceView oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.guidances != widget.guidances) { filteredGuidances diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_navigation_panel.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_navigation_panel.dart similarity index 84% rename from catalyst_voices/apps/voices/lib/pages/workspace/workspace_navigation_panel.dart rename to catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_navigation_panel.dart index a8ffc7fa7f0..052e8090421 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_navigation_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_navigation_panel.dart @@ -2,8 +2,8 @@ import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; -class WorkspaceNavigationPanel extends StatelessWidget { - const WorkspaceNavigationPanel({super.key}); +class ProposalBuilderNavigationPanel extends StatelessWidget { + const ProposalBuilderNavigationPanel({super.key}); @override Widget build(BuildContext context) { 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 new file mode 100644 index 00000000000..9ffa7252673 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart @@ -0,0 +1,108 @@ +import 'dart:async'; + +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_body.dart'; +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_navigation_panel.dart'; +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_setup_panel.dart'; +import 'package:catalyst_voices/widgets/containers/space_scaffold.dart'; +import 'package:catalyst_voices/widgets/navigation/sections_controller.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +class ProposalBuilderPage extends StatefulWidget { + final String proposalId; + + const ProposalBuilderPage({ + super.key, + required this.proposalId, + }); + + @override + State createState() => _ProposalBuilderPageState(); +} + +class _ProposalBuilderPageState extends State { + late final SectionsController _sectionsController; + late final ItemScrollController _bodyItemScrollController; + + SectionStepId? _activeStepId; + StreamSubscription>? _sectionsSub; + + @override + void initState() { + super.initState(); + + final bloc = context.read(); + + _sectionsController = SectionsController(); + _bodyItemScrollController = ItemScrollController(); + + _sectionsController + ..addListener(_handleSectionsControllerChange) + ..attachItemsScrollController(_bodyItemScrollController); + + _sectionsSub = bloc.stream + .map((event) => event.sections) + .distinct(listEquals) + .listen(_updateSections); + + bloc.add(LoadProposalEvent(id: widget.proposalId)); + } + + @override + void didUpdateWidget(covariant ProposalBuilderPage oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.proposalId != oldWidget.proposalId) { + final event = LoadProposalEvent(id: widget.proposalId); + context.read().add(event); + } + } + + @override + void dispose() { + unawaited(_sectionsSub?.cancel()); + _sectionsSub = null; + + _sectionsController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SectionsControllerScope( + controller: _sectionsController, + child: SpaceScaffold( + left: const ProposalBuilderNavigationPanel(), + body: ProposalBuilderBody( + itemScrollController: _bodyItemScrollController, + ), + right: const ProposalBuilderSetupPanel(), + ), + ); + } + + void _updateSections(List
data) { + final state = _sectionsController.value; + + final newState = state.sections.isEmpty + ? SectionsControllerState.initial(sections: data) + : state.copyWith(sections: data); + + _sectionsController.value = newState; + } + + void _handleSectionsControllerChange() { + final activeStepId = _sectionsController.value.activeStepId; + + if (_activeStepId != activeStepId) { + _activeStepId = activeStepId; + + final event = ActiveStepChangedEvent(activeStepId); + context.read().add(event); + } + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_rich_text_step.dart similarity index 69% rename from catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart rename to catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_rich_text_step.dart index 9d5814abbc7..d1b6eb9c951 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_rich_text_step.dart +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_rich_text_step.dart @@ -1,24 +1,31 @@ +import 'package:catalyst_voices/common/codecs/markdown_codec.dart'; import 'package:catalyst_voices/widgets/navigation/section_step_state_builder.dart'; import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_quill/flutter_quill.dart'; -class WorkspaceRichTextStep extends StatefulWidget { +class ProposalBuilderRichTextStep extends StatefulWidget { final RichTextStep step; - const WorkspaceRichTextStep({ + const ProposalBuilderRichTextStep({ super.key, required this.step, }); @override - State createState() => _WorkspaceRichTextStepState(); + State createState() { + return _ProposalBuilderRichTextStepState(); + } } -class _WorkspaceRichTextStepState extends State { +class _ProposalBuilderRichTextStepState + extends State { late final VoicesRichTextController _controller; late final VoicesRichTextEditModeController _editModeController; @@ -26,7 +33,10 @@ class _WorkspaceRichTextStepState extends State { void initState() { super.initState(); - final document = Document.fromJson(widget.step.data.value); + final markdownString = widget.step.initialData ?? const MarkdownData(''); + final delta = markdown.encode(markdownString); + + final document = delta.isNotEmpty ? Document.fromDelta(delta) : Document(); final selectionOffset = document.length == 0 ? 0 : document.length - 1; _controller = VoicesRichTextController( @@ -55,16 +65,31 @@ class _WorkspaceRichTextStepState extends State { ); }, child: VoicesRichText( - title: widget.step.localizedDesc(context), + title: widget.step.description ?? widget.step.name, controller: _controller, editModeController: _editModeController, charsLimit: widget.step.charsLimit, canEditDocumentGetter: _canEditDocument, onEditBlocked: _showEditBlockedRationale, + onSaved: _saveDocument, ), ); } + void _saveDocument(Document document) { + final delta = document.toDelta(); + final markdownString = markdown.decode(delta); + + final sectionStepId = widget.step.sectionStepId; + + final event = UpdateStepAnswerEvent( + id: sectionStepId, + data: markdownString.data.isNotEmpty ? markdownString : null, + ); + + context.read().add(event); + } + bool _canEditDocument(Document document) { final sectionsController = SectionsControllerScope.of(context); diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_setup_panel.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_setup_panel.dart new file mode 100644 index 00000000000..7589a0134d7 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_setup_panel.dart @@ -0,0 +1,63 @@ +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_guidance_view.dart'; +import 'package:catalyst_voices/widgets/cards/comment_card.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ProposalBuilderSetupPanel extends StatelessWidget { + const ProposalBuilderSetupPanel({super.key}); + + @override + Widget build(BuildContext context) { + return SpaceSidePanel( + isLeft: false, + name: context.l10n.workspaceProposalSetup, + onCollapseTap: () {}, + tabs: [ + SpaceSidePanelTab( + name: 'Guidance', + body: const _GuidanceSelector(), + ), + SpaceSidePanelTab( + name: 'Comments', + body: CommentCard( + comment: Comment( + text: 'Lacks clarity on key objectives and measurable outcomes.', + date: DateTime.now(), + userName: 'Community Member', + ), + ), + ), + //No actions for now + // SpaceSidePanelTab( + // name: 'Actions', + // body: const Offstage(), + // ), + ], + ); + } +} + +class _GuidanceSelector extends StatelessWidget { + const _GuidanceSelector(); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.guidance, + builder: (context, state) { + if (state.isNoneSelected) { + return Text(context.l10n.selectASection); + } else if (state.showEmptyState) { + return Text(context.l10n.noGuidanceForThisSection); + } else { + return ProposalBuilderGuidanceView(state.guidances); + } + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/registration/get_started/get_started_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/get_started/get_started_panel.dart index 04cef3fd3eb..cd5a83ad0c6 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/get_started/get_started_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/get_started/get_started_panel.dart @@ -20,6 +20,7 @@ class GetStartedPanel extends StatelessWidget { children: [ const SizedBox(height: 24), RegistrationStageMessage( + key: const Key('GetStartedMessage'), title: Text(context.l10n.accountCreationGetStartedTitle), subtitle: Text(context.l10n.accountCreationGetStatedDesc), spacing: 12, @@ -27,6 +28,7 @@ class GetStartedPanel extends StatelessWidget { ), const SizedBox(height: 32), Text( + key: const Key('GetStartedQuestion'), context.l10n.accountCreationGetStatedWhatNext, style: theme.textTheme.titleSmall?.copyWith( color: theme.colors.textOnPrimaryLevel0, @@ -39,7 +41,7 @@ class GetStartedPanel extends StatelessWidget { children: CreateAccountType.values .map((type) { return RegistrationTile( - key: ValueKey(type), + key: Key(type.toString()), icon: type._icon, title: type._getTitle(context.l10n), subtitle: type._getSubtitle(context.l10n), diff --git a/catalyst_voices/apps/voices/lib/pages/registration/registration_details_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/registration_details_panel.dart index 6fe0c6023bf..d7a29b8a769 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/registration_details_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/registration_details_panel.dart @@ -16,6 +16,7 @@ class RegistrationDetailsPanel extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( + key: const Key('RegistrationDetailsPanel'), selector: (state) => state.step, builder: (context, state) { return switch (state) { diff --git a/catalyst_voices/apps/voices/lib/pages/registration/registration_info_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/registration_info_panel.dart index 5d320a93ef9..8c2928cb06d 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/registration_info_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/registration_info_panel.dart @@ -50,6 +50,7 @@ class RegistrationInfoPanel extends StatelessWidget { ); return InformationPanel( + key: const Key('RegistrationInfoPanel'), title: headerStrings.title, subtitle: headerStrings.subtitle, body: headerStrings.body, diff --git a/catalyst_voices/apps/voices/lib/pages/registration/widgets/information_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/widgets/information_panel.dart index 9f4104b4333..f62b36b524a 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/widgets/information_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/widgets/information_panel.dart @@ -30,7 +30,10 @@ class InformationPanel extends StatelessWidget { body: body, ), const SizedBox(height: 12), - Expanded(child: Center(child: picture)), + Expanded( + key: const Key('PictureContainer'), + child: Center(child: picture), + ), const SizedBox(height: 12), _Footer( progress: progress, @@ -64,16 +67,19 @@ class _Header extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( + key: const Key('HeaderTitle'), title, style: theme.textTheme.titleLarge?.copyWith(color: textColor), ), if (subtitle != null) Text( + key: const Key('HeaderSubtitle'), subtitle, style: theme.textTheme.titleMedium?.copyWith(color: textColor), ), if (body != null) Text( + key: const Key('HeaderBody'), body, style: theme.textTheme.bodyMedium?.copyWith(color: textColor), ), diff --git a/catalyst_voices/apps/voices/lib/pages/registration/widgets/registration_stage_message.dart b/catalyst_voices/apps/voices/lib/pages/registration/widgets/registration_stage_message.dart index 52cc5f6e92f..ccdf1d2fe45 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/widgets/registration_stage_message.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/widgets/registration_stage_message.dart @@ -25,11 +25,13 @@ class RegistrationStageMessage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DefaultTextStyle( + key: const Key('RegistrationDetailsTitle'), style: theme.textTheme.titleMedium!.copyWith(color: textColor), child: title, ), SizedBox(height: spacing), DefaultTextStyle( + key: const Key('RegistrationDetailsBody'), style: theme.textTheme.bodyMedium!.copyWith(color: textColor), child: subtitle, ), diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/discovery_menu.dart b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/discovery_menu.dart index fa588ba1df4..edfe293e62b 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/discovery_menu.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/discovery_menu.dart @@ -11,10 +11,12 @@ class DiscoveryDrawerMenu extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + key: const ValueKey('DiscoveryDrawerMenuItems'), mainAxisSize: MainAxisSize.min, children: [ const SpaceHeader(Space.discovery), VoicesNavTile( + key: const ValueKey('DiscoveryDashboardTile'), leading: VoicesAssets.icons.home.buildIcon(), name: 'Discovery Dashboard', backgroundColor: Space.discovery.backgroundColor(context), @@ -22,17 +24,20 @@ class DiscoveryDrawerMenu extends StatelessWidget { ), const VoicesDivider(), VoicesNavTile( + key: const ValueKey('RolesTile'), leading: VoicesAssets.icons.user.buildIcon(), name: 'Catalyst Roles', onTap: () => Scaffold.of(context).closeDrawer(), ), VoicesNavTile( + key: const ValueKey('FeedbackTile'), leading: VoicesAssets.icons.annotation.buildIcon(), name: 'Feedback', onTap: () => Scaffold.of(context).closeDrawer(), ), const VoicesDivider(), VoicesNavTile( + key: const ValueKey('DocumentationTile'), leading: VoicesAssets.icons.arrowRight.buildIcon(), name: 'Catalyst Gitbook documentation', onTap: () => Scaffold.of(context).closeDrawer(), diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/guest_menu.dart b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/guest_menu.dart index bc2df6ec182..f176fa0e5a5 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/guest_menu.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/guest_menu.dart @@ -15,6 +15,7 @@ class GuestMenu extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + key: const ValueKey('GuestMenuItems'), mainAxisSize: MainAxisSize.min, children: [ VoicesNavTile( diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/individual_private_campaigns.dart b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/individual_private_campaigns.dart index 37b49509d90..c75dec37d76 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/individual_private_campaigns.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/individual_private_campaigns.dart @@ -13,6 +13,7 @@ class IndividualPrivateCampaigns extends StatelessWidget { children: [ const SpaceHeader(Space.treasury), const SectionHeader( + key: ValueKey('Header.treasury'), leading: SizedBox(width: 12), title: Text('Individual private campaigns'), ), diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/my_private_proposals.dart b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/my_private_proposals.dart index 39813bf2fa3..f29a148d347 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/my_private_proposals.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/my_private_proposals.dart @@ -8,32 +8,16 @@ class MyPrivateProposals extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( + return const Column( mainAxisSize: MainAxisSize.min, children: [ - const SpaceHeader(Space.workspace), - const SectionHeader( + SpaceHeader(Space.workspace), + SectionHeader( + key: ValueKey('Header.workspace'), leading: SizedBox(width: 12), title: Text('My private proposals (3/5)'), ), - VoicesNavTile( - name: 'My first proposal', - status: ProposalStatus.draft, - trailing: const MoreOptionsButton(), - onTap: () => Scaffold.of(context).closeDrawer(), - ), - VoicesNavTile( - name: 'My second proposal', - status: ProposalStatus.inProgress, - trailing: const MoreOptionsButton(), - onTap: () => Scaffold.of(context).closeDrawer(), - ), - VoicesNavTile( - name: 'My third proposal', - status: ProposalStatus.inProgress, - trailing: const MoreOptionsButton(), - onTap: () => Scaffold.of(context).closeDrawer(), - ), + // TODO(damian-molinski): watch workspace bloc ], ); } diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/space_header.dart b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/space_header.dart index 724c6d72db3..2814d0808e1 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/space_header.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/space_header.dart @@ -19,6 +19,7 @@ class SpaceHeader extends StatelessWidget { final theme = Theme.of(context); return Container( + key: Key('SpaceHeader.${data.name}'), padding: const EdgeInsets.symmetric(vertical: 14) .add(const EdgeInsets.only(left: 16)), child: Row( diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/spaces_drawer.dart b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/spaces_drawer.dart index 719c6a1ff42..785038549df 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/spaces_drawer.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/spaces_drawer.dart @@ -64,25 +64,29 @@ class _SpacesDrawerState extends State { @override Widget build(BuildContext context) { return VoicesDrawer( - bottom: VoicesDrawerSpaceChooser( - currentSpace: widget.space, - onChanged: (space) => space.go(context), - onOverallTap: () { - Scaffold.of(context).closeDrawer(); - unawaited(const OverallSpacesRoute().push(context)); - }, - builder: (context, value, child) { - final shortcutActivator = widget.spacesShortcutsActivators[value]; + bottom: !widget.isUnlocked + ? null + : VoicesDrawerSpaceChooser( + key: const ValueKey('DrawerSpaceChooser'), + currentSpace: widget.space, + onChanged: (space) => space.go(context), + onOverallTap: () { + Scaffold.of(context).closeDrawer(); + unawaited(const OverallSpacesRoute().push(context)); + }, + builder: (context, value, child) { + final shortcutActivator = + widget.spacesShortcutsActivators[value]; - return VoicesPlainTooltip( - message: value.localizedName(context.l10n), - trailing: shortcutActivator != null - ? ShortcutActivatorView(activator: shortcutActivator) - : null, - child: child!, - ); - }, - ), + return VoicesPlainTooltip( + message: value.localizedName(context.l10n), + trailing: shortcutActivator != null + ? ShortcutActivatorView(activator: shortcutActivator) + : null, + child: child!, + ); + }, + ), child: Column( children: [ const SizedBox(height: 12), diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/voting_rounds.dart b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/voting_rounds.dart index 598cedd01ab..6455b514681 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/drawer/voting_rounds.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/drawer/voting_rounds.dart @@ -14,6 +14,7 @@ class VotingRounds extends StatelessWidget { children: [ const SpaceHeader(Space.voting), const SectionHeader( + key: ValueKey('Header.voting'), leading: SizedBox(width: 12), title: Text('Active funding rounds'), ), diff --git a/catalyst_voices/apps/voices/lib/pages/spaces/spaces_shell_page.dart b/catalyst_voices/apps/voices/lib/pages/spaces/spaces_shell_page.dart index 54bda4db03a..86e21b05b60 100644 --- a/catalyst_voices/apps/voices/lib/pages/spaces/spaces_shell_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/spaces/spaces_shell_page.dart @@ -1,4 +1,8 @@ +import 'dart:async'; + import 'package:catalyst_voices/common/ext/ext.dart'; +import 'package:catalyst_voices/pages/campaign/admin_tools/campaign_admin_tools_dialog.dart'; +import 'package:catalyst_voices/pages/campaign/details/widgets/campaign_management.dart'; import 'package:catalyst_voices/pages/spaces/appbar/spaces_theme_mode_switch.dart'; import 'package:catalyst_voices/pages/spaces/drawer/spaces_drawer.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; @@ -9,9 +13,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SpacesShellPage extends StatefulWidget { - final Space space; - final Widget child; - static final Map _spacesShortcutsActivators = { Space.discovery: LogicalKeySet( LogicalKeyboardKey.control, @@ -35,6 +36,9 @@ class SpacesShellPage extends StatefulWidget { ), }; + final Space space; + final Widget child; + const SpacesShellPage({ super.key, required this.space, @@ -46,37 +50,167 @@ class SpacesShellPage extends StatefulWidget { } class _SpacesShellPageState extends State { + final GlobalKey _adminToolsKey = GlobalKey(debugLabel: 'admin_tools'); + final StreamController _selectedSpaceSC = StreamController.broadcast(); + OverlayEntry? _adminToolsOverlay; + + @override + void didUpdateWidget(SpacesShellPage oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.space != widget.space) { + _selectedSpaceSC.add(widget.space); + } + } + + @override + void dispose() { + _removeAdminToolsOverlay(); + unawaited(_selectedSpaceSC.close()); + super.dispose(); + } + @override Widget build(BuildContext context) { - final sessionBloc = context.watch(); - final isVisitor = sessionBloc.state is VisitorSessionState; - final isUnlocked = sessionBloc.state is ActiveAccountSessionState; - - return CallbackShortcuts( - bindings: { - for (final entry in SpacesShellPage._spacesShortcutsActivators.entries) - entry.value: () => entry.key.go(context), + return BlocListener( + listenWhen: (previous, current) => previous.enabled != current.enabled, + listener: (context, state) { + if (state.enabled) { + _insertAdminToolsOverlay(); + } else { + _removeAdminToolsOverlay(); + } }, - child: Scaffold( - appBar: VoicesAppBar( - leading: isVisitor ? null : const DrawerToggleButton(), - automaticallyImplyLeading: false, - actions: const [ - SpacesThemeModeSwitch(), - SessionActionHeader(), - SessionStateHeader(), - ], - ), - drawer: isVisitor - ? null - : SpacesDrawer( - space: widget.space, - spacesShortcutsActivators: - SpacesShellPage._spacesShortcutsActivators, - isUnlocked: isUnlocked, + child: _Shortcuts( + onToggleAdminTools: _toggleAdminTools, + child: BlocSelector( + selector: (state) => ( + isUnlocked: state is ActiveAccountSessionState, + isVisitor: state is VisitorSessionState + ), + builder: (context, state) { + return Scaffold( + appBar: VoicesAppBar( + leading: state.isVisitor ? null : const DrawerToggleButton(), + automaticallyImplyLeading: false, + actions: _getActions(widget.space), ), - body: widget.child, + drawer: state.isVisitor + ? null + : SpacesDrawer( + space: widget.space, + spacesShortcutsActivators: + SpacesShellPage._spacesShortcutsActivators, + isUnlocked: state.isUnlocked, + ), + body: widget.child, + ); + }, + ), ), ); } + + List _getActions(Space space) { + if (space == Space.treasury) { + return [ + const CampaignManagement(), + const SpacesThemeModeSwitch(), + ]; + } else { + return [ + const SpacesThemeModeSwitch(), + const SessionActionHeader(), + const SessionStateHeader(), + ]; + } + } + + void _toggleAdminTools() { + final cubit = context.read(); + if (cubit.state.enabled) { + cubit.disable(); + } else { + cubit.enable(); + } + } + + void _insertAdminToolsOverlay() { + if (_adminToolsOverlay != null) { + // already shown + return; + } + + final overlayEntry = _createAdminToolsOverlay(); + Overlay.of(context, rootOverlay: true).insert(overlayEntry); + _adminToolsOverlay = overlayEntry; + } + + void _removeAdminToolsOverlay() { + _adminToolsOverlay?.remove(); + _adminToolsOverlay = null; + } + + OverlayEntry _createAdminToolsOverlay() { + return OverlayEntry( + builder: (BuildContext context) { + return StreamBuilder( + // Passing it as a stream, not as a value because when the page + // rebuilds the overlay entry is not rebuilt. + stream: _watchSpace, + builder: (context, snapshot) { + final space = snapshot.data; + if (space == null) { + return const Offstage(); + } + + return DraggableCampaignAdminToolsDialog( + dialogKey: _adminToolsKey, + selectedSpace: space, + onSpaceSelected: (space) => space.go(context), + ); + }, + ); + }, + ); + } + + Stream get _watchSpace async* { + yield widget.space; + yield* _selectedSpaceSC.stream; + } +} + +class _Shortcuts extends StatelessWidget { + final VoidCallback onToggleAdminTools; + final Widget child; + + const _Shortcuts({ + required this.onToggleAdminTools, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return BlocSelector>( + selector: (state) { + return switch (state) { + ActiveAccountSessionState(:final spacesShortcuts) => spacesShortcuts, + _ => {}, + }; + }, + builder: (context, shortcuts) { + return CallbackShortcuts( + bindings: { + for (final entry in shortcuts.entries) + entry.value: () => entry.key.go(context), + CampaignAdminToolsDialog.shortcut: onToggleAdminTools, + }, + child: child, + ); + }, + ); + } } diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_categories_step.dart b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_categories_step.dart new file mode 100644 index 00000000000..0c146272c0e --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_categories_step.dart @@ -0,0 +1,34 @@ +import 'package:catalyst_voices/widgets/navigation/section_step_state_builder.dart'; +import 'package:catalyst_voices/widgets/widgets.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'; + +class TreasuryCampaignCategoriesStep extends StatelessWidget { + final TreasurySectionStep step; + + const TreasuryCampaignCategoriesStep({ + super.key, + required this.step, + }); + + @override + Widget build(BuildContext context) { + return SectionStepStateBuilder( + id: step.sectionStepId, + builder: (context, value, child) { + return WorkspaceTextTileContainer( + name: step.localizedDesc(context), + isSelected: value.isSelected, + headerActions: [ + VoicesTextButton( + onTap: step.isEditable ? () {} : null, + child: Text(context.l10n.stepEdit), + ), + ], + content: step.localizedDesc(context), + ); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_dummy_topic_step.dart b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_details_step.dart similarity index 83% rename from catalyst_voices/apps/voices/lib/pages/treasury/treasury_dummy_topic_step.dart rename to catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_details_step.dart index ad89a55c21e..0ad0967ff5f 100644 --- a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_dummy_topic_step.dart +++ b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_details_step.dart @@ -4,10 +4,10 @@ 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'; -class TreasuryDummyTopicStep extends StatelessWidget { - final DummyTopicStep step; +class TreasuryCampaignDetailsStep extends StatelessWidget { + final TreasurySectionStep step; - const TreasuryDummyTopicStep({ + const TreasuryCampaignDetailsStep({ super.key, required this.step, }); @@ -18,7 +18,7 @@ class TreasuryDummyTopicStep extends StatelessWidget { id: step.sectionStepId, builder: (context, value, child) { return WorkspaceTextTileContainer( - name: step.localizedName(context), + name: step.localizedDesc(context), isSelected: value.isSelected, headerActions: [ VoicesTextButton( diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_stages_edit_step.dart b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_stages_edit_step.dart new file mode 100644 index 00000000000..28bedc8eaab --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_stages_edit_step.dart @@ -0,0 +1,162 @@ +import 'package:catalyst_voices/pages/treasury/steps/treasury_campaign_widgets.dart'; +import 'package:catalyst_voices/widgets/navigation/section_step_state_builder.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class TreasuryCampaignStagesEditStep extends StatelessWidget { + final TreasurySectionStep step; + + const TreasuryCampaignStagesEditStep({ + super.key, + required this.step, + }); + + @override + Widget build(BuildContext context) { + return SectionStepStateBuilder( + id: step.sectionStepId, + builder: (context, value, child) { + return WorkspaceTileContainer( + isSelected: value.isSelected, + content: Column( + children: [ + TreasuryCampaignStepHeader(step: step), + const SizedBox(height: 12), + const TreasuryCampaignTimezone(), + const SizedBox(height: 24), + _DateRange(step: step), + ], + ), + ); + }, + ); + } +} + +class _DateRange extends StatefulWidget { + final TreasurySectionStep step; + + const _DateRange({required this.step}); + + @override + State<_DateRange> createState() => _DateRangeState(); +} + +class _DateRangeState extends State<_DateRange> { + late final VoicesDateTimeFieldController _startDateController; + late final VoicesDateTimeFieldController _endDateController; + + @override + void initState() { + super.initState(); + + final cubit = context.read(); + _startDateController = VoicesDateTimeFieldController(cubit.state.startDate); + _endDateController = VoicesDateTimeFieldController(cubit.state.endDate); + } + + @override + void dispose() { + _startDateController.dispose(); + _endDateController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SegmentHeader( + name: context.l10n.campaignDates, + padding: EdgeInsets.zero, + ), + _EditDate( + controller: _startDateController, + label: context.l10n.campaignStart, + onEdit: _onEditStartDate, + ), + const SizedBox(height: 32), + _EditDate( + controller: _endDateController, + label: context.l10n.campaignEnd, + onEdit: _onEditEndDate, + ), + const SizedBox(height: 36), + Align( + alignment: Alignment.topRight, + child: VoicesFilledButton( + child: Text(context.l10n.saveButtonText), + onTap: () { + context.read().updateCampaignDates( + startDate: _startDateController.value, + endDate: _endDateController.value, + ); + + SectionsControllerScope.of(context).editStep( + widget.step.sectionStepId, + enabled: false, + ); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ); + } + + // ignore: use_setters_to_change_properties + void _onEditStartDate(DateTime? startDate) { + _startDateController.value = startDate; + } + + // ignore: use_setters_to_change_properties + void _onEditEndDate(DateTime? endDate) { + _endDateController.value = endDate; + } +} + +class _EditDate extends StatelessWidget { + final String label; + final VoicesDateTimeFieldController controller; + final ValueChanged onEdit; + + const _EditDate({ + required this.controller, + required this.label, + required this.onEdit, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + Flexible( + child: SizedBox( + width: 220, + child: Text( + label, + style: theme.textTheme.titleSmall, + ), + ), + ), + Flexible( + child: VoicesDateTimeField( + controller: controller, + onChanged: onEdit, + onFieldSubmitted: onEdit, + ), + ), + ], + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_stages_view_step.dart b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_stages_view_step.dart new file mode 100644 index 00000000000..800c34a902e --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_stages_view_step.dart @@ -0,0 +1,151 @@ +import 'package:catalyst_voices/common/formatters/date_formatter.dart'; +import 'package:catalyst_voices/pages/treasury/steps/treasury_campaign_widgets.dart'; +import 'package:catalyst_voices/widgets/navigation/section_step_state_builder.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class TreasuryCampaignStagesViewStep extends StatelessWidget { + final TreasurySectionStep step; + + const TreasuryCampaignStagesViewStep({ + super.key, + required this.step, + }); + + @override + Widget build(BuildContext context) { + return SectionStepStateBuilder( + id: step.sectionStepId, + builder: (context, value, child) { + return WorkspaceTileContainer( + isSelected: value.isSelected, + content: Column( + children: [ + TreasuryCampaignStepHeader(step: step), + const SizedBox(height: 12), + const TreasuryCampaignTimezone(), + const SizedBox(height: 24), + const _DateRange(), + ], + ), + ); + }, + ); + } +} + +class _DateRange extends StatelessWidget { + const _DateRange(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SegmentHeader( + name: context.l10n.startAndEndDates, + padding: EdgeInsets.zero, + ), + const SizedBox(height: 12), + const Row( + children: [ + _StartDate(), + SizedBox(width: 24), + _EndDate(), + ], + ), + const SizedBox(height: 36), + ], + ), + ); + } +} + +class _StartDate extends StatelessWidget { + const _StartDate(); + + @override + Widget build(BuildContext context) { + return Expanded( + child: + BlocSelector( + selector: (state) => state.startDate, + builder: (context, date) { + return _Date( + label: context.l10n.campaignStart, + dateTime: date, + ); + }, + ), + ); + } +} + +class _EndDate extends StatelessWidget { + const _EndDate(); + + @override + Widget build(BuildContext context) { + return Expanded( + child: + BlocSelector( + selector: (state) => state.endDate, + builder: (context, date) { + return _Date( + label: context.l10n.campaignEnd, + dateTime: date, + ); + }, + ), + ); + } +} + +class _Date extends StatelessWidget { + final String label; + final DateTime? dateTime; + + const _Date({ + required this.label, + required this.dateTime, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + label, + style: theme.textTheme.titleSmall, + ), + const SizedBox(height: 8), + Text( + _formatDate(context, dateTime), + style: theme.textTheme.bodyLarge!.copyWith( + color: dateTime == null + ? theme.colors.textDisabled + : theme.colors.onPrimaryContainer, + ), + ), + ], + ); + } + + String _formatDate(BuildContext context, DateTime? date) { + if (date == null) { + return context.l10n.noDateTimeSelected; + } + + return DateFormatter.formatFullDateTime(date, timeOnNewline: true); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_widgets.dart b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_widgets.dart new file mode 100644 index 00000000000..0654ccf0794 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_campaign_widgets.dart @@ -0,0 +1,106 @@ +import 'package:catalyst_voices/common/formatters/date_formatter.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; +import 'package:catalyst_voices/widgets/headers/segment_header.dart'; +import 'package:catalyst_voices/widgets/navigation/sections_controller.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_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; + +class TreasuryCampaignStepHeader extends StatelessWidget { + final TreasurySectionStep step; + + const TreasuryCampaignStepHeader({ + super.key, + required this.step, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: SegmentHeader( + name: step.localizedDesc(context), + actions: [ + ValueListenableBuilder( + valueListenable: SectionsControllerScope.of(context), + builder: (context, sectionsState, child) { + final isEditing = sectionsState.isEditing(step.sectionStepId); + + return VoicesTextButton( + onTap: step.isEditable + ? () => _onToggleEditing(context, !isEditing) + : null, + child: Text( + isEditing + ? context.l10n.cancelButtonText + : context.l10n.stepEdit, + ), + ); + }, + ), + ], + ), + ); + } + + void _onToggleEditing(BuildContext context, bool isEditing) { + SectionsControllerScope.of(context).editStep( + step.sectionStepId, + enabled: isEditing, + ); + } +} + +class TreasuryCampaignTimezone extends StatelessWidget { + const TreasuryCampaignTimezone({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.setupCampaignStagesTimezone, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 12), + const _TimezoneValue(), + ], + ), + ); + } +} + +class _TimezoneValue extends StatelessWidget { + const _TimezoneValue(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + VoicesAssets.icons.globeAlt.buildIcon(size: 20), + const SizedBox(width: 8), + Text( + _formatTimezone(), + style: Theme.of(context).textTheme.labelLarge, + ), + ], + ), + ); + } + + String _formatTimezone() { + return DateFormatter.formatTimezone(DateTimeExt.now()); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_proposal_template_step.dart b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_proposal_template_step.dart new file mode 100644 index 00000000000..0811bf9cda5 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/treasury/steps/treasury_proposal_template_step.dart @@ -0,0 +1,34 @@ +import 'package:catalyst_voices/widgets/navigation/section_step_state_builder.dart'; +import 'package:catalyst_voices/widgets/widgets.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'; + +class TreasuryProposalTemplateStep extends StatelessWidget { + final TreasurySectionStep step; + + const TreasuryProposalTemplateStep({ + super.key, + required this.step, + }); + + @override + Widget build(BuildContext context) { + return SectionStepStateBuilder( + id: step.sectionStepId, + builder: (context, value, child) { + return WorkspaceTextTileContainer( + name: step.localizedDesc(context), + isSelected: value.isSelected, + headerActions: [ + VoicesTextButton( + onTap: step.isEditable ? () {} : null, + child: Text(context.l10n.stepEdit), + ), + ], + content: step.localizedDesc(context), + ); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_body.dart b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_body.dart index 700afefca60..11ba10f83b8 100644 --- a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_body.dart +++ b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_body.dart @@ -1,4 +1,8 @@ -import 'package:catalyst_voices/pages/treasury/treasury_dummy_topic_step.dart'; +import 'package:catalyst_voices/pages/treasury/steps/treasury_campaign_categories_step.dart'; +import 'package:catalyst_voices/pages/treasury/steps/treasury_campaign_details_step.dart'; +import 'package:catalyst_voices/pages/treasury/steps/treasury_campaign_stages_edit_step.dart'; +import 'package:catalyst_voices/pages/treasury/steps/treasury_campaign_stages_view_step.dart'; +import 'package:catalyst_voices/pages/treasury/steps/treasury_proposal_template_step.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; @@ -21,8 +25,14 @@ class TreasuryBody extends StatelessWidget { items: value, stepBuilder: (context, step) { switch (step) { - case DummyTopicStep(): - return TreasuryDummyTopicStep(step: step); + case SetupCampaignDetailsStep(): + return TreasuryCampaignDetailsStep(step: step); + case SetupCampaignStagesStep(): + return _TreasuryCampaignStagesStep(step: step); + case SetupProposalTemplateStep(): + return TreasuryProposalTemplateStep(step: step); + case SetupCampaignCategoriesStep(): + return TreasuryCampaignCategoriesStep(step: step); } }, ); @@ -30,3 +40,22 @@ class TreasuryBody extends StatelessWidget { ); } } + +class _TreasuryCampaignStagesStep extends StatelessWidget { + final TreasurySectionStep step; + + const _TreasuryCampaignStagesStep({required this.step}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: SectionsControllerScope.of(context), + builder: (context, sectionsState, child) { + final isEditing = sectionsState.isEditing(step.sectionStepId); + return isEditing + ? TreasuryCampaignStagesEditStep(step: step) + : TreasuryCampaignStagesViewStep(step: step); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart index e5250fb8910..4c9e0b5142b 100644 --- a/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/treasury/treasury_page.dart @@ -7,25 +7,19 @@ import 'package:flutter/material.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; const sections = [ - CampaignSetup( - id: 0, + TreasurySection( + id: '0', steps: [ - DummyTopicStep( - id: 0, - sectionId: 0, - isEditable: false, - ), - DummyTopicStep(id: 1, sectionId: 0), - DummyTopicStep(id: 2, sectionId: 0), - DummyTopicStep(id: 3, sectionId: 0), + SetupCampaignDetailsStep(id: '0', sectionId: '0'), + SetupCampaignStagesStep(id: '1', sectionId: '0'), + SetupProposalTemplateStep(id: '2', sectionId: '0'), + SetupCampaignCategoriesStep(id: '3', sectionId: '0'), ], ), ]; class TreasuryPage extends StatefulWidget { - const TreasuryPage({ - super.key, - }); + const TreasuryPage({super.key}); @override State createState() => _TreasuryPageState(); diff --git a/catalyst_voices/apps/voices/lib/pages/voting/voting_page.dart b/catalyst_voices/apps/voices/lib/pages/voting/voting_page.dart index d7042441953..e0331f6c9e4 100644 --- a/catalyst_voices/apps/voices/lib/pages/voting/voting_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/voting/voting_page.dart @@ -4,8 +4,8 @@ import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,7 +20,7 @@ and PRISM, but its potential is only barely exploited. final _proposals = [ PendingProposal( id: 'f14/0', - fund: 'F14', + campaignName: 'F14', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', lastUpdateDate: DateTime.now().minusDays(2), @@ -32,7 +32,7 @@ final _proposals = [ ), PendingProposal( id: 'f14/1', - fund: 'F14', + campaignName: 'F14', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', lastUpdateDate: DateTime.now().minusDays(2), @@ -44,7 +44,7 @@ final _proposals = [ ), PendingProposal( id: 'f14/2', - fund: 'F14', + campaignName: 'F14', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', lastUpdateDate: DateTime.now().minusDays(2), diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/answer.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/answer.dart deleted file mode 100644 index ef05833b8a5..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/answer.dart +++ /dev/null @@ -1,5 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const answer = [ - {"insert": "Answer\n"} -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/bonus_mark_up.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/bonus_mark_up.dart deleted file mode 100644 index 2accf562797..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/bonus_mark_up.dart +++ /dev/null @@ -1,78 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const bonusMarkUp = [ - { - 'insert': { - 'image': - 'https://upload.wikimedia.org/wikipedia/commons/b/b6/Image_created_with_a_mobile_phone.png', - }, - 'attributes': {'style': 'width: 181.764; height: 140; '}, - }, - {'insert': '\n\n'}, - { - 'insert': 'Legend Tells About Amazonian The Great Smith', - 'attributes': {'bold': true}, - }, - {'insert': '\n\nAn ancient legend confirms '}, - { - 'insert': 'Amazonian as the Father of the Samurai Sword. Amazonian', - 'attributes': {'bold': true}, - }, - {'insert': ' and his son, '}, - { - 'insert': 'Amateur', - 'attributes': {'italic': true}, - }, - { - 'insert': - ', were the prominent smiths who led a team of armorers, employed by ' - 'Emperor Mommy (683-707) to make swords for his army of warriors. ' - "Later his son, Amateur continued his father's ", - }, - { - 'insert': 'great work', - 'attributes': {'italic': true}, - }, - {'insert': '.\n\n'}, - { - 'insert': 'Amateur', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'bullet'}, - }, - { - 'insert': 'Amauroses', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'bullet'}, - }, - { - 'insert': 'Amateurism', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'bullet'}, - }, - {'insert': '\n'}, - { - 'insert': 'Sword 1', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'ordered'}, - }, - { - 'insert': 'Sword 2', - 'attributes': {'italic': true}, - }, - { - 'insert': '\n', - 'attributes': {'list': 'ordered'}, - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/delivery_and_accountability.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/delivery_and_accountability.dart deleted file mode 100644 index db16aec5191..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/delivery_and_accountability.dart +++ /dev/null @@ -1,53 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const deliveryAndAccountability = [ - { - "insert": - "The Catalyst Team is committed to providing continuous, accessible updates to the Cardano community. Updates and outputs will also be published on the Catalyst public Gitbook. \n\nIn addition to monthly progress reports and completed milestone proof of achievement ceremonies, the Catalyst team will also promote outcomes, outputs, and general progress in the following ways:   \n\n" - }, - { - "insert": "Weekly newsletters:", - "attributes": {"bold": true} - }, - {"insert": "\nReach: 60,000 mailing list members"}, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "Each week, an update email is sent to all mailing list members to provide a run down on progress and highlight key achievements.  " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "Fortnightly technical development updates: Reach: Averaging per week: 3000 report readers, 60,000 Twitter views, 100 retweets   " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "
The Catalyst team will provide technical development updates every two weeks as part of the overall Cardano technical development update communications. This will amount to at least 24 updates over the next 12 months, accounting for the seasonal Winter holiday period. \n
" - }, - { - "insert": "Weekly Town Halls", - "attributes": {"bold": true} - }, - { - "insert": - "\nReach: At least 1000 viewers, up to 10,000 periodically \n
Catalyst Town Hall is a mainstay platform for communicating key progress and achievements, and provides an opportunity to gather insights from attendees about new features or potential changes to Catalyst. Catalyst funded projects that have achieved project-completion status are highlighted weekly.\n
" - }, - { - "insert": "Catalyst Blogs: ", - "attributes": {"bold": true} - }, - { - "insert": - "\nReach: Averaging 5000 readers per blog based on the last 12 months\nRegular blogs published via ProjectCatalyst.io will help to amplify progress and updates to outputs that have been achieved.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/feasibility_checks.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/feasibility_checks.dart deleted file mode 100644 index 075cf767141..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/feasibility_checks.dart +++ /dev/null @@ -1,12 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const feasibilityChecks = [ - { - "insert": "Approach and implementation", - "attributes": {"bold": true} - }, - { - "insert": - "\n\nEngineering and security best practices will be followed to implement the solution, in addition to consultation with both the Catalyst / Cardano community and internal IOG subject matter experts from cryptography, and game theory domains. Prior user research and community feedback informs our initial understanding of challenges to solve for. \n\nCatalyst Voices intends to develop iOS, Android, and Web applications from a single code base with near-native speed and performance. \n\nWe will approach the implementation of sets of features in terms of “modules”. Each module will correspond to a user role, segmenting the experience into sections aimed at completing specific actions. \n\nRole registrations, participation history, and saved preferences will unlock new aspects of the experience to help users engage at their own pace, on their own terms.\n\nLearnings acquired through developing and maintaining existing tools (such as Catalyst Mobile App, Voting Center, Snapshot Module) will be leveraged in order to rewrite the target development frameworks by building a single platform that is highly secure, extensible, and maintainable. While we anticipate unexpected challenges in integrating all features into a single platform, our plan to leverage battle-tested reference implementations should de-risk and accelerate development significantly. \n\nThe project will also benefit from maximizing the results of prior discovery and design research. Wireframes and mock-up designs created for and after user testing for the proposal submission module to replace Ideascale will continue to refine the UX with further user feedback gathered during the delivery of this project. This will include features and UX interactions for user profiles, cross-module navigation, and embedded guidance. \n\nFinally, the development of Catalyst Voices will follow the testing and deployment framework planned for the existing Catalyst stack. The latest experimental features will be deployed to a public devnet, and the latest stable features will be deployed to a public testnet. The community will have opportunities to engage with new features and provide feedback before promoting features to the production release candidate. \n\nRecurring voting events will run every 2-weeks on the Catalyst testnet to provide more frequent opportunities to engage with each of the phases of proposing, reviewing, and voting.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/problem_statement.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/problem_statement.dart deleted file mode 100644 index 3c431c41fb4..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/problem_statement.dart +++ /dev/null @@ -1,8 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const problemStatement = [ - { - "insert": - "Catalyst's short participation windows with fragmented UX experience and complicated manual processes frustrate and limit community engagement.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/public_description.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/public_description.dart deleted file mode 100644 index 9e70174bb1b..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/public_description.dart +++ /dev/null @@ -1,121 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const publicDescription = [ - { - "insert": "Introducing Catalyst Voices
", - "attributes": {"bold": true} - }, - { - "insert": - "Through this 12 month project, the Catalyst Team proposes to unleash the wisdom of the community by delivering a front-end web-browser based application that radically lowers the barriers to meaningful participation in collective decision-making for voters, representatives, and proposers. \n\nA unified front-end interface to meet the needs of the Catalyst community. Developed with continuous feedback from the community, the Catalyst Team will deliver a unified experience to replace the patchwork composed of wallets, Ideascale, standalone web apps, and the Catalyst mobile app today. \n\nThe proposed product design is informed by insights gleaned over nearly three years of operating Catalyst, ongoing discovery research, and feedback from the community. \n
Prior feedback and research indicate 3 significant opportunities to improve the Catalyst experience with a unified platform:\nStreamline the experience by designing modules to serve the needs of each role" - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "Unlock continuous opportunities for ideation, feedback, building skills and reputation, as well as earning rewards" - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "Offer better guidance by creating context based on identity, preferences and intent
" - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": "The proposed outputs will: ", - "attributes": {"bold": true} - }, - { - "insert": - "\nreduce the time and steps required of Catalyst users to complete important actions eliminating the frustration of context switching " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "accelerate the onboarding and upskilling of both casual and committed Catalyst users" - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "improve the quality of participation with just-in-time tips, and more data provided to voters about Catalyst proposals   " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "introduce participation history that maintains data and context over time, " - }, - { - "insert": "\n", - "attributes": {"list": "bullet"} - }, - { - "insert": - "
Simplified user processes mean more time spent on activities that matter, enabling new capabilities and co-building opportunities that push the boundaries of distributed decision-making. \n\nAll these benefits add up to productivity gains, a more collaborative and focused community, and better funding decisions that create more value for the Cardano ecosystem. \n" - }, - { - "insert": - "
Unlocking Incremental Value With Milestones & Continuous Testing", - "attributes": {"bold": true} - }, - { - "insert": - "\nThe proposed project will be delivered via a series of milestones, each unlocking new capabilities and creating value for Catalyst and the Cardano \necosystems. \n" - }, - { - "insert": "
Milestones include:", - "attributes": {"bold": true} - }, - {"insert": "\nOpen Source Setup"}, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - { - "insert": "Architectural Updates to registrations to support multiple roles" - }, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - {"insert": "Backend and Wallet Integration Updates"}, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - {"insert": "Voting & Delegation"}, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - {"insert": "Proposal Submission & Commentary"}, - { - "insert": "\n", - "attributes": {"list": "ordered"} - }, - {"insert": "\n"}, - { - "insert": "Continuous Testing & Learning", - "attributes": {"bold": true} - }, - { - "insert": - "\nAlong the way, continuous delivery to the Catalyst testnet will ensure that the community has meaningful feedback loops to help guide development - rather than waiting to give feedback. Voters, representatives, and proposers will have a chance to test drive the entire Catalyst process end-to-end every 2 weeks from inside Catalyst Voices once available.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/solution_statement.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/solution_statement.dart deleted file mode 100644 index 731e2a145a6..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/solution_statement.dart +++ /dev/null @@ -1,8 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const solutionStatement = [ - { - "insert": - "Catalyst Voices provides a unified experience and platform including production-ready liquid democracy, meaningful collaboration opportunities & data-driven context for better onboarding&decisions.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/title.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/title.dart deleted file mode 100644 index 8c4f522d7b2..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/title.dart +++ /dev/null @@ -1,8 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const title = [ - { - "insert": - "F10 / IOG Catalyst Team : Ideascale replacement and web-browser based Voting Centre with liquid democracy aka “Catalyst Voices”\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/value_for_money.dart b/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/value_for_money.dart deleted file mode 100644 index e18697d3b5c..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/rich_text/value_for_money.dart +++ /dev/null @@ -1,16 +0,0 @@ -// ignore_for_file: prefer_single_quotes, require_trailing_commas, lines_longer_than_80_chars - -const valueForMoney = [ - { - "insert": - "The requested total budget for developing this first iteration of Catalyst Voices as a functional replacement for Ideascale, the existing mobile application, and wallet interface for registration is 840,000 ADA. This is a 12 Month project.\n    \nFund 11 Period : Open Source Activation             
₳75,000     \nFund 11 Period : Voices Architectural Changes,        
₳150,000\nFund 12 Period : Backend & Wallet Integration        
₳189,000    \nFund 13 Period : Voting & Delegation Implementation  
₳200,000    \nFund 13 Period : Voices First Release - Proposal Process Implementation     
₳226,000    
\n" - }, - { - "insert": "Total: ₳840,000", - "attributes": {"bold": true} - }, - { - "insert": - "\n
To deliver this work we require a small team of rust backend developers, QA engineers, front end developers, site reliability engineers and UI designers.\n
This is a moderately complex proposal due to the development of fully decentralized role based access control to replace the Web2 authorizations and user management required by Ideascale and other typical systems. It will also require throughout the entire project, Architectural Design to work on and refine the proposed CIPs and other technical aspects of the system, Engineering Management to keep the project on track, and Product Management that the final product delights and empowers users across the community.\n
This project will also require regular updates to the community and high levels of community engagement to properly respond to and evaluate feedback or queries we are receiving. Especially as it relates to refining and finalizing the CIPs necessary to underpin the operation of this Project, which will also be critical work underpinning future work both in Catalyst Voices and in future work envisioned for the “Athena” distributed Project Catalyst which we ultimately desire to have all the functionality proposed here.\n
We will also be building and deploying test versions of Catalyst Voices continuously to our internal test deployments, and these will be publicly accessible. This is to allow the community to easily see what state the development is in and the available functionality without needing to run the system themselves. We want to allow the greatest number of Project Catalyst community members to track our progress as possible.\n" - } -]; diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_empty_state.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_empty_state.dart new file mode 100644 index 00000000000..20441eaabfd --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_empty_state.dart @@ -0,0 +1,36 @@ +import 'package:catalyst_voices/widgets/empty_state/empty_state.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class WorkspaceEmptyStateSelector extends StatelessWidget { + const WorkspaceEmptyStateSelector({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.showEmptyState, + builder: (context, state) { + return Offstage( + offstage: !state, + child: const _WorkspaceEmptyState(), + ); + }, + ); + } +} + +class _WorkspaceEmptyState extends StatelessWidget { + const _WorkspaceEmptyState(); + + @override + Widget build(BuildContext context) { + // TODO(damian-molinski): Strings and looks is not final + return const Center( + child: EmptyState( + title: 'No proposals found', + description: 'Created proposals will appear here', + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_error.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_error.dart new file mode 100644 index 00000000000..2aae3aa58dc --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_error.dart @@ -0,0 +1,50 @@ +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +typedef _StateData = ({bool show, LocalizedException? error}); + +class WorkspaceErrorSelector extends StatelessWidget { + const WorkspaceErrorSelector({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => (show: state.showError, error: state.error), + builder: (context, state) { + final errorMessage = state.error?.message(context); + + return Offstage( + offstage: !state.show, + child: _WorkspaceError( + message: errorMessage ?? context.l10n.somethingWentWrong, + ), + ); + }, + ); + } +} + +class _WorkspaceError extends StatelessWidget { + final String message; + + const _WorkspaceError({ + required this.message, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: VoicesErrorIndicator( + message: message, + onRetry: () { + const event = LoadProposalsEvent(); + context.read().add(event); + }, + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_header.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_header.dart new file mode 100644 index 00000000000..e482a01ffa6 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_header.dart @@ -0,0 +1,230 @@ +import 'package:catalyst_voices/common/ext/space_ext.dart'; +import 'package:catalyst_voices/routes/routes.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class WorkspaceHeader extends StatelessWidget { + const WorkspaceHeader({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 32), + _TitleText(), + SizedBox(height: 48), + Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SubtitleText(), + SizedBox(height: 16), + _DescriptionText(), + SizedBox(height: 32), + _DraftProposalButton(), + ], + ), + ), + SizedBox(width: 20), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _SearchTextField(), + ], + ), + ), + ], + ), + SizedBox(height: 16), + _TabsSelector(), + ], + ), + ), + ); + } +} + +class _TitleText extends StatelessWidget { + const _TitleText(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Text( + Space.workspace.localizedName(context.l10n), + style: theme.textTheme.headlineLarge?.copyWith( + color: theme.colorScheme.primary, + ), + ); + } +} + +class _SubtitleText extends StatelessWidget { + const _SubtitleText(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Text( + context.l10n.myProposals, + style: theme.textTheme.displaySmall?.copyWith( + color: theme.colors.textOnPrimaryLevel0, + ), + ); + } +} + +class _DescriptionText extends StatelessWidget { + const _DescriptionText(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Text( + context.l10n.workspaceDescription, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colors.textOnPrimaryLevel0, + ), + ); + } +} + +class _DraftProposalButton extends StatelessWidget { + const _DraftProposalButton(); + + @override + Widget build(BuildContext context) { + return VoicesFilledButton( + onTap: () async { + final id = await context.read().createNewDraftProposal(); + + if (context.mounted) { + ProposalBuilderRoute(proposalId: id).go(context); + } + }, + leading: VoicesAssets.icons.plus.buildIcon(), + child: Text(context.l10n.newDraftProposal), + ); + } +} + +class _TabsSelector extends StatelessWidget { + const _TabsSelector(); + + @override + Widget build(BuildContext context) { + final tab = context.read().state.tab; + + return DefaultTabController( + length: WorkspaceTabType.values.length, + initialIndex: tab.index, + child: BlocListener( + listener: (context, state) { + final index = state.tab.index; + final tabController = DefaultTabController.of(context); + + if (tabController.index != index) { + tabController.animateTo(index); + } + }, + listenWhen: (previous, current) => previous.tab != current.tab, + child: BlocSelector( + selector: (state) { + return ( + draftProposalCount: state.draftProposalCount, + finalProposalCount: state.finalProposalCount + ); + }, + builder: (context, state) { + return _Tabs( + draftProposalCount: state.draftProposalCount, + finalProposalCount: state.finalProposalCount, + ); + }, + ), + ), + ); + } +} + +class _Tabs extends StatelessWidget { + final int draftProposalCount; + final int finalProposalCount; + + const _Tabs({ + required this.draftProposalCount, + required this.finalProposalCount, + }); + + @override + Widget build(BuildContext context) { + return TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + onTap: (index) { + final tab = WorkspaceTabType.values[index]; + final event = TabChangedEvent(tab); + context.read().add(event); + }, + tabs: [ + Tab(text: context.l10n.draftProposalsX(draftProposalCount)), + Tab(text: context.l10n.finalProposalsX(finalProposalCount)), + ], + ); + } +} + +class _SearchTextField extends StatelessWidget { + const _SearchTextField(); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 372), + child: VoicesTextField( + decoration: VoicesTextFieldDecoration( + labelText: context.l10n.searchProposals, + hintText: context.l10n.search, + prefixIcon: VoicesAssets.icons.search.buildIcon(), + filled: true, + ), + keyboardType: TextInputType.text, + onFieldSubmitted: (value) { + final event = SearchQueryChangedEvent(value, isSubmitted: true); + context.read().add(event); + }, + onChanged: (value) { + final event = SearchQueryChangedEvent(value); + context.read().add(event); + }, + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_loading.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_loading.dart new file mode 100644 index 00000000000..08afcced232 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_loading.dart @@ -0,0 +1,39 @@ +import 'package:catalyst_voices/widgets/indicators/voices_circular_progress_indicator.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class WorkspaceLoadingSelector extends StatelessWidget { + const WorkspaceLoadingSelector({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.isLoading, + builder: (context, state) { + return Offstage( + offstage: !state, + child: TickerMode( + enabled: state, + child: const _WorkspaceLoading(), + ), + ); + }, + ); + } +} + +class _WorkspaceLoading extends StatelessWidget { + const _WorkspaceLoading(); + + @override + Widget build(BuildContext context) { + return const Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only(top: 32), + child: VoicesCircularProgressIndicator(), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart index 5e06dc2e530..1752b1dad22 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_page.dart @@ -1,167 +1,46 @@ -import 'package:catalyst_voices/pages/workspace/rich_text/answer.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/bonus_mark_up.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/delivery_and_accountability.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/feasibility_checks.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/problem_statement.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/public_description.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/solution_statement.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/title.dart'; -import 'package:catalyst_voices/pages/workspace/rich_text/value_for_money.dart'; -import 'package:catalyst_voices/pages/workspace/workspace_body.dart'; -import 'package:catalyst_voices/pages/workspace/workspace_navigation_panel.dart'; -import 'package:catalyst_voices/pages/workspace/workspace_setup_panel.dart'; -import 'package:catalyst_voices/widgets/containers/space_scaffold.dart'; -import 'package:catalyst_voices/widgets/navigation/sections_controller.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:catalyst_voices/pages/workspace/workspace_empty_state.dart'; +import 'package:catalyst_voices/pages/workspace/workspace_error.dart'; +import 'package:catalyst_voices/pages/workspace/workspace_header.dart'; +import 'package:catalyst_voices/pages/workspace/workspace_loading.dart'; +import 'package:catalyst_voices/pages/workspace/workspace_proposals.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:flutter/material.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -final sections = [ - const ProposalSetup( - id: 0, - steps: [ - TitleStep( - id: 0, - sectionId: 0, - data: DocumentJson(title), - guidances: mockGuidance, - ), - ], - ), - ProposalSummary( - id: 1, - steps: [ - ProblemStep( - id: 0, - sectionId: 1, - data: const DocumentJson(problemStatement), - charsLimit: 200, - guidances: [ - mockGuidance[0], - ], - ), - const SolutionStep( - id: 1, - sectionId: 1, - data: DocumentJson(solutionStatement), - charsLimit: 200, - guidances: mockGuidance, - ), - const PublicDescriptionStep( - id: 2, - sectionId: 1, - data: DocumentJson(publicDescription), - charsLimit: 3000, - guidances: mockGuidance, - ), - ], - ), - const ProposalSolution( - id: 2, - steps: [ - ProblemPerspectiveStep( - id: 0, - sectionId: 2, - data: DocumentJson(answer), - charsLimit: 200, - ), - PerspectiveRationaleStep( - id: 1, - sectionId: 2, - data: DocumentJson(answer), - charsLimit: 200, - ), - ProjectEngagementStep( - id: 2, - sectionId: 2, - data: DocumentJson(answer), - charsLimit: 200, - ), - ], - ), - const ProposalImpact( - id: 3, - steps: [ - BonusMarkUpStep( - id: 0, - sectionId: 3, - data: DocumentJson(bonusMarkUp), - charsLimit: 900, - ), - ValueForMoneyStep( - id: 1, - sectionId: 3, - data: DocumentJson(valueForMoney), - charsLimit: 2600, - ), - ], - ), - const CompatibilityAndFeasibility( - id: 4, - steps: [ - DeliveryAndAccountabilityStep( - id: 0, - sectionId: 4, - data: DocumentJson(deliveryAndAccountability), - ), - FeasibilityChecksStep( - id: 1, - sectionId: 4, - data: DocumentJson(feasibilityChecks), - ), - ], - ), -]; +import 'package:flutter_bloc/flutter_bloc.dart'; class WorkspacePage extends StatefulWidget { - const WorkspacePage({ - super.key, - }); + const WorkspacePage({super.key}); @override State createState() => _WorkspacePageState(); } class _WorkspacePageState extends State { - late final SectionsController _sectionsController; - late final ItemScrollController _bodyItemScrollController; - @override void initState() { super.initState(); - _sectionsController = SectionsController(); - _bodyItemScrollController = ItemScrollController(); - - _sectionsController.attachItemsScrollController(_bodyItemScrollController); - - _populateSections(); - } - - @override - void dispose() { - _sectionsController.dispose(); - super.dispose(); + context.read().add(const LoadProposalsEvent()); } @override Widget build(BuildContext context) { - return SectionsControllerScope( - controller: _sectionsController, - child: SpaceScaffold( - left: const WorkspaceNavigationPanel(), - body: WorkspaceBody( - itemScrollController: _bodyItemScrollController, - ), - right: const WorkspaceSetupPanel(), + return const Scaffold( + body: Column( + children: [ + WorkspaceHeader(), + Expanded( + child: Stack( + fit: StackFit.expand, + children: [ + WorkspaceErrorSelector(), + WorkspaceEmptyStateSelector(), + WorkspaceProposalsSelector(), + WorkspaceLoadingSelector(), + ], + ), + ), + ], ), ); } - - void _populateSections() { - _sectionsController.value = SectionsControllerState.initial( - sections: sections, - ); - } } diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_proposals.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_proposals.dart new file mode 100644 index 00000000000..ee6becf2bf8 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_proposals.dart @@ -0,0 +1,79 @@ +import 'package:catalyst_voices/routes/routes.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +typedef _ListItems = List; + +class WorkspaceProposalsSelector extends StatelessWidget { + const WorkspaceProposalsSelector({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.proposals, + builder: (context, state) { + return Offstage( + offstage: state.isEmpty, + child: _WorkspaceProposals(items: state), + ); + }, + ); + } +} + +class _WorkspaceProposals extends StatelessWidget { + final List items; + + const _WorkspaceProposals({ + required this.items, + }); + + @override + Widget build(BuildContext context) { + return ListView.separated( + itemBuilder: (context, index) { + final item = items[index]; + + return _ProposalListTile( + key: ValueKey('WorkspaceProposal${item.id}ListTileKey'), + item: item, + onTap: () { + ProposalBuilderRoute(proposalId: item.id).go(context); + }, + ); + }, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemCount: items.length, + padding: const EdgeInsets.all(32), + ); + } +} + +// TODO(damian-molinski): Looks is not final +class _ProposalListTile extends StatelessWidget { + final WorkspaceProposalListItem item; + final VoidCallback? onTap; + + const _ProposalListTile({ + super.key, + required this.item, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Material( + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + child: Text(item.name), + ), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_setup_panel.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_setup_panel.dart deleted file mode 100644 index 279dc34265d..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_setup_panel.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:catalyst_voices/pages/workspace/workspace_guidance_view.dart'; -import 'package:catalyst_voices/widgets/cards/comment_card.dart'; -import 'package:catalyst_voices/widgets/widgets.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'; - -const List mockGuidance = [ - Guidance( - title: 'Use a Compelling Hook or Unique Angle', - description: - '''Adding an element of intrigue or a unique approach can make your title stand out. For example, “Revolutionizing Urban Mobility with Eco-Friendly Innovation” not only describes the proposal but also piques curiosity.''', - type: GuidanceType.tips, - weight: 1, - ), - Guidance( - title: 'Be Specific and Solution-Oriented', - description: - '''Use keywords that pinpoint the problem you’re solving or the opportunity you’re capitalizing on. A title like “Streamlining Supply Chains for Cost-Effective and Rapid Delivery” instantly tells the reader what the proposal aims to achieve.''', - type: GuidanceType.mandatory, - weight: 2, - ), - Guidance( - title: 'Highlight the Benefit or Outcome', - description: - '''Make sure the reader can immediately see the value or the end result of your proposal. A title like “Boosting Engagement and Growth through Targeted Digital Strategies” puts the focus on the positive outcomes.''', - type: GuidanceType.mandatory, - weight: 1, - ), - Guidance( - title: 'Education', - description: 'Use keywords that pinpoint the problem yo', - type: GuidanceType.education, - weight: 1, - ), -]; - -class WorkspaceSetupPanel extends StatelessWidget { - const WorkspaceSetupPanel({super.key}); - - @override - Widget build(BuildContext context) { - return SpaceSidePanel( - isLeft: false, - name: context.l10n.workspaceProposalSetup, - onCollapseTap: () {}, - tabs: [ - SpaceSidePanelTab( - name: 'Guidance', - body: SetupSectionListener( - SectionsControllerScope.of(context), - ), - ), - SpaceSidePanelTab( - name: 'Comments', - body: CommentCard( - comment: Comment( - text: 'Lacks clarity on key objectives and measurable outcomes.', - date: DateTime.now(), - userName: 'Community Member', - ), - ), - ), - //No actions for now - // SpaceSidePanelTab( - // name: 'Actions', - // body: const Offstage(), - // ), - ], - ); - } -} - -class SetupSectionListener extends StatelessWidget { - final SectionsController _controller; - - const SetupSectionListener( - this._controller, { - super.key, - }); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _controller, - builder: (context, value, _) { - final activeStepId = value.activeStepId; - final activeStepGuidances = value.activeStepGuidances; - - if (activeStepId == null) { - return Text(context.l10n.selectASection); - } else if (activeStepGuidances == null || activeStepGuidances.isEmpty) { - return Text(context.l10n.noGuidanceForThisSection); - } else { - return GuidanceView(activeStepGuidances); - } - }, - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/routes/app_router.dart b/catalyst_voices/apps/voices/lib/routes/app_router.dart index 5fb2fa2623d..3a7925c7445 100644 --- a/catalyst_voices/apps/voices/lib/routes/app_router.dart +++ b/catalyst_voices/apps/voices/lib/routes/app_router.dart @@ -13,12 +13,13 @@ abstract final class AppRouter { ); static GoRouter init({ + String? initialLocation, List guards = const [], Listenable? refreshListenable, }) { return GoRouter( navigatorKey: _rootNavigatorKey, - initialLocation: Routes.initialLocation, + initialLocation: initialLocation ?? Routes.initialLocation, redirect: (context, state) async => _guard(context, state, guards), observers: [ SentryNavigatorObserver(), diff --git a/catalyst_voices/apps/voices/lib/routes/guards/user_access_guard.dart b/catalyst_voices/apps/voices/lib/routes/guards/user_access_guard.dart new file mode 100644 index 00000000000..e917195eea6 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/routes/guards/user_access_guard.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:catalyst_voices/routes/guards/route_guard.dart'; +import 'package:catalyst_voices/routes/routes.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +final class UserAccessGuard implements RouteGuard { + const UserAccessGuard(); + + @override + FutureOr redirect(BuildContext context, GoRouterState state) { + final account = context.read().state.account; + + if (account == null) { + return const DiscoveryRoute().location; + } + if (account.isAdmin) { + return null; + } + if (state.path == const VotingRoute().location || + state.path == const FundedProjectsRoute().location) { + return const DiscoveryRoute().location; + } + if (account.roles.any( + (role) => [AccountRole.proposer, AccountRole.drep].contains(role), + )) return null; + return const DiscoveryRoute().location; + } +} + +final class AdminAccessGuard implements RouteGuard { + const AdminAccessGuard(); + @override + FutureOr redirect(BuildContext context, GoRouterState state) { + final account = context.read().state.account; + if (account?.isAdmin ?? false) { + return null; + } else { + return const DiscoveryRoute().location; + } + } +} diff --git a/catalyst_voices/apps/voices/lib/routes/routing/login_route.dart b/catalyst_voices/apps/voices/lib/routes/routing/login_route.dart deleted file mode 100644 index 4bec9dce343..00000000000 --- a/catalyst_voices/apps/voices/lib/routes/routing/login_route.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:catalyst_voices/pages/login/login.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -part 'login_route.g.dart'; - -@TypedGoRoute(path: '/login') -final class LoginRoute extends GoRouteData { - const LoginRoute(); - - @override - Widget build(BuildContext context, GoRouterState state) { - return const LoginPage(); - } -} diff --git a/catalyst_voices/apps/voices/lib/routes/routing/overall_spaces_route.dart b/catalyst_voices/apps/voices/lib/routes/routing/overall_spaces_route.dart index b02d4bc8b8c..b7b3857d61b 100644 --- a/catalyst_voices/apps/voices/lib/routes/routing/overall_spaces_route.dart +++ b/catalyst_voices/apps/voices/lib/routes/routing/overall_spaces_route.dart @@ -1,4 +1,7 @@ import 'package:catalyst_voices/pages/overall_spaces/overall_spaces.dart'; +import 'package:catalyst_voices/routes/guards/composite_route_guard_mixin.dart'; +import 'package:catalyst_voices/routes/guards/route_guard.dart'; +import 'package:catalyst_voices/routes/guards/session_unlocked_guard.dart'; import 'package:catalyst_voices/routes/routing/routes.dart'; import 'package:catalyst_voices/routes/routing/transitions/fade_page_transition_mixin.dart'; import 'package:flutter/widgets.dart'; @@ -10,9 +13,12 @@ part 'overall_spaces_route.g.dart'; path: '/${Routes.currentMilestone}/spaces', ) final class OverallSpacesRoute extends GoRouteData - with FadePageTransitionMixin { + with FadePageTransitionMixin, CompositeRouteGuardMixin { const OverallSpacesRoute(); + @override + List get routeGuards => [const SessionUnlockedGuard()]; + @override Widget build(BuildContext context, GoRouterState state) { return const OverallSpacesPage(); diff --git a/catalyst_voices/apps/voices/lib/routes/routing/routes.dart b/catalyst_voices/apps/voices/lib/routes/routing/routes.dart index e533df0b659..c503877e43e 100644 --- a/catalyst_voices/apps/voices/lib/routes/routing/routes.dart +++ b/catalyst_voices/apps/voices/lib/routes/routing/routes.dart @@ -1,7 +1,6 @@ import 'package:catalyst_voices/routes/routing/account_route.dart' as account; import 'package:catalyst_voices/routes/routing/coming_soon_route.dart' as coming_soon; -import 'package:catalyst_voices/routes/routing/login_route.dart' as login; import 'package:catalyst_voices/routes/routing/overall_spaces_route.dart' as overall_spaces; import 'package:catalyst_voices/routes/routing/spaces_route.dart' as spaces; @@ -15,7 +14,6 @@ abstract final class Routes { static final List routes = [ ...account.$appRoutes, ...coming_soon.$appRoutes, - ...login.$appRoutes, ...spaces.$appRoutes, ...overall_spaces.$appRoutes, ]; diff --git a/catalyst_voices/apps/voices/lib/routes/routing/routing.dart b/catalyst_voices/apps/voices/lib/routes/routing/routing.dart index e5311519885..8f416f5e55f 100644 --- a/catalyst_voices/apps/voices/lib/routes/routing/routing.dart +++ b/catalyst_voices/apps/voices/lib/routes/routing/routing.dart @@ -1,6 +1,5 @@ export 'account_route.dart' hide $appRoutes; export 'coming_soon_route.dart' hide $appRoutes; -export 'login_route.dart' hide $appRoutes; export 'overall_spaces_route.dart' hide $appRoutes; export 'routes.dart'; export 'spaces_route.dart' hide $appRoutes; diff --git a/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.dart b/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.dart index 8d9ca25a7ac..fdb45992f80 100644 --- a/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.dart +++ b/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.dart @@ -1,9 +1,14 @@ import 'package:catalyst_voices/pages/discovery/discovery.dart'; import 'package:catalyst_voices/pages/funded_projects/funded_projects_page.dart'; +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder.dart'; import 'package:catalyst_voices/pages/spaces/spaces.dart'; import 'package:catalyst_voices/pages/treasury/treasury.dart'; import 'package:catalyst_voices/pages/voting/voting_page.dart'; -import 'package:catalyst_voices/pages/workspace/workspace_page.dart'; +import 'package:catalyst_voices/pages/workspace/workspace.dart'; +import 'package:catalyst_voices/routes/guards/composite_route_guard_mixin.dart'; +import 'package:catalyst_voices/routes/guards/route_guard.dart'; +import 'package:catalyst_voices/routes/guards/session_unlocked_guard.dart'; +import 'package:catalyst_voices/routes/guards/user_access_guard.dart'; import 'package:catalyst_voices/routes/routing/routes.dart'; import 'package:catalyst_voices/routes/routing/transitions/transitions.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; @@ -12,15 +17,16 @@ import 'package:go_router/go_router.dart'; part 'spaces_route.g.dart'; +const _prefix = Routes.currentMilestone; + @TypedShellRoute( routes: >[ - TypedGoRoute(path: '/${Routes.currentMilestone}/discovery'), - TypedGoRoute(path: '/${Routes.currentMilestone}/workspace'), - TypedGoRoute(path: '/${Routes.currentMilestone}/voting'), - TypedGoRoute( - path: '/${Routes.currentMilestone}/funded_projects', - ), - TypedGoRoute(path: '/${Routes.currentMilestone}/treasury'), + TypedGoRoute(path: '/$_prefix/discovery'), + TypedGoRoute(path: '/$_prefix/workspace'), + TypedGoRoute(path: '/$_prefix/workspace/:proposalId'), + TypedGoRoute(path: '/$_prefix/voting'), + TypedGoRoute(path: '/$_prefix/funded_projects'), + TypedGoRoute(path: '/$_prefix/treasury'), ], ) final class SpacesShellRouteData extends ShellRouteData { @@ -68,18 +74,52 @@ final class DiscoveryRoute extends GoRouteData with FadePageTransitionMixin { } } -final class WorkspaceRoute extends GoRouteData with FadePageTransitionMixin { +final class WorkspaceRoute extends GoRouteData + with FadePageTransitionMixin, CompositeRouteGuardMixin { const WorkspaceRoute(); + @override + List get routeGuards => [ + const SessionUnlockedGuard(), + const UserAccessGuard(), + ]; + @override Widget build(BuildContext context, GoRouterState state) { return const WorkspacePage(); } } -final class VotingRoute extends GoRouteData with FadePageTransitionMixin { +final class ProposalBuilderRoute extends GoRouteData + with FadePageTransitionMixin, CompositeRouteGuardMixin { + final String proposalId; + + const ProposalBuilderRoute({ + required this.proposalId, + }); + + @override + List get routeGuards => [ + const SessionUnlockedGuard(), + const UserAccessGuard(), + ]; + + @override + Widget build(BuildContext context, GoRouterState state) { + return ProposalBuilderPage(proposalId: proposalId); + } +} + +final class VotingRoute extends GoRouteData + with FadePageTransitionMixin, CompositeRouteGuardMixin { const VotingRoute(); + @override + List get routeGuards => [ + const SessionUnlockedGuard(), + const UserAccessGuard(), + ]; + @override Widget build(BuildContext context, GoRouterState state) { return const VotingPage(); @@ -87,18 +127,31 @@ final class VotingRoute extends GoRouteData with FadePageTransitionMixin { } final class FundedProjectsRoute extends GoRouteData - with FadePageTransitionMixin { + with FadePageTransitionMixin, CompositeRouteGuardMixin { const FundedProjectsRoute(); + @override + List get routeGuards => [ + const SessionUnlockedGuard(), + const UserAccessGuard(), + ]; + @override Widget build(BuildContext context, GoRouterState state) { return const FundedProjectsPage(); } } -final class TreasuryRoute extends GoRouteData with FadePageTransitionMixin { +final class TreasuryRoute extends GoRouteData + with FadePageTransitionMixin, CompositeRouteGuardMixin { const TreasuryRoute(); + @override + List get routeGuards => [ + const SessionUnlockedGuard(), + const AdminAccessGuard(), + ]; + @override Widget build(BuildContext context, GoRouterState state) { return const TreasuryPage(); diff --git a/catalyst_voices/apps/voices/lib/widgets/app_bar/session/session_action_header.dart b/catalyst_voices/apps/voices/lib/widgets/app_bar/session/session_action_header.dart index daef794462f..4c09f181e65 100644 --- a/catalyst_voices/apps/voices/lib/widgets/app_bar/session/session_action_header.dart +++ b/catalyst_voices/apps/voices/lib/widgets/app_bar/session/session_action_header.dart @@ -39,6 +39,7 @@ class _GetStartedButton extends StatelessWidget { @override Widget build(BuildContext context) { return VoicesFilledButton( + key: const Key('GetStartedButton'), onTap: () => unawaited(RegistrationDialog.show(context)), child: Text(context.l10n.getStarted), ); diff --git a/catalyst_voices/apps/voices/lib/widgets/buttons/voices_buttons.dart b/catalyst_voices/apps/voices/lib/widgets/buttons/voices_buttons.dart index 408077d9fd8..a5768ea08a2 100644 --- a/catalyst_voices/apps/voices/lib/widgets/buttons/voices_buttons.dart +++ b/catalyst_voices/apps/voices/lib/widgets/buttons/voices_buttons.dart @@ -15,6 +15,7 @@ class DrawerToggleButton extends StatelessWidget { @override Widget build(BuildContext context) { return VoicesIconButton( + key: const Key('DrawerButton'), onTap: () => Scaffold.maybeOf(context)?.openDrawer(), child: VoicesAssets.icons.menu.buildIcon(), ); @@ -165,6 +166,7 @@ class VoicesLearnMoreButton extends StatelessWidget { @override Widget build(BuildContext context) { return VoicesTextButton( + key: const Key('LearnMoreButton'), trailing: VoicesAssets.icons.externalLink.buildIcon(), onTap: onTap, child: Text(context.l10n.learnMore), diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart new file mode 100644 index 00000000000..543cfae12f1 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart @@ -0,0 +1,126 @@ +import 'package:catalyst_voices/common/formatters/date_formatter.dart'; +import 'package:catalyst_voices/widgets/widgets.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'; + +class CampaignStageCard extends StatelessWidget { + final CampaignInfo campaign; + + const CampaignStageCard({ + super.key, + required this.campaign, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + return DecoratedBox( + decoration: BoxDecoration( + color: theme.colors.elevationsOnSurfaceNeutralLv1White, + border: Border.all( + color: theme.colors.outlineBorderVariant!, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + VoicesAvatar( + icon: VoicesAssets.icons.speakerphone.buildIcon(), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + campaign.stage.localizedName(context.l10n), + style: textTheme.titleMedium, + ), + Text( + campaign.description, + style: textTheme.bodyMedium, + ), + if (_getDateInformation(context).isNotEmpty) + _CampaignDateInformation( + value: _getDateInformation(context), + ), + const SizedBox(height: 16), + ], + ), + ), + ], + ), + if (campaign.stage == CampaignStage.live) ...[ + const SizedBox(height: 16), + OutlinedButton( + // TODO(ryszard-schossler): add logic + onPressed: () {}, + child: Text(context.l10n.viewProposals), + ), + ] else if (campaign.stage == CampaignStage.completed) ...[ + const SizedBox(height: 16), + OutlinedButton( + // TODO(ryszard-schossler): add logic + onPressed: () {}, + child: Text(context.l10n.viewVotingResults), + ), + ], + ], + ), + ), + ); + } + + String _getDateInformation(BuildContext context) { + switch (campaign.stage) { + case CampaignStage.draft: + case CampaignStage.scheduled: + final formattedDate = + DateFormatter.formatDateTimeParts(campaign.startDate); + return context.l10n + .campaignBeginsOn(formattedDate.$1, formattedDate.$2); + case CampaignStage.live: + final formattedDate = + DateFormatter.formatDateTimeParts(campaign.endDate); + return context.l10n.campaignEndsOn(formattedDate.$1, formattedDate.$2); + case CampaignStage.completed: + return ''; + } + } +} + +class _CampaignDateInformation extends StatelessWidget { + final String value; + + const _CampaignDateInformation({required this.value}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 30), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + VoicesAssets.icons.calendar.buildIcon(), + const SizedBox(width: 8), + Text( + value, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ], + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/funded_proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/funded_proposal_card.dart index dcec7e5eeb3..aed06e0da37 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/funded_proposal_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/funded_proposal_card.dart @@ -1,10 +1,8 @@ -import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:catalyst_voices/widgets/widgets.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_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; /// Displays a proposal in funded state on a card. @@ -45,7 +43,7 @@ class FundedProposalCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _FundCategory( - fund: proposal.fund, + fund: proposal.campaignName, category: proposal.category, ), const SizedBox(height: 4), @@ -184,7 +182,7 @@ class _FundedDate extends StatelessWidget { } class _FundsAndComments extends StatelessWidget { - final Coin funds; + final String funds; final int commentsCount; const _FundsAndComments({ @@ -206,7 +204,7 @@ class _FundsAndComments extends StatelessWidget { Column( children: [ Text( - CryptocurrencyFormatter.formatAmount(funds), + funds, style: Theme.of(context).textTheme.titleLarge, ), Text( diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart index 17e2aae73e0..3cf6253019d 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart @@ -2,7 +2,7 @@ import 'package:catalyst_voices/common/ext/guidance_ext.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:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; class GuidanceCard extends StatelessWidget { @@ -57,6 +57,14 @@ class GuidanceCard extends StatelessWidget { ); } - String _buildTypeTitle(BuildContext context) => - '${guidance.type.localizedType(context.l10n)} ${guidance.weightText}'; + String _buildTypeTitle(BuildContext context) { + final weight = guidance.weight; + final localizedType = guidance.type.localizedType(context.l10n); + + if (weight == null) { + return localizedType; + } + + return '$localizedType $weight'; + } } diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart index 78db52bdffc..e907f17eb9a 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/pending_proposal_card.dart @@ -1,17 +1,19 @@ -import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:catalyst_voices/common/formatters/date_formatter.dart'; import 'package:catalyst_voices/widgets/widgets.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_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; /// Displays a proposal in pending state on a card. class PendingProposalCard extends StatelessWidget { final AssetGenImage image; final PendingProposal proposal; + final bool showStatus; + final bool showLastUpdate; + final bool showComments; + final bool showSegments; final bool isFavorite; final ValueChanged? onFavoriteChanged; @@ -19,6 +21,10 @@ class PendingProposalCard extends StatelessWidget { super.key, required this.image, required this.proposal, + this.showStatus = true, + this.showLastUpdate = true, + this.showComments = true, + this.showSegments = true, this.isFavorite = false, this.onFavoriteChanged, }); @@ -29,7 +35,7 @@ class PendingProposalCard extends StatelessWidget { width: 326, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, borderRadius: BorderRadius.circular(12), ), child: Column( @@ -37,6 +43,7 @@ class PendingProposalCard extends StatelessWidget { children: [ _Header( image: image, + showStatus: showStatus, isFavorite: isFavorite, onFavoriteChanged: onFavoriteChanged, ), @@ -46,27 +53,31 @@ class PendingProposalCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _FundCategory( - fund: proposal.fund, + fund: proposal.campaignName, category: proposal.category, ), const SizedBox(height: 4), _Title(text: proposal.title), - const SizedBox(height: 4), - _LastUpdateDate(dateTime: proposal.lastUpdateDate), + if (showLastUpdate) ...[ + const SizedBox(height: 4), + _LastUpdateDate(dateTime: proposal.lastUpdateDate), + ], const SizedBox(height: 24), _FundsAndComments( funds: proposal.fundsRequested, commentsCount: proposal.commentsCount, + showComments: showComments, ), const SizedBox(height: 24), _Description(text: proposal.description), ], ), ), - _CompletedSegments( - completed: proposal.completedSegments, - total: proposal.totalSegments, - ), + if (showSegments) + _CompletedSegments( + completed: proposal.completedSegments, + total: proposal.totalSegments, + ), ], ), ); @@ -75,11 +86,13 @@ class PendingProposalCard extends StatelessWidget { class _Header extends StatelessWidget { final AssetGenImage image; + final bool showStatus; final bool isFavorite; final ValueChanged? onFavoriteChanged; const _Header({ required this.image, + required this.showStatus, required this.isFavorite, required this.onFavoriteChanged, }); @@ -112,18 +125,19 @@ class _Header extends StatelessWidget { ), ), ), - Positioned( - left: 12, - bottom: 12, - child: VoicesChip.rectangular( - padding: const EdgeInsets.fromLTRB(10, 6, 10, 4), - leading: VoicesAssets.icons.briefcase.buildIcon( - color: Theme.of(context).colorScheme.primary, + if (showStatus) + Positioned( + left: 12, + bottom: 12, + child: VoicesChip.rectangular( + padding: const EdgeInsets.fromLTRB(10, 6, 10, 4), + leading: VoicesAssets.icons.briefcase.buildIcon( + color: Theme.of(context).colorScheme.primary, + ), + content: Text(context.l10n.publishedProposal), + backgroundColor: Theme.of(context).colors.primary98, ), - content: Text(context.l10n.publishedProposal), - backgroundColor: Theme.of(context).colors.primary98, ), - ), ], ), ); @@ -191,12 +205,14 @@ class _LastUpdateDate extends StatelessWidget { } class _FundsAndComments extends StatelessWidget { - final Coin funds; + final String funds; final int commentsCount; + final bool showComments; const _FundsAndComments({ required this.funds, required this.commentsCount, + required this.showComments, }); @override @@ -213,7 +229,7 @@ class _FundsAndComments extends StatelessWidget { Column( children: [ Text( - CryptocurrencyFormatter.formatAmount(funds), + funds, style: Theme.of(context).textTheme.titleLarge, ), Text( @@ -222,19 +238,21 @@ class _FundsAndComments extends StatelessWidget { ), ], ), - VoicesChip.rectangular( - padding: const EdgeInsets.fromLTRB(8, 6, 12, 6), - leading: VoicesAssets.icons.checkCircle.buildIcon( - color: Theme.of(context).colorScheme.primary, - ), - content: Text( - context.l10n.noOfComments(commentsCount), - style: TextStyle( + if (showComments) + VoicesChip.rectangular( + padding: const EdgeInsets.fromLTRB(8, 6, 12, 6), + leading: VoicesAssets.icons.checkCircle.buildIcon( color: Theme.of(context).colorScheme.primary, ), + content: Text( + context.l10n.noOfComments(commentsCount), + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + backgroundColor: + Theme.of(context).colors.onSurfaceNeutralOpaqueLv1, ), - backgroundColor: Theme.of(context).colors.onSurfaceNeutralOpaqueLv1, - ), ], ), ); diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/proposal_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/proposal_card.dart new file mode 100644 index 00000000000..aa01e73a214 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/cards/proposal_card.dart @@ -0,0 +1,56 @@ +import 'package:catalyst_voices/widgets/cards/pending_proposal_card.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; + +/// A proposal card spanning proposals in different stages. +/// +/// Designed to work with as many cases as [ProposalViewModel] will support. +class ProposalCard extends StatelessWidget { + final AssetGenImage image; + final ProposalViewModel proposal; + final bool showStatus; + final bool showLastUpdate; + final bool showComments; + final bool showSegments; + final bool isFavorite; + final ValueChanged? onFavoriteChanged; + + const ProposalCard({ + super.key, + required this.image, + required this.proposal, + this.showStatus = true, + this.showLastUpdate = true, + this.showComments = true, + this.showSegments = true, + this.isFavorite = false, + this.onFavoriteChanged, + }); + + @override + Widget build(BuildContext context) { + final proposal = this.proposal; + + return switch (proposal) { + PendingProposal() => PendingProposalCard( + image: image, + proposal: proposal, + showStatus: showStatus, + showLastUpdate: showLastUpdate, + showComments: showComments, + showSegments: showSegments, + isFavorite: isFavorite, + onFavoriteChanged: onFavoriteChanged, + ), + FundedProposal() => FundedProposalCard( + image: image, + proposal: proposal, + isFavorite: isFavorite, + onFavoriteChanged: onFavoriteChanged, + ), + (_) => const Offstage(), + }; + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/common/affix_decorator.dart b/catalyst_voices/apps/voices/lib/widgets/common/affix_decorator.dart index b9ff8621fc9..3a28ad7bb0b 100644 --- a/catalyst_voices/apps/voices/lib/widgets/common/affix_decorator.dart +++ b/catalyst_voices/apps/voices/lib/widgets/common/affix_decorator.dart @@ -57,15 +57,20 @@ class AffixDecorator extends StatelessWidget { children: [ if (prefix != null) ...[ IconTheme( + key: const Key('DecoratorIconBefore'), data: iconTheme ?? IconTheme.of(context), child: prefix, ), SizedBox(width: gap), ], - Flexible(child: child), + Flexible( + key: const Key('DecoratorData'), + child: child, + ), if (suffix != null) ...[ SizedBox(width: gap), IconTheme( + key: const Key('DecoratorIconAfter'), data: iconTheme ?? IconTheme.of(context), child: suffix, ), diff --git a/catalyst_voices/apps/voices/lib/widgets/common/proposal_status_container.dart b/catalyst_voices/apps/voices/lib/widgets/common/proposal_status_container.dart index 472b7b8889c..4e70afb49af 100644 --- a/catalyst_voices/apps/voices/lib/widgets/common/proposal_status_container.dart +++ b/catalyst_voices/apps/voices/lib/widgets/common/proposal_status_container.dart @@ -82,42 +82,42 @@ extension _ProposalStatusExt on ProposalStatus { ProposalStatus.draft => _ProposalStatusContainerConfig( icon: VoicesAssets.icons.pencilAlt, iconColor: colors.iconsForeground, - text: context.l10n.proposalStatusDraft, + text: context.l10n.draft, textColor: colors.textPrimary, backgroundColor: colors.onSurfaceNeutralOpaqueLv1, ), ProposalStatus.inProgress => _ProposalStatusContainerConfig( icon: VoicesAssets.icons.annotation, iconColor: colors.iconsPrimary, - text: context.l10n.proposalStatusInProgress, + text: context.l10n.inProgress, textColor: colors.textPrimary, backgroundColor: colors.onSurfaceNeutralOpaqueLv1, ), ProposalStatus.private => _ProposalStatusContainerConfig( icon: VoicesAssets.icons.eyeOff, iconColor: colors.iconsForeground, - text: context.l10n.proposalStatusPrivate, + text: context.l10n.private, textColor: colors.textPrimary, backgroundColor: colors.onSurfaceNeutralOpaqueLv1, ), ProposalStatus.open => _ProposalStatusContainerConfig( icon: VoicesAssets.icons.checkCircle, iconColor: colors.iconsSuccess, - text: context.l10n.proposalStatusOpen, + text: context.l10n.open, textColor: colors.textPrimary, backgroundColor: colors.onSurfaceNeutralOpaqueLv1, ), ProposalStatus.live => _ProposalStatusContainerConfig( icon: VoicesAssets.icons.play, iconColor: colors.iconsForeground, - text: context.l10n.proposalStatusLive, + text: context.l10n.live.toUpperCase(), textColor: colors.textPrimary, backgroundColor: colors.successContainer, ), ProposalStatus.completed => _ProposalStatusContainerConfig( icon: VoicesAssets.icons.flag, iconColor: colors.iconsForeground, - text: context.l10n.proposalStatusCompleted, + text: context.l10n.completed, textColor: colors.textPrimary, backgroundColor: colors.onSurfaceNeutralOpaqueLv1, ), diff --git a/catalyst_voices/apps/voices/lib/widgets/containers/grey_out_container.dart b/catalyst_voices/apps/voices/lib/widgets/containers/grey_out_container.dart new file mode 100644 index 00000000000..4d95bbc1f91 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/containers/grey_out_container.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class GreyOutContainer extends StatelessWidget { + final bool greyOut; + final Widget child; + + const GreyOutContainer({ + super.key, + this.greyOut = true, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: greyOut, + child: Opacity( + opacity: greyOut ? 0.5 : 1, + child: child, + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/containers/workspace_tile_container.dart b/catalyst_voices/apps/voices/lib/widgets/containers/workspace_tile_container.dart index cdb6c4625b6..2ce92aa3af2 100644 --- a/catalyst_voices/apps/voices/lib/widgets/containers/workspace_tile_container.dart +++ b/catalyst_voices/apps/voices/lib/widgets/containers/workspace_tile_container.dart @@ -1,7 +1,7 @@ import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:flutter/material.dart'; -/// Opinionated container usual used inside space main body. +/// Opinionated container usually used inside space main body. class WorkspaceTileContainer extends StatelessWidget { final bool isSelected; final Widget content; diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/document_property.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/document_property.dart new file mode 100644 index 00000000000..672c9f2ab97 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/document_property.dart @@ -0,0 +1,34 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +class DocumentPropertyWidget extends StatelessWidget { + final BaseDocumentDefinition definition; + const DocumentPropertyWidget({super.key, required this.definition}); + + @override + Widget build(BuildContext context) { + return switch (definition) { + SegmentDefinition() => throw UnimplementedError(), + SectionDefinition() => throw UnimplementedError(), + SingleLineTextEntryDefinition() => throw UnimplementedError(), + SingleLineHttpsURLEntryDefinition() => throw UnimplementedError(), + MultiLineTextEntryDefinition() => throw UnimplementedError(), + MultiLineTextEntryMarkdownDefinition() => throw UnimplementedError(), + DropDownSingleSelectDefinition() => throw UnimplementedError(), + MultiSelectDefinition() => throw UnimplementedError(), + SingleLineTextEntryListDefinition() => throw UnimplementedError(), + MultiLineTextEntryListMarkdownDefinition() => throw UnimplementedError(), + SingleLineHttpsURLEntryListDefinition() => throw UnimplementedError(), + NestedQuestionsListDefinition() => throw UnimplementedError(), + NestedQuestionsDefinition() => throw UnimplementedError(), + SingleGroupedTagSelectorDefinition() => throw UnimplementedError(), + TagGroupDefinition() => throw UnimplementedError(), + TagSelectionDefinition() => throw UnimplementedError(), + TokenValueCardanoADADefinition() => throw UnimplementedError(), + DurationInMonthsDefinition() => throw UnimplementedError(), + YesNoChoiceDefinition() => throw UnimplementedError(), + AgreementConfirmationDefinition() => throw UnimplementedError(), + SPDXLicenceOrUrlDefinition() => throw UnimplementedError(), + }; + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/drawer/voices_drawer.dart b/catalyst_voices/apps/voices/lib/widgets/drawer/voices_drawer.dart index 730851c1b04..528a08c6cb4 100644 --- a/catalyst_voices/apps/voices/lib/widgets/drawer/voices_drawer.dart +++ b/catalyst_voices/apps/voices/lib/widgets/drawer/voices_drawer.dart @@ -116,13 +116,16 @@ class VoicesDrawerChooser extends StatelessWidget { children: [ if (leading != null) leading!, IconButton( + key: const ValueKey('DrawerChooserPreviousButton'), onPressed: _selectedIndex > 0 ? _onSelectPrevious : null, icon: VoicesAssets.icons.chevronLeft.buildIcon(size: 20), ), for (final item in items) MouseRegion( + key: ValueKey('DrawerChooser$item'), cursor: SystemMouseCursors.click, child: GestureDetector( + key: const ValueKey('DrawerChooserItem'), behavior: HitTestBehavior.opaque, onTap: () => onSelected(item), child: itemBuilder( @@ -133,6 +136,7 @@ class VoicesDrawerChooser extends StatelessWidget { ), ), IconButton( + key: const ValueKey('DrawerChooserNextButton'), onPressed: _selectedIndex < (items.length - 1) ? _onSelectNext : null, icon: VoicesAssets.icons.chevronRight.buildIcon(size: 20), diff --git a/catalyst_voices/apps/voices/lib/widgets/drawer/voices_drawer_space_chooser.dart b/catalyst_voices/apps/voices/lib/widgets/drawer/voices_drawer_space_chooser.dart index 8bc36c01470..c67ec723a2e 100644 --- a/catalyst_voices/apps/voices/lib/widgets/drawer/voices_drawer_space_chooser.dart +++ b/catalyst_voices/apps/voices/lib/widgets/drawer/voices_drawer_space_chooser.dart @@ -2,8 +2,10 @@ import 'package:catalyst_voices/widgets/avatars/space_avatar.dart'; import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart'; import 'package:catalyst_voices/widgets/drawer/voices_drawer.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class VoicesDrawerSpaceChooser extends StatelessWidget { final Space currentSpace; @@ -21,15 +23,21 @@ class VoicesDrawerSpaceChooser extends StatelessWidget { @override Widget build(BuildContext context) { - return VoicesDrawerChooser( - items: Space.values, - selectedItem: currentSpace, - onSelected: onChanged, - itemBuilder: _itemBuilder, - leading: VoicesIconButton( - onTap: onOverallTap, - child: VoicesAssets.icons.allSpacesMenu.buildIcon(size: 20), - ), + return BlocSelector>( + selector: (state) => state.spaces, + builder: (context, spaces) { + return VoicesDrawerChooser( + items: spaces, + selectedItem: currentSpace, + onSelected: onChanged, + itemBuilder: _itemBuilder, + leading: VoicesIconButton( + key: const ValueKey('DrawerChooserAllSpacesButton'), + onTap: onOverallTap, + child: VoicesAssets.icons.allSpacesMenu.buildIcon(size: 20), + ), + ); + }, ); } diff --git a/catalyst_voices/apps/voices/lib/widgets/empty_state/empty_state.dart b/catalyst_voices/apps/voices/lib/widgets/empty_state/empty_state.dart new file mode 100644 index 00000000000..3b4d2936c6a --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/empty_state/empty_state.dart @@ -0,0 +1,76 @@ +import 'package:catalyst_voices/widgets/images/voices_image_scheme.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:flutter/material.dart'; + +class EmptyState extends StatelessWidget { + final String? title; + final String? description; + final Widget? image; + final Widget? imageBackground; + + const EmptyState({ + super.key, + this.title, + this.description, + this.image, + this.imageBackground, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 64), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + image ?? + VoicesImagesScheme( + image: CatalystSvgPicture.asset( + VoicesAssets.images.noProposalForeground.path, + ), + background: imageBackground ?? + Container( + height: 180, + decoration: BoxDecoration( + color: theme.colors.onSurfaceNeutral08, + shape: BoxShape.circle, + ), + ), + ), + const SizedBox(height: 24), + SizedBox( + width: 430, + child: Column( + children: [ + Text( + _buildTitle(context), + style: textTheme.titleMedium + ?.copyWith(color: theme.colors.textOnPrimaryLevel1), + ), + const SizedBox(height: 8), + Text( + _buildDescription(context), + style: textTheme.bodyMedium + ?.copyWith(color: theme.colors.textOnPrimaryLevel1), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + ); + } + + String _buildDescription(BuildContext context) { + return description ?? context.l10n.noProposalStateDescription; + } + + String _buildTitle(BuildContext context) { + return title ?? context.l10n.noProposalStateTitle; + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/headers/brand_header.dart b/catalyst_voices/apps/voices/lib/widgets/headers/brand_header.dart index 5cd46afd663..3e5c0f49ba5 100644 --- a/catalyst_voices/apps/voices/lib/widgets/headers/brand_header.dart +++ b/catalyst_voices/apps/voices/lib/widgets/headers/brand_header.dart @@ -19,6 +19,7 @@ class BrandHeader extends StatelessWidget { children: [ Theme.of(context).brandAssets.brand.logo(context).buildPicture(), IconButton( + key: const ValueKey('MenuCloseButton'), onPressed: Navigator.of(context).pop, icon: VoicesAssets.icons.x.buildIcon(size: 22), ), diff --git a/catalyst_voices/apps/voices/lib/widgets/headers/segment_header.dart b/catalyst_voices/apps/voices/lib/widgets/headers/segment_header.dart index e1892893135..ef5936d8747 100644 --- a/catalyst_voices/apps/voices/lib/widgets/headers/segment_header.dart +++ b/catalyst_voices/apps/voices/lib/widgets/headers/segment_header.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; class SegmentHeader extends StatelessWidget { final String name; final Widget? leading; + final EdgeInsets padding; final List actions; final bool isSelected; final VoidCallback? onTap; @@ -12,6 +13,7 @@ class SegmentHeader extends StatelessWidget { super.key, required this.name, this.leading, + this.padding = const EdgeInsets.symmetric(horizontal: 24, vertical: 12), this.actions = const [], this.isSelected = false, this.onTap, @@ -57,7 +59,7 @@ class SegmentHeader extends StatelessWidget { decoration: BoxDecoration( color: backgroundColor.resolve(_states), ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + padding: padding, child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/catalyst_voices/apps/voices/lib/widgets/images/voices_image_scheme.dart b/catalyst_voices/apps/voices/lib/widgets/images/voices_image_scheme.dart new file mode 100644 index 00000000000..ddf0bf9bce3 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/images/voices_image_scheme.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class VoicesImagesScheme extends StatelessWidget { + final Widget image; + final Widget? background; + + const VoicesImagesScheme({ + super.key, + required this.image, + required this.background, + }); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + if (background != null) background!, + image, + ], + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/indicators/voices_indicator.dart b/catalyst_voices/apps/voices/lib/widgets/indicators/voices_indicator.dart index 6d3e5add6c5..e1dbcacaf02 100644 --- a/catalyst_voices/apps/voices/lib/widgets/indicators/voices_indicator.dart +++ b/catalyst_voices/apps/voices/lib/widgets/indicators/voices_indicator.dart @@ -52,7 +52,7 @@ class VoicesIndicator extends StatelessWidget { size: 20, ), const SizedBox(width: 10), - Expanded( + Flexible( child: DefaultTextStyle.merge( style: (theme.textTheme.titleSmall ?? const TextStyle()).copyWith( height: 1, diff --git a/catalyst_voices/apps/voices/lib/widgets/menu/voices_modal_menu.dart b/catalyst_voices/apps/voices/lib/widgets/menu/voices_modal_menu.dart new file mode 100644 index 00000000000..61b24038779 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/menu/voices_modal_menu.dart @@ -0,0 +1,179 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; + +class VoicesModalMenu extends StatelessWidget { + final String? selectedId; + final List menuItems; + final ValueChanged? onTap; + + const VoicesModalMenu({ + super.key, + this.selectedId, + required this.menuItems, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final onTap = this.onTap; + + return Column( + mainAxisSize: MainAxisSize.min, + children: menuItems + .map( + (item) { + return _VoicesModalMenuItemTile( + key: ValueKey('VoicesModalMenu[${item.id}]Key'), + label: item.label, + isSelected: selectedId == item.id, + isEnabled: item.isEnabled, + onTap: onTap != null ? () => onTap(item.id) : null, + ); + }, + ) + .separatedBy(const SizedBox(height: 8)) + .toList(), + ); + } +} + +class _VoicesModalMenuItemTile extends StatefulWidget { + final String label; + final bool isSelected; + final bool isEnabled; + final VoidCallback? onTap; + + const _VoicesModalMenuItemTile({ + required super.key, + required this.label, + required this.isSelected, + required this.isEnabled, + this.onTap, + }); + + @override + State<_VoicesModalMenuItemTile> createState() { + return _VoicesModalMenuItemTileState(); + } +} + +class _VoicesModalMenuItemTileState extends State<_VoicesModalMenuItemTile> { + late _BackgroundColor _backgroundColor; + late _ForegroundColor _foregroundColor; + late _LabelTextStyle _labelTextStyle; + late _BorderColor _border; + + Set get _states => { + if (!widget.isEnabled) WidgetState.disabled, + if (widget.isSelected) WidgetState.selected, + }; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final theme = Theme.of(context); + + _backgroundColor = _BackgroundColor(theme.colorScheme.brightness); + _foregroundColor = _ForegroundColor(theme.colors); + _labelTextStyle = _LabelTextStyle(theme.textTheme); + _border = _BorderColor(theme.colorScheme.brightness); + } + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: widget.isEnabled ? widget.onTap : null, + borderRadius: BorderRadius.circular(8), + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + constraints: const BoxConstraints(minWidth: 320), + decoration: BoxDecoration( + color: _backgroundColor.resolve(_states), + border: _border.resolve(_states), + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16) + .add(const EdgeInsets.only(bottom: 2)), + child: Text( + widget.label, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: _labelTextStyle + .resolve(_states) + .copyWith(color: _foregroundColor.resolve(_states)), + ), + ), + ); + } +} + +class _BackgroundColor extends WidgetStateProperty { + final Brightness _brightness; + + _BackgroundColor(this._brightness); + + @override + Color? resolve(Set states) { + if (states.contains(WidgetState.selected)) { + // TODO(damian-molinski): Those colors are not using properties. + // TODO(damian-molinski): Dark/Transparent/On primary surface P40 016 + // TODO(damian-molinski): Light/Transparent/On surface P40 08 + return switch (_brightness) { + Brightness.dark => const Color(0x29123cd3), + Brightness.light => const Color(0x1f123cd3), + }; + } + + return null; + } +} + +class _ForegroundColor extends WidgetStateProperty { + final VoicesColorScheme _colors; + + _ForegroundColor(this._colors); + + @override + Color? resolve(Set states) { + if (states.contains(WidgetState.disabled)) { + return _colors.textDisabled; + } + + return _colors.textOnPrimaryLevel1; + } +} + +class _LabelTextStyle extends WidgetStateProperty { + final TextTheme _textTheme; + + _LabelTextStyle(this._textTheme); + + @override + TextStyle resolve(Set states) { + if (states.contains(WidgetState.selected)) { + return _textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.bold); + } + + return _textTheme.bodyLarge!; + } +} + +class _BorderColor extends WidgetStateProperty { + final Brightness _brightness; + + _BorderColor(this._brightness); + + @override + BoxBorder resolve(Set states) { + // TODO(damian-molinski): Those colors are not using properties. + // TODO(damian-molinski): Elevations/On surface/Neutral/Transparent/on surface N10 08 + // TODO(damian-molinski): Elevations/On surface/Neutral/Transparent/on surface N10 08 + return switch (_brightness) { + Brightness.dark => Border.all(color: const Color(0x1fbfc8d9)), + Brightness.light => Border.all(color: const Color(0x14212a3d)), + }; + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/menu/voices_node_menu.dart b/catalyst_voices/apps/voices/lib/widgets/menu/voices_node_menu.dart index 8a365d2b104..9498840bb8b 100644 --- a/catalyst_voices/apps/voices/lib/widgets/menu/voices_node_menu.dart +++ b/catalyst_voices/apps/voices/lib/widgets/menu/voices_node_menu.dart @@ -6,7 +6,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; final class VoicesNodeMenuItem extends Equatable { - final int id; + final String id; final String label; final bool isEnabled; @@ -28,8 +28,8 @@ class VoicesNodeMenu extends StatelessWidget { final String name; final Widget? icon; final VoidCallback? onHeaderTap; - final int? selectedItemId; - final ValueChanged onItemTap; + final String? selectedItemId; + final ValueChanged onItemTap; final List items; final bool isExpandable; final bool isExpanded; diff --git a/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_align_title_header.dart b/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_align_title_header.dart new file mode 100644 index 00000000000..3de74608216 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_align_title_header.dart @@ -0,0 +1,54 @@ +import 'package:catalyst_voices/widgets/buttons/voices_buttons.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesAlignTitleHeader extends StatelessWidget { + final String title; + final TextStyle? titleStyle; + final EdgeInsets? padding; + + const VoicesAlignTitleHeader({ + super.key, + required this.title, + this.padding, + this.titleStyle, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding ?? const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: titleStyle ?? Theme.of(context).textTheme.titleMedium, + ), + const _CloseButton(), + ], + ), + ); + } +} + +class _CloseButton extends StatelessWidget { + const _CloseButton(); + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colors; + + final style = IconButton.styleFrom( + backgroundColor: colors.iconsBackground, + foregroundColor: colors.iconsForeground, + ); + + return IconButtonTheme( + data: IconButtonThemeData(style: style), + child: XButton( + onTap: () async => Navigator.of(context).maybePop(), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_details_dialog.dart b/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_details_dialog.dart new file mode 100644 index 00000000000..f39cc2ee7bd --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_details_dialog.dart @@ -0,0 +1,39 @@ +import 'package:catalyst_voices/widgets/modals/voices_desktop_dialog.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesDetailsDialog extends StatelessWidget { + final Widget header; + final Widget body; + final BoxConstraints constraints; + final Color? backgroundColor; + + const VoicesDetailsDialog({ + super.key, + required this.header, + required this.body, + this.constraints = const BoxConstraints( + minWidth: 600, + maxWidth: 900, + minHeight: double.infinity, + ), + this.backgroundColor, + }); + + @override + Widget build(BuildContext context) { + return VoicesSinglePaneDialog( + showClose: false, + showBorder: true, + constraints: constraints, + backgroundColor: backgroundColor ?? + Theme.of(context).colors.elevationsOnSurfaceNeutralLv0, + child: Column( + children: [ + header, + Expanded(child: body), + ], + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_details_dialog_header.dart b/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_details_dialog_header.dart new file mode 100644 index 00000000000..ab13a9515fe --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/modals/details/voices_details_dialog_header.dart @@ -0,0 +1,161 @@ +import 'package:catalyst_voices/widgets/buttons/voices_buttons.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesDetailsDialogHeader extends StatelessWidget { + final String title; + final String titleLabel; + + const VoicesDetailsDialogHeader({ + super.key, + required this.title, + required this.titleLabel, + }); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + // Later this upper constraints may change + constraints: const BoxConstraints(minHeight: 166, maxHeight: 166), + child: Stack( + children: [ + const Positioned.fill(child: _Background()), + _Foreground(titleLabel: titleLabel, title: title), + ], + ), + ); + } +} + +class _Background extends StatelessWidget { + const _Background(); + + @override + Widget build(BuildContext context) { + final asset = VoicesAssets.images.comingSoonBkg; + return DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black.withOpacity(0), + Colors.black.withOpacity(0.4), + Colors.black.withOpacity(0.4), + ], + stops: const [ + 0, + 0.7452, + 1, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + position: DecorationPosition.foreground, + child: CatalystImage.asset( + asset.path, + fit: BoxFit.cover, + ), + ); + } +} + +class _Foreground extends StatelessWidget { + final String titleLabel; + final String title; + + const _Foreground({ + required this.titleLabel, + required this.title, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(12).add(const EdgeInsets.only(bottom: 4)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Align( + alignment: Alignment.topLeft, + child: _CloseButton(), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _TitleLabelText(titleLabel), + _TitleText(title), + ], + ), + ), + ], + ), + ); + } +} + +class _TitleLabelText extends StatelessWidget { + final String data; + + const _TitleLabelText(this.data); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + return Text( + data, + maxLines: 1, + overflow: TextOverflow.ellipsis, + // TODO(damian-molinski): Always using white colors without token. + // Colors/sys color neutral md ref/N100 + style: textTheme.titleSmall?.copyWith(color: Colors.white), + ); + } +} + +class _TitleText extends StatelessWidget { + final String data; + + const _TitleText(this.data); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + return Text( + data, + maxLines: 1, + overflow: TextOverflow.ellipsis, + // TODO(damian-molinski): Always using white colors without token. + // Colors/sys color neutral md ref/N100 + style: textTheme.displayMedium?.copyWith(color: Colors.white), + ); + } +} + +class _CloseButton extends StatelessWidget { + const _CloseButton(); + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colors; + + final style = IconButton.styleFrom( + backgroundColor: colors.iconsBackground, + foregroundColor: colors.iconsForeground, + ); + + return IconButtonTheme( + data: IconButtonThemeData(style: style), + child: XButton( + onTap: () async => Navigator.of(context).maybePop(), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/modals/voices_desktop_dialog.dart b/catalyst_voices/apps/voices/lib/widgets/modals/voices_desktop_dialog.dart index c3677d45f03..df3a5af8c7a 100644 --- a/catalyst_voices/apps/voices/lib/widgets/modals/voices_desktop_dialog.dart +++ b/catalyst_voices/apps/voices/lib/widgets/modals/voices_desktop_dialog.dart @@ -12,7 +12,8 @@ class VoicesSinglePaneDialog extends StatelessWidget { final BoxConstraints constraints; final Color? backgroundColor; final bool showBorder; - final VoidCallback? onCancel; + final bool showClose; + final Alignment closeAlignment; final Widget child; const VoicesSinglePaneDialog({ @@ -20,7 +21,8 @@ class VoicesSinglePaneDialog extends StatelessWidget { this.constraints = const BoxConstraints(minWidth: 900, minHeight: 600), this.backgroundColor, this.showBorder = false, - this.onCancel, + this.showClose = true, + this.closeAlignment = Alignment.topRight, required this.child, }); @@ -33,8 +35,12 @@ class VoicesSinglePaneDialog extends StatelessWidget { child: Stack( children: [ child, - _DialogCloseButton( - onCancel: onCancel, + Offstage( + offstage: !showClose, + child: _CloseButtonPosition( + alignment: closeAlignment, + child: const _CloseButton(), + ), ), ], ), @@ -86,7 +92,10 @@ class VoicesTwoPaneDialog extends StatelessWidget { ), ], ), - if (showCloseButton) const _DialogCloseButton(), + Offstage( + offstage: !showCloseButton, + child: const _CloseButtonPosition(child: _CloseButton()), + ), ], ), ); @@ -128,11 +137,13 @@ class _VoicesDesktopDialog extends StatelessWidget { } } -class _DialogCloseButton extends StatelessWidget { - final VoidCallback? onCancel; +class _CloseButtonPosition extends StatelessWidget { + final AlignmentGeometry alignment; + final Widget child; - const _DialogCloseButton({ - this.onCancel, + const _CloseButtonPosition({ + this.alignment = Alignment.topRight, + required this.child, }); @override @@ -142,16 +153,25 @@ class _DialogCloseButton extends StatelessWidget { ); return Align( - alignment: Alignment.topRight, + key: const Key('DialogCloseButton'), + alignment: alignment, child: IconButtonTheme( data: const IconButtonThemeData(style: buttonStyle), - child: XButton( - onTap: () { - onCancel?.call(); - unawaited(Navigator.of(context).maybePop()); - }, - ), + child: child, ), ); } } + +class _CloseButton extends StatelessWidget { + const _CloseButton(); + + @override + Widget build(BuildContext context) { + return XButton( + onTap: () { + unawaited(Navigator.of(context).maybePop()); + }, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/modals/voices_upload_file_dialog.dart b/catalyst_voices/apps/voices/lib/widgets/modals/voices_upload_file_dialog.dart index 63811e2aae1..3d6ccc2073b 100644 --- a/catalyst_voices/apps/voices/lib/widgets/modals/voices_upload_file_dialog.dart +++ b/catalyst_voices/apps/voices/lib/widgets/modals/voices_upload_file_dialog.dart @@ -14,13 +14,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; +typedef OnVoicesFileUploaded = Future Function(VoicesFile value); + class VoicesUploadFileDialog extends StatefulWidget { final String title; final String itemNameToUpload; final String? info; final List? allowedExtensions; - final Future Function(VoicesFile value)? onUpload; - final VoidCallback? onCancel; + final OnVoicesFileUploaded? onUpload; const VoicesUploadFileDialog({ super.key, @@ -29,7 +30,6 @@ class VoicesUploadFileDialog extends StatefulWidget { this.info, this.allowedExtensions, this.onUpload, - this.onCancel, }); static Future show( @@ -38,8 +38,7 @@ class VoicesUploadFileDialog extends StatefulWidget { String? itemNameToUpload, String? info, List? allowedExtensions, - Future Function(VoicesFile value)? onUpload, - VoidCallback? onCancel, + OnVoicesFileUploaded? onUpload, }) { return VoicesDialog.show( context: context, @@ -50,7 +49,6 @@ class VoicesUploadFileDialog extends StatefulWidget { info: info, allowedExtensions: allowedExtensions, onUpload: onUpload, - onCancel: onCancel, ); }, ); @@ -68,7 +66,6 @@ class _VoicesUploadFileDialogState extends State { Widget build(BuildContext context) { return VoicesSinglePaneDialog( constraints: const BoxConstraints(maxWidth: 600, maxHeight: 450), - onCancel: widget.onCancel, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -100,7 +97,6 @@ class _VoicesUploadFileDialogState extends State { }); await widget.onUpload?.call(file); }, - onCancel: widget.onCancel, ), ], ), @@ -111,13 +107,11 @@ class _VoicesUploadFileDialogState extends State { class _Buttons extends StatefulWidget { final VoicesFile? selectedFile; - final Future Function(VoicesFile value)? onUpload; - final VoidCallback? onCancel; + final OnVoicesFileUploaded? onUpload; const _Buttons({ this.selectedFile, this.onUpload, - this.onCancel, }); @override @@ -134,7 +128,6 @@ class _ButtonsState extends State<_Buttons> { Expanded( child: VoicesOutlinedButton( onTap: () { - widget.onCancel?.call(); Navigator.of(context).pop(); }, child: Text(context.l10n.cancelButtonText), diff --git a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_controller.dart b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_controller.dart index f3dc14e24be..88380292998 100644 --- a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_controller.dart +++ b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_controller.dart @@ -9,36 +9,27 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; final class SectionsControllerState extends Equatable { final List
sections; - final Set openedSections; + final Set openedSections; final SectionStepId? activeStepId; final Set editStepsIds; - final GuidanceType? activeGuidance; const SectionsControllerState({ this.sections = const [], this.openedSections = const {}, this.activeStepId, this.editStepsIds = const {}, - this.activeGuidance, }); - int? get activeSectionId => activeStepId?.sectionId; + String? get activeSectionId => activeStepId?.sectionId; - int? get activeStep => activeStepId?.stepId; - - List? get activeStepGuidances { - final activeStepId = this.activeStepId; - if (activeStepId == null) { - return null; - } else { - return sections[activeStepId.sectionId] - .steps[activeStepId.stepId] - .guidances; - } - } + String? get activeStep => activeStepId?.stepId; bool get allSegmentsClosed => openedSections.isEmpty; + bool isEditing(SectionStepId stepId) { + return editStepsIds.contains(stepId); + } + List get listItems { final openedSections = {...this.openedSections}; @@ -73,17 +64,15 @@ final class SectionsControllerState extends Equatable { SectionsControllerState copyWith({ List
? sections, - Set? openedSections, + Set? openedSections, Optional? activeStepId, Set? editStepsIds, - Optional? activeGuidance, }) { return SectionsControllerState( sections: sections ?? this.sections, openedSections: openedSections ?? this.openedSections, activeStepId: activeStepId.dataOr(this.activeStepId), editStepsIds: editStepsIds ?? this.editStepsIds, - activeGuidance: activeGuidance?.dataOr(this.activeGuidance), ); } @@ -93,7 +82,6 @@ final class SectionsControllerState extends Equatable { openedSections, activeStepId, editStepsIds, - activeGuidance, ]; } @@ -113,7 +101,7 @@ final class SectionsController extends ValueNotifier { _itemsScrollController = null; } - void toggleSection(int id) { + void toggleSection(String id) { final openedSections = {...value.openedSections}; final allSegmentsClosed = value.allSegmentsClosed; final shouldOpen = !openedSections.contains(id); @@ -159,7 +147,7 @@ final class SectionsController extends ValueNotifier { unawaited(_scrollToSectionStep(id)); } - void focusSection(int id) { + void focusSection(String id) { unawaited(_scrollToSection(id)); } @@ -183,17 +171,13 @@ final class SectionsController extends ValueNotifier { ); } - void setActiveGuidance(GuidanceType? type) { - value = value.copyWith(activeGuidance: Optional(type)); - } - @override void dispose() { detachItemsScrollController(); super.dispose(); } - Future _scrollToSection(int id) async { + Future _scrollToSection(String id) async { final index = value.listItems.indexWhere((e) => e is Section && e.id == id); if (index == -1) { return; diff --git a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_list_view.dart b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_list_view.dart index 5d53389bc69..836f91293f4 100644 --- a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_list_view.dart +++ b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_list_view.dart @@ -32,38 +32,46 @@ class SectionsListView @override Widget build(BuildContext context) { - return ScrollablePositionedList.separated( - padding: padding?.resolve(Directionality.of(context)), - itemScrollController: itemScrollController, - itemBuilder: (context, index) { - final item = items[index]; + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + // Disables the iOS like overscroll behavior to avoid jumping UI. + // TODO(dtscalac): remove the workaround when + // https://github.com/google/flutter.widgets/issues/276 is fixed + physics: const ClampingScrollPhysics(), + ), + child: ScrollablePositionedList.separated( + padding: padding?.resolve(Directionality.of(context)), + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + final item = items[index]; - if (item is T) { - return KeyedSubtree( - key: item.buildKey(), - child: headerBuilder(context, item), - ); - } + if (item is T) { + return KeyedSubtree( + key: item.buildKey(), + child: headerBuilder(context, item), + ); + } - if (item is T2) { - return KeyedSubtree( - key: item.buildKey(), - child: stepBuilder(context, item), - ); - } + if (item is T2) { + return KeyedSubtree( + key: item.buildKey(), + child: stepBuilder(context, item), + ); + } - throw ArgumentError('Unknown section item type[${item.runtimeType}]'); - }, - separatorBuilder: (context, index) { - final item = items[index]; + throw ArgumentError('Unknown section item type[${item.runtimeType}]'); + }, + separatorBuilder: (context, index) { + final item = items[index]; - if (item is SectionStep) { - return const SizedBox(height: 12); - } + if (item is SectionStep) { + return const SizedBox(height: 12); + } - return const SizedBox(height: 24); - }, - itemCount: items.length, + return const SizedBox(height: 24); + }, + itemCount: items.length, + ), ); } } diff --git a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_menu.dart b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_menu.dart index d0435ea1206..e8d23e6bb6f 100644 --- a/catalyst_voices/apps/voices/lib/widgets/navigation/sections_menu.dart +++ b/catalyst_voices/apps/voices/lib/widgets/navigation/sections_menu.dart @@ -31,9 +31,9 @@ class SectionsMenuListener extends StatelessWidget { class SectionsMenu extends StatelessWidget { final List
sections; - final Set openedSections; + final Set openedSections; final SectionStepId? selectedStep; - final ValueChanged onSectionTap; + final ValueChanged onSectionTap; final ValueChanged onStepSelected; const SectionsMenu({ diff --git a/catalyst_voices/apps/voices/lib/widgets/pickers/voices_calendar_picker.dart b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_calendar_picker.dart new file mode 100644 index 00000000000..591253add14 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_calendar_picker.dart @@ -0,0 +1,93 @@ +import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; + +class VoicesCalendarDatePicker extends StatefulWidget { + final ValueChanged onDateSelected; + final VoidCallback cancelEvent; + final DateTime initialDate; + final DateTime firstDate; + final DateTime lastDate; + + factory VoicesCalendarDatePicker({ + Key? key, + required ValueChanged onDateSelected, + required VoidCallback cancelEvent, + DateTime? initialDate, + DateTime? firstDate, + DateTime? lastDate, + }) { + final now = DateTime.now(); + return VoicesCalendarDatePicker._( + key: key, + onDateSelected: onDateSelected, + initialDate: initialDate ?? now, + firstDate: firstDate ?? now, + lastDate: lastDate ?? DateTime(now.year + 1, now.month, now.day), + cancelEvent: cancelEvent, + ); + } + + const VoicesCalendarDatePicker._({ + super.key, + required this.onDateSelected, + required this.initialDate, + required this.firstDate, + required this.lastDate, + required this.cancelEvent, + }); + + @override + State createState() => + _VoicesCalendarDatePickerState(); +} + +class _VoicesCalendarDatePickerState extends State { + DateTime selectedDate = DateTime.now(); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 450, + child: Material( + clipBehavior: Clip.hardEdge, + color: Colors.transparent, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + CalendarDatePicker( + initialDate: widget.initialDate, + firstDate: widget.firstDate, + lastDate: widget.lastDate, + onDateChanged: (val) { + selectedDate = val; + }, + ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + VoicesTextButton( + onTap: widget.cancelEvent, + child: Text(context.l10n.cancelButtonText), + ), + VoicesTextButton( + onTap: () => widget.onDateSelected(selectedDate), + child: Text(context.l10n.ok.toUpperCase()), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart new file mode 100644 index 00000000000..36c79ccaf4e --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart @@ -0,0 +1,141 @@ +import 'package:catalyst_voices/common/ext/time_of_day_ext.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesTimePicker extends StatefulWidget { + final ValueChanged onTap; + final TimeOfDay? selectedTime; + final String? timeZone; + + const VoicesTimePicker({ + super.key, + required this.onTap, + this.selectedTime, + this.timeZone, + }); + + @override + State createState() => _VoicesTimePickerState(); +} + +class _VoicesTimePickerState extends State { + late final ScrollController _scrollController; + late final List _timeList; + + final double itemExtent = 40; + + @override + void initState() { + super.initState(); + + _timeList = _generateTimeList(); + _scrollController = ScrollController(); + + final initialSelection = widget.selectedTime; + if (initialSelection != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToTime(initialSelection); + }); + } + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + clipBehavior: Clip.hardEdge, + child: Container( + height: 350, + width: 150, + decoration: BoxDecoration( + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey, + borderRadius: BorderRadius.circular(20), + ), + child: ListView.builder( + controller: _scrollController, + itemExtent: itemExtent, + itemCount: _timeList.length, + itemBuilder: (context, index) { + final timeOfDay = _timeList[index]; + + return _TimeText( + key: ValueKey(timeOfDay.formatted), + value: timeOfDay, + onTap: widget.onTap, + isSelected: timeOfDay == widget.selectedTime, + timeZone: widget.timeZone, + ); + }, + ), + ), + ); + } + + void _scrollToTime(TimeOfDay value) { + final index = _timeList.indexWhere((e) => e == widget.selectedTime); + + if (index != -1) { + _scrollController.jumpTo(index * itemExtent); + } + } + + List _generateTimeList() { + return [ + for (var hour = 0; hour < 24; hour++) + for (final minute in [0, 30]) TimeOfDay(hour: hour, minute: minute), + ]; + } +} + +class _TimeText extends StatelessWidget { + final ValueChanged onTap; + final TimeOfDay value; + final bool isSelected; + final String? timeZone; + + const _TimeText({ + super.key, + required this.value, + required this.onTap, + this.isSelected = false, + this.timeZone, + }); + + @override + Widget build(BuildContext context) { + final timeZone = this.timeZone; + + return Material( + clipBehavior: Clip.hardEdge, + type: MaterialType.transparency, + child: InkWell( + onTap: () => onTap(value), + child: ColoredBox( + color: !isSelected + ? Colors.transparent + : Theme.of(context).colors.onSurfaceNeutral08!, + child: Padding( + key: key, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + value.formatted, + style: Theme.of(context).textTheme.bodyLarge, + ), + if (isSelected && timeZone != null) Text(timeZone), + ], + ), + ), + ), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart index 848796f5364..a8b4aeab9cb 100644 --- a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart @@ -511,11 +511,13 @@ class _TopBar extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - Text( - title, - style: Theme.of(context).textTheme.titleMedium, + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), ), - const Spacer(), + const SizedBox(width: 16), VoicesTextButton( onTap: onToggleEditMode, child: Text( diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart new file mode 100644 index 00000000000..441c961a2e9 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_field.dart @@ -0,0 +1,223 @@ +import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_date_time_text_field.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; + +final class VoicesDateFieldController extends ValueNotifier { + VoicesDateFieldController([super._value]); +} + +class VoicesDateField extends StatefulWidget { + final VoicesDateFieldController? controller; + final ValueChanged? onChanged; + final ValueChanged? onFieldSubmitted; + final VoidCallback? onCalendarTap; + final bool dimBorder; + final BorderRadius borderRadius; + + const VoicesDateField({ + super.key, + this.controller, + this.onChanged, + required this.onFieldSubmitted, + this.onCalendarTap, + this.dimBorder = false, + this.borderRadius = const BorderRadius.all(Radius.circular(16)), + }); + + @override + State createState() => _VoicesDateFieldState(); +} + +class _VoicesDateFieldState extends State { + late final TextEditingController _textEditingController; + late final MaskTextInputFormatter dateFormatter; + + VoicesDateFieldController? _controller; + + VoicesDateFieldController get _effectiveController { + return widget.controller ?? (_controller ??= VoicesDateFieldController()); + } + + String get _pattern => 'dd/MM/yyyy'; + + @override + void initState() { + final initialDate = _effectiveController.value; + final initialText = _convertDateToText(initialDate); + + _textEditingController = TextEditingController(text: initialText); + _textEditingController.addListener(_handleTextChanged); + + _effectiveController.addListener(_handleDateChanged); + + dateFormatter = MaskTextInputFormatter( + mask: _pattern, + filter: { + 'd': RegExp('[0-9]'), + 'M': RegExp('[0-9]'), + 'y': RegExp('[0-9]'), + }, + type: MaskAutoCompletionType.eager, + ); + + super.initState(); + } + + @override + void didUpdateWidget(covariant VoicesDateField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + (oldWidget.controller ?? _controller)?.removeListener(_handleDateChanged); + (widget.controller ?? _controller)?.addListener(_handleDateChanged); + + final date = _effectiveController.value; + _textEditingController.text = _convertDateToText(date); + } + } + + @override + void dispose() { + _textEditingController.dispose(); + + _controller?.dispose(); + _controller = null; + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final onChanged = widget.onChanged; + final onFieldSubmitted = widget.onFieldSubmitted; + + return VoicesDateTimeTextField( + controller: _textEditingController, + onChanged: onChanged != null + ? (value) => onChanged(_convertTextToDate(value)) + : null, + validator: _validate, + hintText: _pattern.toUpperCase(), + onFieldSubmitted: onFieldSubmitted != null + ? (value) => onFieldSubmitted(_convertTextToDate(value)) + : null, + suffixIcon: ExcludeFocus( + child: VoicesIconButton( + onTap: widget.onCalendarTap, + child: VoicesAssets.icons.calendar.buildIcon(), + ), + ), + borderRadius: widget.borderRadius, + dimBorder: widget.dimBorder, + inputFormatters: [dateFormatter], + ); + } + + void _handleTextChanged() { + final date = _convertTextToDate(_textEditingController.text); + if (_effectiveController.value != date) { + _effectiveController.value = date; + } + } + + void _handleDateChanged() { + final text = _convertDateToText(_effectiveController.value); + if (_textEditingController.text != text) { + _textEditingController.text = text; + } + } + + String _convertDateToText(DateTime? value) { + if (value == null) { + return ''; + } + + final day = value.day.toString().padLeft(2, '0'); + final month = value.month.toString().padLeft(2, '0'); + final year = value.year; + + return '$day/$month/$year'; + } + + DateTime? _convertTextToDate(String value) { + if (value.isEmpty) return null; + + final parts = value.split('/'); + if (parts.length != 3) return null; + + try { + final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; + final formatDt = DateTime.parse(reformatted); + + if (formatDt.month < 1 || formatDt.month > 12) return null; + if (formatDt.day < 1 || formatDt.day > 31) return null; + if (formatDt.year < 1900 || formatDt.year > 2100) return null; + + return formatDt; + } catch (e) { + return null; + } + } + + VoicesTextFieldValidationResult _validate(String value) { + final today = DateTime.now(); + final maxDate = DateTime(today.year + 1, today.month, today.day); + + if (value.isEmpty) { + return const VoicesTextFieldValidationResult.success(); + } + + final l10n = context.l10n; + + if (value.length != 10) { + return VoicesTextFieldValidationResult.error( + '${l10n.format}: ${_pattern.toUpperCase()}', + ); + } + + final dateRegex = RegExp(r'^(\d{2})/(\d{2})/(\d{4})$'); + if (!dateRegex.hasMatch(value)) { + return VoicesTextFieldValidationResult.error( + '${l10n.format}: ${_pattern.toUpperCase()}', + ); + } + + final parts = value.split('/'); + final reformatted = '${parts[2]}-${parts[1]}-${parts[0]}'; + + // Need this because DateTime.parse accepts out-of-range component values + // and interprets them as overflows into the next larger component + final day = int.parse(parts[0]); + final month = int.parse(parts[1]); + final year = int.parse(parts[2]); + final formatDt = DateTime.parse(reformatted); + if (month < 1 || month > 12) { + return VoicesTextFieldValidationResult.error( + '${l10n.format}: ${_pattern.toUpperCase()}', + ); + } + if (day < 1 || day > 31) { + return VoicesTextFieldValidationResult.error( + l10n.datePickerDaysInMonthError, + ); + } + + final daysInMonth = DateTime(year, month + 1, 0).day; + if (day > daysInMonth) { + return VoicesTextFieldValidationResult.error( + l10n.datePickerDaysInMonthError, + ); + } + if (formatDt.isBefore(today.subtract(const Duration(days: 1))) || + formatDt.isAfter(maxDate)) { + return VoicesTextFieldValidationResult.error( + l10n.datePickerDateRangeError, + ); + } + + return const VoicesTextFieldValidationResult.success(); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart new file mode 100644 index 00000000000..06d387f5901 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_field.dart @@ -0,0 +1,328 @@ +import 'package:catalyst_voices/widgets/pickers/voices_calendar_picker.dart'; +import 'package:catalyst_voices/widgets/pickers/voices_time_picker.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_date_field.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_time_field.dart'; +import 'package:flutter/material.dart'; + +typedef DateTimeParts = ({DateTime date, TimeOfDay time}); + +enum PickerType { date, time } + +final class VoicesDateTimeFieldController extends ValueNotifier { + VoicesDateTimeFieldController([super._value]); +} + +class VoicesDateTimeField extends StatefulWidget { + final VoicesDateTimeFieldController? controller; + final ValueChanged? onChanged; + final ValueChanged? onFieldSubmitted; + final String? timeZone; + + const VoicesDateTimeField({ + super.key, + this.controller, + this.onChanged, + required this.onFieldSubmitted, + this.timeZone, + }); + + @override + State createState() => _VoicesDateTimeFieldState(); +} + +class _VoicesDateTimeFieldState extends State { + late final VoicesDateFieldController _dateController; + late final VoicesTimeFieldController _timeController; + + final GlobalKey _dateFiledKey = GlobalKey(); + final GlobalKey _timeFieldKey = GlobalKey(); + + VoicesDateTimeFieldController? _controller; + + VoicesDateTimeFieldController get _effectiveController { + return widget.controller ?? + (_controller ??= VoicesDateTimeFieldController()); + } + + OverlayEntry? _overlayEntry; + PickerType? _pickerType; + VoidCallback? _scrollListener; + bool _isDateOverlayOpen = false; + bool _isTimeOverlayOpen = false; + + @override + void initState() { + super.initState(); + + final dateTime = _effectiveController.value; + final parts = dateTime != null ? _convertDateTimeToParts(dateTime) : null; + + _effectiveController.addListener(_handleDateTimeChanged); + + _dateController = VoicesDateFieldController(parts?.date); + _dateController.addListener(_handleDateChanged); + + _timeController = VoicesTimeFieldController(parts?.time); + _timeController.addListener(_handleTimeChanged); + } + + @override + void didUpdateWidget(covariant VoicesDateTimeField oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.controller != oldWidget.controller) { + (oldWidget.controller ?? _controller) + ?.removeListener(_handleDateTimeChanged); + (widget.controller ?? _controller)?.addListener(_handleDateTimeChanged); + } + } + + @override + void dispose() { + _controller?.dispose(); + _controller = null; + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 220), + child: VoicesDateField( + key: _dateFiledKey, + controller: _dateController, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(16), + ), + dimBorder: _isDateOverlayOpen, + onCalendarTap: _showDatePicker, + onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(), + ), + ), + ConstrainedBox( + constraints: const BoxConstraints.tightFor(width: 200), + child: VoicesTimeField( + key: _timeFieldKey, + controller: _timeController, + borderRadius: const BorderRadius.horizontal( + right: Radius.circular(16), + ), + dimBorder: _isTimeOverlayOpen, + onClockTap: _showTimePicker, + onFieldSubmitted: (_) { + widget.onFieldSubmitted?.call(_effectiveController.value); + }, + timeZone: widget.timeZone, + ), + ), + ], + ); + } + + Future _showDatePicker() async { + final picker = VoicesCalendarDatePicker( + initialDate: _dateController.value, + onDateSelected: (value) { + _removeOverlay(); + _dateController.value = value; + }, + cancelEvent: _removeOverlay, + ); + + final initialPosition = _getRenderBoxOffset(_dateFiledKey); + _pickerType = PickerType.date; + + _showOverlay( + initialPosition: initialPosition, + child: picker, + ); + } + + Future _showTimePicker() async { + final picker = VoicesTimePicker( + onTap: (value) { + _removeOverlay(); + _timeController.value = value; + }, + selectedTime: _timeController.value, + timeZone: widget.timeZone, + ); + + final initialPosition = _getRenderBoxOffset(_timeFieldKey); + _pickerType = PickerType.time; + + _showOverlay( + initialPosition: initialPosition, + child: picker, + ); + } + + void _handleDateTimeChanged() { + final dateTime = _effectiveController.value; + + final parts = dateTime != null ? _convertDateTimeToParts(dateTime) : null; + + if (_dateController.value != parts?.date) { + _dateController.value = parts?.date; + } + + if (_timeController.value != parts?.time) { + _timeController.value = parts?.time; + } + } + + void _handleDateChanged() => _syncControllers(); + + void _handleTimeChanged() => _syncControllers(); + + void _syncControllers() { + final date = _dateController.value; + final time = _timeController.value; + + final dateTime = date != null && time != null + ? DateTime(date.year, date.month, date.day, time.hour, time.minute) + : null; + + if (_effectiveController.value != dateTime) { + _effectiveController.value = dateTime; + } + } + + DateTimeParts? _convertDateTimeToParts(DateTime value) { + final date = DateTime(value.year, value.month, value.day); + final time = TimeOfDay(hour: value.hour, minute: value.minute); + + return (date: date, time: time); + } + + void _showOverlay({ + Offset initialPosition = Offset.zero, + required Widget child, + }) { + FocusScope.of(context).unfocus(); + + if (_pickerType != null) { + _removeOverlay(); + } + + setState(() { + if (_pickerType == PickerType.date) { + _isDateOverlayOpen = true; + } else { + _isTimeOverlayOpen = true; + } + }); + + final overlay = Overlay.of(context, rootOverlay: true); + final scrollPosition = Scrollable.maybeOf(context)?.position; + + _overlayEntry = OverlayEntry( + builder: (context) => Stack( + children: [ + Positioned.fill( + child: MouseRegion( + opaque: false, + hitTestBehavior: HitTestBehavior.translucent, + child: GestureDetector( + onTapDown: (details) async { + final tapPosition = details.globalPosition; + final dateBox = _getRenderBox(_dateFiledKey); + final timeBox = _getRenderBox(_timeFieldKey); + + if (_isBoxTapped(dateBox, tapPosition)) { + return _handleTap(PickerType.date); + } else if (_isBoxTapped(timeBox, tapPosition)) { + return _handleTap(PickerType.time); + } else { + _removeOverlay(); + } + }, + behavior: HitTestBehavior.translucent, + excludeFromSemantics: true, + onPanUpdate: null, + onPanDown: null, + onPanCancel: null, + onPanEnd: null, + onPanStart: null, + child: Container( + color: Colors.transparent, + ), + ), + ), + ), + Positioned( + top: initialPosition.dy + 50 - (scrollPosition?.pixels ?? 0), + left: initialPosition.dx, + child: child, + ), + ], + ), + ); + + if (scrollPosition != null) { + void listener() { + if (_overlayEntry != null) { + _overlayEntry?.markNeedsBuild(); + } + } + + scrollPosition.addListener(listener); + _scrollListener = listener; + } + + overlay.insert(_overlayEntry!); + } + + void _removeOverlay() { + if (_overlayEntry != null) { + setState(() { + if (_pickerType == PickerType.date) { + _isDateOverlayOpen = false; + } else { + _isTimeOverlayOpen = false; + } + }); + + final scrollPosition = Scrollable.maybeOf(context)?.position; + if (scrollPosition != null && _scrollListener != null) { + scrollPosition.removeListener(_scrollListener!); + } + + _overlayEntry?.remove(); + _overlayEntry = null; + _scrollListener = null; + _pickerType = null; + } + } + + Offset _getRenderBoxOffset(GlobalKey key) { + final renderObject = key.currentContext?.findRenderObject(); + if (renderObject is! RenderBox) { + return Offset.zero; + } + + return renderObject.localToGlobal(Offset.zero); + } + + RenderBox? _getRenderBox(GlobalKey key) { + return key.currentContext?.findRenderObject() as RenderBox?; + } + + bool _isBoxTapped(RenderBox? box, Offset tapPosition) { + return box != null && + (box.localToGlobal(Offset.zero) & box.size).contains(tapPosition); + } + + Future _handleTap(PickerType pickerType) async { + _removeOverlay(); + if (pickerType == PickerType.date) { + await _showDatePicker(); + } else { + await _showTimePicker(); + } + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart new file mode 100644 index 00000000000..b1ad08e0a86 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart @@ -0,0 +1,96 @@ +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class VoicesDateTimeTextField extends StatelessWidget { + final TextEditingController? controller; + final ValueChanged? onChanged; + final VoicesTextFieldValidator? validator; + final String hintText; + final ValueChanged? onFieldSubmitted; + final Widget suffixIcon; + final bool dimBorder; + final BorderRadius borderRadius; + final List? inputFormatters; + final AutovalidateMode? autovalidateMode; + + const VoicesDateTimeTextField({ + super.key, + this.controller, + this.onChanged, + this.validator, + required this.hintText, + required this.onFieldSubmitted, + required this.suffixIcon, + this.dimBorder = false, + this.borderRadius = const BorderRadius.all(Radius.circular(16)), + this.inputFormatters, + this.autovalidateMode = AutovalidateMode.onUserInteraction, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; + + final onFieldSubmitted = this.onFieldSubmitted; + + final borderSide = !dimBorder + ? BorderSide( + color: theme.colors.outlineBorderVariant!, + width: 0.75, + ) + : BorderSide( + color: theme.primaryColor, + width: 2, + ); + + return VoicesTextField( + controller: controller, + onChanged: onChanged, + validator: validator, + decoration: VoicesTextFieldDecoration( + suffixIcon: ExcludeFocus(child: suffixIcon), + fillColor: theme.colors.elevationsOnSurfaceNeutralLv1Grey, + filled: true, + enabledBorder: OutlineInputBorder( + borderSide: borderSide, + borderRadius: borderRadius, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: theme.primaryColor, + width: 2, + ), + borderRadius: borderRadius, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 2, + ), + borderRadius: borderRadius, + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: 2, + ), + borderRadius: borderRadius, + ), + hintText: hintText, + hintStyle: textTheme.bodyLarge?.copyWith( + color: theme.colors.textDisabled, + ), + errorStyle: textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + errorMaxLines: 3, + ), + onFieldSubmitted: onFieldSubmitted, + inputFormatters: inputFormatters, + autovalidateMode: autovalidateMode, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_password_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_password_text_field.dart index e7c20a7810b..190bfaa79b4 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_password_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_password_text_field.dart @@ -19,6 +19,9 @@ final class VoicesPasswordTextField extends StatelessWidget { /// Optional decoration. See [VoicesTextField] for more details. final VoicesTextFieldDecoration? decoration; + /// [VoicesTextField.autofocus]. + final bool autofocus; + const VoicesPasswordTextField({ super.key, this.controller, @@ -26,6 +29,7 @@ final class VoicesPasswordTextField extends StatelessWidget { this.onChanged, this.onSubmitted, this.decoration, + this.autofocus = false, }); @override @@ -33,6 +37,7 @@ final class VoicesPasswordTextField extends StatelessWidget { return VoicesTextField( controller: controller, keyboardType: TextInputType.visiblePassword, + autofocus: autofocus, obscureText: true, textInputAction: textInputAction, onChanged: onChanged, diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart index 31220324918..2c9e363c8ae 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart @@ -20,6 +20,9 @@ class VoicesTextField extends StatefulWidget { /// [TextField.decoration] final VoicesTextFieldDecoration? decoration; + /// [VoicesTextField.autofocus]. + final bool autofocus; + /// [TextField.keyboardType] final TextInputType? keyboardType; @@ -68,11 +71,15 @@ class VoicesTextField extends StatefulWidget { /// [TextField.inputFormatters] final List? inputFormatters; + /// [AutovalidateMode] + final AutovalidateMode? autovalidateMode; + const VoicesTextField({ super.key, this.controller, this.focusNode, this.decoration, + this.autofocus = false, this.keyboardType, this.textInputAction, this.textCapitalization = TextCapitalization.none, @@ -91,6 +98,7 @@ class VoicesTextField extends StatefulWidget { required this.onFieldSubmitted, this.onSaved, this.inputFormatters, + this.autovalidateMode, }); @override @@ -176,12 +184,14 @@ class _VoicesTextFieldState extends State { resizableVertically: resizable, child: TextFormField( textAlignVertical: TextAlignVertical.top, + autofocus: widget.autofocus, expands: resizable, controller: _obtainController(), focusNode: widget.focusNode, onFieldSubmitted: widget.onFieldSubmitted, onSaved: widget.onSaved, inputFormatters: widget.inputFormatters, + autovalidateMode: widget.autovalidateMode, decoration: InputDecoration( filled: widget.decoration?.filled, fillColor: widget.decoration?.fillColor, @@ -244,17 +254,18 @@ class _VoicesTextFieldState extends State { : textTheme.bodySmall! .copyWith(color: theme.colors.textDisabled), hintText: widget.decoration?.hintText, - hintStyle: widget.enabled - ? textTheme.bodyLarge - : textTheme.bodyLarge! - .copyWith(color: theme.colors.textDisabled), + hintStyle: _getHintStyle( + textTheme, + theme, + orDefault: widget.enabled + ? textTheme.bodyLarge + : textTheme.bodyLarge! + .copyWith(color: theme.colors.textDisabled), + ), errorText: widget.decoration?.errorText ?? _validation.errorMessage, errorMaxLines: widget.decoration?.errorMaxLines, - errorStyle: widget.enabled - ? textTheme.bodySmall - : textTheme.bodySmall! - .copyWith(color: theme.colors.textDisabled), + errorStyle: _getErrorStyle(textTheme, theme), prefixIcon: _wrapIconIfExists( widget.decoration?.prefixIcon, const EdgeInsetsDirectional.only(start: 8, end: 4), @@ -317,6 +328,13 @@ class _VoicesTextFieldState extends State { } } + TextStyle? _getHintStyle( + TextTheme textTheme, + ThemeData theme, { + TextStyle? orDefault, + }) => + widget.decoration?.hintStyle ?? orDefault; + Widget? _getStatusSuffixWidget() { final showStatusIcon = widget.decoration?.showStatusSuffixIcon ?? true; if (!showStatusIcon) { @@ -390,6 +408,15 @@ class _VoicesTextFieldState extends State { return customController; } + TextStyle? _getErrorStyle(TextTheme textTheme, ThemeData theme) { + if (widget.decoration?.errorStyle != null) { + return widget.decoration?.errorStyle; + } + return widget.enabled + ? textTheme.bodySmall + : textTheme.bodySmall!.copyWith(color: theme.colors.textDisabled); + } + void _onChanged() { _validate(_obtainController().text); } @@ -441,7 +468,8 @@ class VoicesTextFieldValidationResult with EquatableMixin { /// The validation can be either a success, a warning or an error. final VoicesTextFieldStatus status; - /// The error message to be used in case of a [warning] or an [error]. + /// The error message to be used in case of a [VoicesTextFieldStatus.warning] + /// or an [VoicesTextFieldStatus.error]. final String? errorMessage; const VoicesTextFieldValidationResult({ @@ -454,25 +482,23 @@ class VoicesTextFieldValidationResult with EquatableMixin { 'errorMessage can be only used for warning or error status', ); - @override - List get props => [status, errorMessage]; - /// Returns a successful validation result. /// /// The method was designed to be used as /// [VoicesTextField.validator] param: /// /// ``` - /// validator: VoicesTextFieldValidationResult.success, + /// validator: (value) { + /// return const VoicesTextFieldValidationResult.success(); + /// }, /// ``` /// /// in cases where the text field state is known in advance /// and dynamic validation is not needed. - static VoicesTextFieldValidator success() { - return (_) => const VoicesTextFieldValidationResult( + const VoicesTextFieldValidationResult.success() + : this( status: VoicesTextFieldStatus.success, ); - } /// Returns a warning validation result. /// @@ -480,17 +506,18 @@ class VoicesTextFieldValidationResult with EquatableMixin { /// [VoicesTextField.validator] param: /// /// ``` - /// validator: VoicesTextFieldValidationResult.warning, + /// validator: (value) { + /// return const VoicesTextFieldValidationResult.warning(); + /// }, /// ``` /// /// in cases where the text field state is known in advance /// and dynamic validation is not needed. - static VoicesTextFieldValidator warning([String? message]) { - return (_) => VoicesTextFieldValidationResult( + const VoicesTextFieldValidationResult.warning([String? message]) + : this( status: VoicesTextFieldStatus.warning, errorMessage: message, ); - } /// Returns an error validation result. /// @@ -498,17 +525,21 @@ class VoicesTextFieldValidationResult with EquatableMixin { /// [VoicesTextField.validator] param: /// /// ``` - /// validator: VoicesTextFieldValidationResult.error, + /// validator: (value) { + /// return const VoicesTextFieldValidationResult.error(); + /// }, /// ``` /// /// in cases where the text field state is known in advance /// and dynamic validation is not needed. - static VoicesTextFieldValidator error([String? message]) { - return (_) => VoicesTextFieldValidationResult( + const VoicesTextFieldValidationResult.error([String? message]) + : this( status: VoicesTextFieldStatus.error, errorMessage: message, ); - } + + @override + List get props => [status, errorMessage]; } /// Defines the appearance of the [VoicesTextField]. @@ -560,9 +591,15 @@ class VoicesTextFieldDecoration { /// [InputDecoration.hintText]. final String? hintText; + /// [InputDecoration.hintStyle]. + final TextStyle? hintStyle; + /// [InputDecoration.errorText]. final String? errorText; + /// [InputDecoration.errorStyle] + final TextStyle? errorStyle; + /// [InputDecoration.errorMaxLines]. final int? errorMaxLines; @@ -602,7 +639,9 @@ class VoicesTextFieldDecoration { this.labelText, this.helperText, this.hintText, + this.hintStyle, this.errorText, + this.errorStyle, this.errorMaxLines, this.prefixIcon, this.prefixText, diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart new file mode 100644 index 00000000000..72ff91f17fb --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_time_field.dart @@ -0,0 +1,175 @@ +import 'package:catalyst_voices/common/ext/time_of_day_ext.dart'; +import 'package:catalyst_voices/widgets/buttons/voices_icon_button.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_date_time_text_field.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:mask_text_input_formatter/mask_text_input_formatter.dart'; + +final class VoicesTimeFieldController extends ValueNotifier { + VoicesTimeFieldController([super._value]); +} + +class VoicesTimeField extends StatefulWidget { + final VoicesTimeFieldController? controller; + final ValueChanged? onChanged; + final ValueChanged? onFieldSubmitted; + final VoidCallback? onClockTap; + final bool dimBorder; + final BorderRadius borderRadius; + final String? timeZone; + + const VoicesTimeField({ + super.key, + this.controller, + this.onChanged, + required this.onFieldSubmitted, + this.onClockTap, + this.dimBorder = false, + this.borderRadius = const BorderRadius.all(Radius.circular(16)), + this.timeZone, + }); + + @override + State createState() => _VoicesTimeFieldState(); +} + +class _VoicesTimeFieldState extends State { + late final TextEditingController _textEditingController; + late final MaskTextInputFormatter timeFormatter; + + VoicesTimeFieldController? _controller; + + VoicesTimeFieldController get _effectiveController { + return widget.controller ?? (_controller ??= VoicesTimeFieldController()); + } + + String get _pattern => 'HH:MM'; + String get timeZone => widget.timeZone ?? ''; + + @override + void initState() { + final initialTime = _effectiveController.value; + final initialText = _convertTimeToText(initialTime); + + _textEditingController = TextEditingController(text: initialText); + _textEditingController.addListener(_handleTextChanged); + + _effectiveController.addListener(_handleDateChanged); + + timeFormatter = MaskTextInputFormatter( + mask: _pattern, + filter: { + 'H': RegExp('[0-9]'), + 'M': RegExp('[0-9]'), + }, + type: MaskAutoCompletionType.eager, + ); + + super.initState(); + } + + @override + void didUpdateWidget(covariant VoicesTimeField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + (oldWidget.controller ?? _controller)?.removeListener(_handleDateChanged); + (widget.controller ?? _controller)?.addListener(_handleDateChanged); + + final time = _effectiveController.value; + _textEditingController.text = _convertTimeToText(time); + } + } + + @override + void dispose() { + _textEditingController.dispose(); + + _controller?.dispose(); + _controller = null; + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final onChanged = widget.onChanged; + final onFieldSubmitted = widget.onFieldSubmitted; + + return VoicesDateTimeTextField( + controller: _textEditingController, + onChanged: onChanged != null + ? (value) => onChanged(_convertTextToTime(value)) + : null, + validator: _validate, + hintText: '${_pattern.toUpperCase()} $timeZone', + onFieldSubmitted: onFieldSubmitted != null + ? (value) => onFieldSubmitted(_convertTextToTime(value)) + : null, + suffixIcon: ExcludeFocus( + child: VoicesIconButton( + onTap: widget.onClockTap, + child: VoicesAssets.icons.clock.buildIcon(), + ), + ), + dimBorder: widget.dimBorder, + borderRadius: widget.borderRadius, + inputFormatters: [timeFormatter], + ); + } + + void _handleTextChanged() { + final text = _textEditingController.text; + final time = _convertTextToTime(text); + if (_effectiveController.value != time) { + _effectiveController.value = time; + } + } + + void _handleDateChanged() { + final time = _effectiveController.value; + final text = _convertTimeToText(time); + if (_textEditingController.text != text) { + _textEditingController.text = text; + } + } + + String _convertTimeToText(TimeOfDay? value) { + return value?.formatted ?? ''; + } + + TimeOfDay? _convertTextToTime(String value) { + if (value.isEmpty) return null; + if (_validate(value).status != VoicesTextFieldStatus.success) { + return null; + } + + try { + final parts = value.split(':'); + final rawHours = parts[0]; + final rawMinutes = parts[1]; + + final hour = int.parse(rawHours); + final minute = int.parse(rawMinutes); + + return TimeOfDay(hour: hour, minute: minute); + } catch (e) { + return null; + } + } + + VoicesTextFieldValidationResult _validate(String value) { + if (value.isEmpty) { + return const VoicesTextFieldValidationResult.success(); + } + final l10n = context.l10n; + + final pattern = RegExp(r'^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$'); + if (!pattern.hasMatch(value)) { + return VoicesTextFieldValidationResult.error('${l10n.format}: $_pattern'); + } + + return const VoicesTextFieldValidationResult.success(); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/voices_expansion_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/voices_expansion_tile.dart new file mode 100644 index 00000000000..46aca16a6a8 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/voices_expansion_tile.dart @@ -0,0 +1,107 @@ +import 'package:catalyst_voices/widgets/buttons/voices_buttons.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesExpansionTile extends StatefulWidget { + final Widget title; + final List children; + final bool initiallyExpanded; + + const VoicesExpansionTile({ + super.key, + required this.title, + this.children = const [], + this.initiallyExpanded = false, + }); + + @override + State createState() => _VoicesExpansionTileState(); +} + +class _VoicesExpansionTileState extends State { + final _controller = ExpansionTileController(); + + bool _isExpanded = false; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + Widget build(BuildContext context) { + return _ThemeOverride( + child: Builder( + builder: (context) { + final theme = Theme.of(context); + + return ExpansionTile( + title: DefaultTextStyle( + style: theme.textTheme.titleLarge ?? const TextStyle(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: widget.title, + ), + trailing: ChevronExpandButton( + isExpanded: _isExpanded, + onTap: _toggleExpand, + ), + controller: _controller, + initiallyExpanded: _isExpanded, + onExpansionChanged: _updateExpended, + children: widget.children, + ); + }, + ), + ); + } + + void _updateExpended(bool value) { + setState(() { + _isExpanded = value; + }); + } + + void _toggleExpand() { + if (_controller.isExpanded) { + _controller.collapse(); + } else { + _controller.expand(); + } + } +} + +class _ThemeOverride extends StatelessWidget { + final Widget child; + + const _ThemeOverride({ + required this.child, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Theme( + data: theme.copyWith( + // listTileTheme is required here because ExpansionTile does not let + // us set shape or ripple used internally by ListTile. + listTileTheme: const ListTileThemeData(shape: RoundedRectangleBorder()), + expansionTileTheme: ExpansionTileThemeData( + backgroundColor: theme.colors.elevationsOnSurfaceNeutralLv0, + collapsedBackgroundColor: theme.colors.elevationsOnSurfaceNeutralLv0, + tilePadding: const EdgeInsets.fromLTRB(24, 8, 12, 8), + childrenPadding: const EdgeInsets.fromLTRB(24, 16, 24, 24), + textColor: theme.colors.textOnPrimaryLevel1, + collapsedTextColor: theme.colors.textOnPrimaryLevel1, + iconColor: theme.colors.iconsForeground, + collapsedIconColor: theme.colors.iconsForeground, + shape: const RoundedRectangleBorder(), + collapsedShape: const RoundedRectangleBorder(), + ), + ), + child: child, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/voices_nav_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/voices_nav_tile.dart index fb27a289a3a..70e2d515ff6 100644 --- a/catalyst_voices/apps/voices/lib/widgets/tiles/voices_nav_tile.dart +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/voices_nav_tile.dart @@ -42,6 +42,7 @@ class VoicesNavTile extends StatelessWidget { ); return IconTheme( + key: const ValueKey('UserDrawerMenuItem'), data: iconTheme, child: IconButtonTheme( data: const IconButtonThemeData(style: iconButtonStyle), diff --git a/catalyst_voices/apps/voices/lib/widgets/toggles/voices_theme_mode_switch.dart b/catalyst_voices/apps/voices/lib/widgets/toggles/voices_theme_mode_switch.dart index d93aabca0cb..4211d352938 100644 --- a/catalyst_voices/apps/voices/lib/widgets/toggles/voices_theme_mode_switch.dart +++ b/catalyst_voices/apps/voices/lib/widgets/toggles/voices_theme_mode_switch.dart @@ -19,6 +19,7 @@ class VoicesThemeModeSwitch extends StatelessWidget { Text('${context.l10n.themeLight} / ${context.l10n.themeDark}'), const SizedBox(width: 8), VoicesSwitch( + key: const Key('ThemeSwitch'), value: Theme.of(context).brightness == Brightness.dark, onChanged: (value) { onChanged( diff --git a/catalyst_voices/apps/voices/lib/widgets/widgets.dart b/catalyst_voices/apps/voices/lib/widgets/widgets.dart index d3ad43f3ba5..2cabd912a34 100644 --- a/catalyst_voices/apps/voices/lib/widgets/widgets.dart +++ b/catalyst_voices/apps/voices/lib/widgets/widgets.dart @@ -46,8 +46,10 @@ export 'indicators/voices_status_indicator.dart'; export 'list/bullet_list.dart'; export 'menu/voices_list_tile.dart'; export 'menu/voices_menu.dart'; +export 'menu/voices_modal_menu.dart'; export 'menu/voices_node_menu.dart'; export 'menu/voices_wallet_tile.dart'; +export 'modals/details/voices_details_dialog.dart'; export 'modals/voices_alert_dialog.dart'; export 'modals/voices_desktop_dialog.dart'; export 'modals/voices_dialog.dart'; @@ -68,9 +70,11 @@ export 'separators/voices_text_divider.dart'; export 'separators/voices_vertical_divider.dart'; export 'text_field/seed_phrase_field.dart'; export 'text_field/voices_autocomplete.dart'; +export 'text_field/voices_date_time_field.dart'; export 'text_field/voices_email_text_field.dart'; export 'text_field/voices_password_text_field.dart'; export 'text_field/voices_text_field.dart'; +export 'tiles/voices_expansion_tile.dart'; export 'tiles/voices_nav_tile.dart'; export 'toggles/voices_checkbox.dart'; export 'toggles/voices_checkbox_group.dart'; diff --git a/catalyst_voices/apps/voices/macos/Flutter/GeneratedPluginRegistrant.swift b/catalyst_voices/apps/voices/macos/Flutter/GeneratedPluginRegistrant.swift index 15dd4f0ba5b..b03a5488ba8 100644 --- a/catalyst_voices/apps/voices/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/catalyst_voices/apps/voices/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,6 +14,7 @@ import package_info_plus import path_provider_foundation import quill_native_bridge_macos import sentry_flutter +import shared_preferences_foundation import super_native_extensions import url_launcher_macos import video_player_avfoundation @@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) QuillNativeBridgePlugin.register(with: registry.registrar(forPlugin: "QuillNativeBridgePlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) diff --git a/catalyst_voices/apps/voices/pubspec.yaml b/catalyst_voices/apps/voices/pubspec.yaml index 27987e9e804..2fdddf8d30e 100644 --- a/catalyst_voices/apps/voices/pubspec.yaml +++ b/catalyst_voices/apps/voices/pubspec.yaml @@ -48,15 +48,20 @@ dependencies: flutter_localized_locales: ^2.0.5 flutter_quill: ^10.8.2 flutter_quill_extensions: ^10.8.2 + flutter_secure_storage: ^9.2.2 flutter_web_plugins: sdk: flutter formz: ^0.7.0 go_router: ^14.0.2 google_fonts: ^6.2.1 intl: ^0.19.0 + markdown: ^7.2.2 + markdown_quill: ^4.2.0 + mask_text_input_formatter: ^2.9.0 result_type: ^0.2.0 scrollable_positioned_list: ^0.3.8 sentry_flutter: ^8.8.0 + shared_preferences: ^2.3.3 url_launcher: ^6.2.2 url_strategy: ^0.3.0 # TODO(dtscalac): win32 dependency is just a transitive dependency and shouldn't be imported @@ -75,6 +80,7 @@ dev_dependencies: sdk: flutter mockito: ^5.4.4 mocktail: ^1.0.1 + patrol_finders: ^2.5.1 sentry_dart_plugin: ^2.1.0 flutter: diff --git a/catalyst_voices/apps/voices/test/common/codecs/markdown_codec_test.dart b/catalyst_voices/apps/voices/test/common/codecs/markdown_codec_test.dart new file mode 100644 index 00000000000..dd0961ed796 --- /dev/null +++ b/catalyst_voices/apps/voices/test/common/codecs/markdown_codec_test.dart @@ -0,0 +1,89 @@ +import 'package:catalyst_voices/common/codecs/markdown_codec.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter_quill/quill_delta.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(MarkdownCodec, () { + test('code and encode empty string', () { + // Given + const source = MarkdownData(''); + + // When + final delta = markdown.encode(source); + final md = markdown.decode(delta); + + // Then + expect(md, source); + }); + + test('code and encode plain text', () { + // Given + const source = MarkdownData('Hello Catalyst!'); + + // When + final delta = markdown.encode(source); + final md = markdown.decode(delta); + + // Then + expect(md, source); + }); + + group('decode', () { + test('empty delta file builds empty string', () { + // Given + final delta = Delta(); + + // When + final markdownString = markdown.decode(delta); + + // Then + expect(markdownString.data, isEmpty); + }); + + test('plan text delta file builds correct string', () { + // Given + const plainText = 'Hello Catalyst!'; + final delta = Delta() + ..insert(plainText) + ..insert('\n'); + + // When + final markdownString = markdown.decode(delta); + + // Then + expect(markdownString.data, plainText); + }); + }); + + group('encode', () { + test('empty markdown string builds valid empty delta', () { + // Given + const markdownString = MarkdownData(''); + + // When + final delta = markdown.encode(markdownString); + + // Then + expect(delta.isEmpty, isTrue); + }); + + test('plan text markdown builds correct delta', () { + // Given + const plainText = 'Hello Catalyst!'; + const markdownString = MarkdownData(plainText); + + // When + final delta = markdown.encode(markdownString); + + // Then + expect(delta.isNotEmpty, isTrue); + expect(delta.operations, hasLength(1)); + + final operation = delta.operations[0]; + expect(operation.key, 'insert'); + expect(operation.data, '$plainText\n'); + }); + }); + }); +} diff --git a/catalyst_voices/apps/voices/test/widgets/cards/campaign_stage_card_test.dart b/catalyst_voices/apps/voices/test/widgets/cards/campaign_stage_card_test.dart new file mode 100644 index 00000000000..553e1aef056 --- /dev/null +++ b/catalyst_voices/apps/voices/test/widgets/cards/campaign_stage_card_test.dart @@ -0,0 +1,154 @@ +import 'package:catalyst_voices/widgets/cards/campaign_stage_card.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + late VoicesColorScheme voicesColors; + + final draftCampaignTest = CampaignInfo( + id: 'campaign_draft', + stage: CampaignStage.draft, + startDate: DateTime(2024, 11, 19, 13, 00, 00), + endDate: DateTime(2024, 11, 20, 13, 00, 00), + description: 'Draft Campaign Test', + ); + + final scheduledCampaignTest = CampaignInfo( + id: 'campaign_scheduled', + stage: CampaignStage.scheduled, + startDate: DateTime(2024, 11, 19, 13, 00, 00), + endDate: DateTime(2024, 11, 20, 13, 00, 00), + description: 'Scheduled Campaign Test', + ); + + final liveCampaignTest = CampaignInfo( + id: 'campaign_live', + stage: CampaignStage.live, + startDate: DateTime(2024, 11, 19, 13, 00, 00), + endDate: DateTime(2024, 11, 20, 13, 00, 00), + description: 'Live Campaign Test', + ); + + final completedCampaignText = CampaignInfo( + id: 'campaign_completed', + stage: CampaignStage.completed, + startDate: DateTime(2024, 11, 19, 13, 00, 00), + endDate: DateTime(2024, 11, 20, 13, 00, 00), + description: 'Completed Campaign Test', + ); + + setUp(() { + voicesColors = const VoicesColorScheme.optional( + outlineBorderVariant: Colors.red, + elevationsOnSurfaceNeutralLv1White: Colors.blue, + ); + }); + + Widget buildTestWidget(CampaignInfo campaign) { + return Scaffold( + body: SizedBox( + width: 1000, + child: CampaignStageCard( + campaign: campaign, + ), + ), + ); + } + + group(CampaignStageCard, () { + testWidgets('Renders all elements correctly', (tester) async { + await tester.pumpApp( + buildTestWidget(completedCampaignText), + voicesColors: voicesColors, + ); + + await tester.pumpAndSettle(); + + expect(find.byType(CampaignStageCard), findsOneWidget); + expect(find.byType(CatalystSvgIcon), findsOneWidget); + }); + + testWidgets('Renders correctly for draft campaign', (tester) async { + await tester.pumpApp( + buildTestWidget(draftCampaignTest), + voicesColors: voicesColors, + ); + + await tester.pumpAndSettle(); + + expect(find.byType(CampaignStageCard), findsOneWidget); + expect(find.byType(Text), findsExactly(3)); + expect(find.byType(CatalystSvgIcon), findsExactly(2)); + expect( + find.text('Campaign Starting Soon (Ready to deploy)'), + findsOneWidget, + ); + expect(find.text(draftCampaignTest.description), findsOneWidget); + expect(find.byType(OutlinedButton), findsNothing); + }); + + testWidgets('Renders correctly for scheduled campaign', (tester) async { + await tester.pumpApp( + buildTestWidget(scheduledCampaignTest), + voicesColors: voicesColors, + ); + + await tester.pumpAndSettle(); + + expect(find.byType(CampaignStageCard), findsOneWidget); + expect(find.byType(Text), findsExactly(3)); + expect(find.byType(CatalystSvgIcon), findsExactly(2)); + expect( + find.text('Campaign Starting Soon (Ready to deploy)'), + findsOneWidget, + ); + expect(find.text(scheduledCampaignTest.description), findsOneWidget); + expect(find.byType(OutlinedButton), findsNothing); + }); + + testWidgets('Renders correctly for live campaign', (tester) async { + await tester.pumpApp( + buildTestWidget(liveCampaignTest), + voicesColors: voicesColors, + ); + + await tester.pumpAndSettle(); + + expect(find.byType(CampaignStageCard), findsOneWidget); + expect(find.byType(Text), findsExactly(4)); + expect(find.byType(CatalystSvgIcon), findsExactly(2)); + expect( + find.text('Campaign Is Live (Published)'), + findsOneWidget, + ); + expect(find.text(liveCampaignTest.description), findsOneWidget); + expect(find.byType(OutlinedButton), findsOneWidget); + expect(find.text('View proposals'), findsOneWidget); + }); + + testWidgets('Renders correctly for completed campaign', (tester) async { + await tester.pumpApp( + buildTestWidget(completedCampaignText), + voicesColors: voicesColors, + ); + + await tester.pumpAndSettle(); + + expect(find.byType(CampaignStageCard), findsOneWidget); + expect(find.byType(Text), findsExactly(3)); + expect(find.byType(CatalystSvgIcon), findsOneWidget); + expect( + find.text('Campaign Concluded, Result are in!'), + findsOneWidget, + ); + expect(find.text(completedCampaignText.description), findsOneWidget); + expect(find.byType(OutlinedButton), findsOneWidget); + expect(find.text('View Voting Results'), findsOneWidget); + }); + }); +} diff --git a/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart b/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart index 7afb9e3c309..11956edf72b 100644 --- a/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart +++ b/catalyst_voices/apps/voices/test/widgets/text_field/voices_text_field_test.dart @@ -106,7 +106,9 @@ void main() { await tester.pumpWidget( _MaterialApp( child: VoicesTextField( - validator: VoicesTextFieldValidationResult.success(), + validator: (value) { + return const VoicesTextFieldValidationResult.success(); + }, onFieldSubmitted: (value) {}, ), ), diff --git a/catalyst_voices/justfile b/catalyst_voices/justfile index 9d20ac33cac..a870a7d6a9b 100755 --- a/catalyst_voices/justfile +++ b/catalyst_voices/justfile @@ -12,6 +12,7 @@ setup-code: generate-code: setup-code melos l10n melos build_runner + melos build_runner_repository just generate-gateway-services # Syntax sugar for linking packages and building generated code @@ -27,13 +28,9 @@ check-code: test-code: earthly +test-unit -# Generates gateway services in packages/internal/catalyst_voices_services +# Generates gateway services in packages/internal/catalyst_voices_repositories generate-gateway-services: cd .. && earthly ./catalyst_voices+code-generator --platform=linux/amd64 --save_locally=true -# Test generated gateway services -test-gateway-services: - cd .. && earthly ./catalyst_voices+test-flutter-code-generator --platform=linux/amd64 - # Pre Push Checks pre-push: check-code diff --git a/catalyst_voices/melos.yaml b/catalyst_voices/melos.yaml index ff9db278646..367385f9d2c 100644 --- a/catalyst_voices/melos.yaml +++ b/catalyst_voices/melos.yaml @@ -89,6 +89,7 @@ command: asn1lib: ^1.5.3 bip39: ^1.0.6 bloc_concurrency: ^0.2.2 + chopper: ^8.0.3 collection: ^1.18.0 cryptography: ^2.7.0 dotted_border: ^2.1.0 @@ -111,6 +112,8 @@ command: formz: ^0.7.0 intl: ^0.19.0 logging: ^1.2.0 + markdown: ^7.2.2 + markdown_quill: ^4.2.0 meta: ^1.10.0 result_type: ^0.2.0 password_strength: ^0.2.0 @@ -121,10 +124,11 @@ command: convert: ^3.1.1 path: ^1.9.0 pinenacl: ^0.6.0 - ulid: ^2.0.0 uuid: ^4.5.1 sentry_flutter: ^8.8.0 scrollable_positioned_list: ^0.3.8 + shared_preferences: ^2.3.3 + shared_preferences_platform_interface: ^2.4.1 # TODO(dtscalac): win32 dependency is just a transitive dependency and shouldn't be imported # but here we import it explicitly to make sure the latest version is used which addresses # the problem from here: https://github.com/jonataslaw/get_cli/issues/263 @@ -134,6 +138,7 @@ command: dev_dependencies: test: ^1.24.9 build_runner: ^2.4.12 + chopper_generator: ^8.0.3 mocktail: ^1.0.1 scripts: @@ -147,13 +152,23 @@ scripts: run: | melos exec -c 1 \ --depends-on="build_runner" \ - --ignore="catalyst_voices_services" -- \ + --ignore="catalyst_voices_repositories" -- \ dart run build_runner build --delete-conflicting-outputs description: | Run `build_runner` in every package which contains the build_runner dependency. - The catalyst_voices_services is skipped because to run a build_runner there you + The catalyst_voices_repositories is skipped because to run a build_runner there you must generate first swagger docs (see related Earthfile). + build_runner_repository: + run: | + melos exec -c 1 \ + --depends-on="build_runner" \ + --scope="catalyst_voices_repositories" -- \ + dart run build_runner build --delete-conflicting-outputs \ + --build-filter="lib/src/dto/*" + description: | + Run `build_runner` in catalyst_voices_repositories package only in selected folders + metrics: run: | melos exec -c 1 -- \ @@ -164,16 +179,16 @@ scripts: format-apply: run: | - melos exec -c 1 --dir-exists="lib" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' | tr '\n' ' ' | xargs dart format --fix" && - melos exec -c 1 --dir-exists="test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' | tr '\n' ' ' | xargs dart format --fix" && - melos exec -c 1 --dir-exists="integration_test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' | tr '\n' ' ' | xargs dart format --fix" + melos exec -c 1 --dir-exists="lib" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' ! -path './.dart_tool/*' | tr '\n' ' ' | xargs dart format --fix" && + melos exec -c 1 --dir-exists="test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' ! -path './.dart_tool/*' | tr '\n' ' ' | xargs dart format --fix" && + melos exec -c 1 --dir-exists="integration_test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' ! -path './.dart_tool/*' | tr '\n' ' ' | xargs dart format --fix" description: Run `dart format` for all packages. format-check: run: | - melos exec -c 1 --dir-exists="lib" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' | tr '\n' ' ' | xargs dart format --output none --set-exit-if-changed" && - melos exec -c 1 --dir-exists="test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' | tr '\n' ' ' | xargs dart format --output none --set-exit-if-changed" && - melos exec -c 1 --dir-exists="integration_test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' | tr '\n' ' ' | xargs dart format --output none --set-exit-if-changed" + melos exec -c 1 --dir-exists="lib" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' ! -path './.dart_tool/*' | tr '\n' ' ' | xargs dart format --output none --set-exit-if-changed" && + melos exec -c 1 --dir-exists="test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' ! -path './.dart_tool/*' | tr '\n' ' ' | xargs dart format --output none --set-exit-if-changed" && + melos exec -c 1 --dir-exists="integration_test" -- "find . -name "*.dart" ! -name "*.g.dart" ! -path '*/generated/*' ! -path './.dart_tool/*' | tr '\n' ' ' | xargs dart format --output none --set-exit-if-changed" description: Run `dart format` checks for all packages. license-check: diff --git a/catalyst_voices/packages/internal/catalyst_voices_assets/assets/images/no_proposal_foreground.svg b/catalyst_voices/packages/internal/catalyst_voices_assets/assets/images/no_proposal_foreground.svg new file mode 100644 index 00000000000..df9c9749f62 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_assets/assets/images/no_proposal_foreground.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools.dart new file mode 100644 index 00000000000..57574cb273e --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools.dart @@ -0,0 +1,2 @@ +export 'admin_tools_cubit.dart'; +export 'admin_tools_state.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools_cubit.dart new file mode 100644 index 00000000000..0252eb3be66 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools_cubit.dart @@ -0,0 +1,35 @@ +import 'package:catalyst_voices_blocs/src/admin_tools/admin_tools_state.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +abstract interface class AdminTools { + /// The current state of admin tools. + AdminToolsState get state; + + /// A stream with updates for admin tools state. + Stream get stream; +} + +/// Manages the admin tools. +/// +/// The admin tools can be used to override application state (UI). +final class AdminToolsCubit extends Cubit + implements AdminTools { + AdminToolsCubit() : super(const AdminToolsState()); + + void enable() { + emit(state.copyWith(enabled: true)); + } + + void disable() { + emit(state.copyWith(enabled: false)); + } + + void updateCampaignStage(CampaignStage stage) { + emit(state.copyWith(campaignStage: stage)); + } + + void updateSessionStatus(SessionStatus status) { + emit(state.copyWith(sessionStatus: status)); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools_state.dart new file mode 100644 index 00000000000..5131c625f1a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/admin_tools/admin_tools_state.dart @@ -0,0 +1,30 @@ +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +/// The state of admin tools. +final class AdminToolsState extends Equatable { + final bool enabled; + final CampaignStage campaignStage; + final SessionStatus sessionStatus; + + const AdminToolsState({ + this.enabled = false, + this.campaignStage = CampaignStage.scheduled, + this.sessionStatus = SessionStatus.actor, + }); + + AdminToolsState copyWith({ + bool? enabled, + CampaignStage? campaignStage, + SessionStatus? sessionStatus, + }) { + return AdminToolsState( + enabled: enabled ?? this.enabled, + campaignStage: campaignStage ?? this.campaignStage, + sessionStatus: sessionStatus ?? this.sessionStatus, + ); + } + + @override + List get props => [enabled, campaignStage, sessionStatus]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication.dart deleted file mode 100644 index 0db6272d6e7..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication.dart +++ /dev/null @@ -1 +0,0 @@ -export 'authentication_bloc.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_bloc.dart deleted file mode 100644 index 7ed1412c0e8..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_bloc.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:async'; - -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -part 'authentication_event.dart'; -part 'authentication_state.dart'; - -final class AuthenticationBloc - extends Bloc { - final AuthenticationRepository _authenticationRepository; - - late StreamSubscription - _authenticationStatusSubscription; - - AuthenticationBloc({ - required AuthenticationRepository authenticationRepository, - }) : _authenticationRepository = authenticationRepository, - super(const AuthenticationState.unknown()) { - on<_AuthenticationStatusChanged>(_onAuthenticationStatusChanged); - on(_onAuthenticationLogoutRequested); - _authenticationStatusSubscription = _authenticationRepository.status.listen( - (status) => add(_AuthenticationStatusChanged(status)), - ); - } - - bool get isAuthenticated => - state.status == AuthenticationStatus.authenticated; - - bool get isInitial => state.status == AuthenticationStatus.unknown; - AuthenticationStatus get status => state.status; - - @override - Future close() { - _authenticationStatusSubscription.cancel(); - return super.close(); - } - - void _onAuthenticationLogoutRequested( - AuthenticationLogoutRequested event, - Emitter emit, - ) { - _authenticationRepository.logOut(); - } - - Future _onAuthenticationStatusChanged( - _AuthenticationStatusChanged event, - Emitter emit, - ) async { - switch (event.status) { - case AuthenticationStatus.unauthenticated: - return emit(const AuthenticationState.unauthenticated()); - case AuthenticationStatus.authenticated: - final sessionData = await _authenticationRepository.getSessionData(); - - return emit( - sessionData != null - ? AuthenticationState.authenticated(sessionData) - : const AuthenticationState.unauthenticated(), - ); - case AuthenticationStatus.unknown: - return emit(const AuthenticationState.unknown()); - } - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_event.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_event.dart deleted file mode 100644 index 2c0e160b760..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_event.dart +++ /dev/null @@ -1,13 +0,0 @@ -part of 'authentication_bloc.dart'; - -abstract final class AuthenticationEvent { - const AuthenticationEvent(); -} - -final class AuthenticationLogoutRequested extends AuthenticationEvent {} - -final class _AuthenticationStatusChanged extends AuthenticationEvent { - final AuthenticationStatus status; - - const _AuthenticationStatusChanged(this.status); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_state.dart deleted file mode 100644 index 857aa20c0c0..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/authentication/authentication_state.dart +++ /dev/null @@ -1,28 +0,0 @@ -part of 'authentication_bloc.dart'; - -final class AuthenticationState extends Equatable { - final AuthenticationStatus status; - final SessionData? sessionData; - - const AuthenticationState.authenticated( - SessionData sessionData, - ) : this._( - status: AuthenticationStatus.authenticated, - sessionData: sessionData, - ); - - const AuthenticationState.unauthenticated() - : this._( - status: AuthenticationStatus.unauthenticated, - ); - - const AuthenticationState.unknown() : this._(); - - const AuthenticationState._({ - this.status = AuthenticationStatus.unknown, - this.sessionData, - }); - - @override - List get props => [status, sessionData ?? '']; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder.dart new file mode 100644 index 00000000000..7687728e768 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder.dart @@ -0,0 +1 @@ +export 'campaign_builder_cubit.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_cubit.dart new file mode 100644 index 00000000000..4b689c568e6 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_cubit.dart @@ -0,0 +1,50 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'campaign_builder_state.dart'; + +class CampaignBuilderCubit extends Cubit { + CampaignBuilderCubit() : super(const CampaignBuilderState(isLoading: true)); + + void getCampaignStatus() { + emit(state.copyWith(isLoading: true)); + + emit( + state.copyWith( + isLoading: false, + publish: const Optional(CampaignPublish.draft), + ), + ); + } + + void updateCampaignPublish(CampaignPublish publish) { + emit(state.copyWith(isLoading: true)); + + // TODO(ryszard-schossler): call backend to update campaign status + + emit( + state.copyWith( + isLoading: false, + publish: Optional(publish), + ), + ); + } + + void updateCampaignDates({ + required DateTime? startDate, + required DateTime? endDate, + }) { + emit(state.copyWith(isLoading: true)); + + // TODO(ryszard-schossler): call backend to update campaign dates + + emit( + state.copyWith( + isLoading: false, + startDate: Optional(startDate), + endDate: Optional(endDate), + ), + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_state.dart new file mode 100644 index 00000000000..68f5b71dc3a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_state.dart @@ -0,0 +1,32 @@ +part of 'campaign_builder_cubit.dart'; + +final class CampaignBuilderState extends Equatable { + final bool isLoading; + final CampaignPublish? publish; + final DateTime? startDate; + final DateTime? endDate; + + const CampaignBuilderState({ + this.isLoading = false, + this.publish, + this.startDate, + this.endDate, + }); + + CampaignBuilderState copyWith({ + bool? isLoading, + Optional? publish, + Optional? startDate, + Optional? endDate, + }) { + return CampaignBuilderState( + isLoading: isLoading ?? this.isLoading, + publish: publish.dataOr(this.publish), + startDate: startDate.dataOr(this.startDate), + endDate: endDate.dataOr(this.endDate), + ); + } + + @override + List get props => [isLoading, publish, startDate, endDate]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details.dart new file mode 100644 index 00000000000..7bce826377c --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details.dart @@ -0,0 +1,3 @@ +export 'campaign_details_bloc.dart'; +export 'campaign_details_event.dart'; +export 'campaign_details_state.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_bloc.dart new file mode 100644 index 00000000000..33ce035756d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_bloc.dart @@ -0,0 +1,56 @@ +import 'package:catalyst_voices_blocs/src/campaign/details/campaign_details.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +final class CampaignDetailsBloc + extends Bloc { + final CampaignRepository _campaignRepository; + + CampaignDetailsBloc( + this._campaignRepository, + ) : super(const CampaignDetailsState()) { + on(_onLoadCampaignEvent); + } + + Future _onLoadCampaignEvent( + LoadCampaignEvent event, + Emitter emit, + ) async { + final id = event.id; + + emit( + state.copyWith( + title: const Optional.empty(), + listItems: const [], + ), + ); + + final campaign = await _campaignRepository.getCampaign(id: id); + final listItems = _mapCampaignToListItems(campaign); + + emit( + state.copyWith( + title: Optional.of(campaign.name), + listItems: listItems, + ), + ); + } + + List _mapCampaignToListItems(Campaign campaign) { + final sections = + campaign.sections.map(CampaignCategorySection.fromCategory).toList(); + + return [ + CampaignDetailsListItem( + description: campaign.description, + startDate: campaign.startDate, + endDate: campaign.endDate, + proposalsCount: campaign.proposalsCount, + categoriesCount: campaign.categoriesCount, + ), + CampaignCategoriesListItem(sections: sections), + ]; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_event.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_event.dart new file mode 100644 index 00000000000..82d35c99240 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_event.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; + +sealed class CampaignDetailsEvent extends Equatable { + const CampaignDetailsEvent(); +} + +final class LoadCampaignEvent extends CampaignDetailsEvent { + final String id; + + const LoadCampaignEvent({ + required this.id, + }); + + @override + List get props => [id]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_state.dart new file mode 100644 index 00000000000..985c204f09d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/details/campaign_details_state.dart @@ -0,0 +1,29 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +final class CampaignDetailsState extends Equatable { + final String? title; + final List listItems; + + const CampaignDetailsState({ + this.title, + this.listItems = const [], + }); + + CampaignDetailsState copyWith({ + Optional? title, + List? listItems, + }) { + return CampaignDetailsState( + title: title.dataOr(this.title), + listItems: listItems ?? this.listItems, + ); + } + + @override + List get props => [ + title, + listItems, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info.dart new file mode 100644 index 00000000000..95235bd7bac --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info.dart @@ -0,0 +1,2 @@ +export 'campaign_info_cubit.dart'; +export 'campaign_info_state.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info_cubit.dart new file mode 100644 index 00000000000..f17c20ab8f1 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info_cubit.dart @@ -0,0 +1,72 @@ +import 'dart:async'; + +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Gets the campaign info. +final class CampaignInfoCubit extends Cubit { + final CampaignService _campaignService; + final AdminTools _adminTools; + + AdminToolsState _adminToolsState; + StreamSubscription? _adminToolsSub; + + CampaignInfoCubit( + this._campaignService, + this._adminTools, + ) : _adminToolsState = _adminTools.state, + super(const CampaignInfoState()) { + _adminToolsSub = _adminTools.stream.listen(_onAdminToolsChanged); + } + + /// Loads the currently active campaign. + Future load() async { + emit(const CampaignInfoState(isLoading: true)); + + final campaign = await _campaignService.getActiveCampaign(); + CampaignInfo? campaignInfo; + + if (campaign == null) { + campaignInfo = null; + } else if (_adminToolsState.enabled) { + campaignInfo = _mockCampaign(campaign); + } else { + campaignInfo = CampaignInfo.fromCampaign(campaign, DateTimeExt.now()); + } + + if (!isClosed) { + emit(CampaignInfoState(campaign: campaignInfo)); + } + } + + @override + Future close() async { + await _adminToolsSub?.cancel(); + _adminToolsSub = null; + + return super.close(); + } + + Future _onAdminToolsChanged(AdminToolsState adminTools) async { + _adminToolsState = adminTools; + await load(); + } + + CampaignInfo _mockCampaign(Campaign campaign) { + final campaignStage = + CampaignStage.fromCampaign(campaign, DateTimeExt.now()); + if (_adminToolsState.campaignStage == campaignStage) { + // campaign has already target stage, no need to mock it + return CampaignInfo.fromCampaign(campaign, DateTimeExt.now()); + } else { + return CampaignInfo.mockStageFromCampaign( + campaign, + _adminToolsState.campaignStage, + ); + } + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info_state.dart new file mode 100644 index 00000000000..5e14d03bda7 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/info/campaign_info_state.dart @@ -0,0 +1,16 @@ +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +/// The state of the campaign. +final class CampaignInfoState extends Equatable { + final bool isLoading; + final CampaignInfo? campaign; + + const CampaignInfoState({ + this.isLoading = false, + this.campaign, + }); + + @override + List get props => [isLoading, campaign]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/catalyst_voices_blocs.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/catalyst_voices_blocs.dart index 12654d8b2f3..eb7919e93ea 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/catalyst_voices_blocs.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/catalyst_voices_blocs.dart @@ -1,6 +1,11 @@ -export 'authentication/authentication.dart'; +export 'admin_tools/admin_tools.dart'; export 'bloc_error_emitter_mixin.dart'; export 'brand/brand.dart'; -export 'login/login.dart'; +export 'campaign/campaign_builder/campaign_builder.dart'; +export 'campaign/details/campaign_details.dart'; +export 'campaign/info/campaign_info.dart'; +export 'proposal_builder/proposal_builder.dart'; +export 'proposals/proposals.dart'; export 'registration/registration.dart'; export 'session/session.dart'; +export 'workspace/workspace.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login.dart deleted file mode 100644 index ef50ddcb013..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login.dart +++ /dev/null @@ -1 +0,0 @@ -export 'login_bloc.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_bloc.dart deleted file mode 100644 index 2131dfaee76..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_bloc.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:formz/formz.dart'; - -part 'login_event.dart'; -part 'login_state.dart'; - -final class LoginBloc extends Bloc { - final AuthenticationRepository _authenticationRepository; - - LoginBloc({ - required AuthenticationRepository authenticationRepository, - }) : _authenticationRepository = authenticationRepository, - super(LoginState()) { - on(_onEmailChanged); - on(_onPasswordChanged); - on(_onSubmitted); - } - - void _onEmailChanged( - LoginEmailChanged event, - Emitter emit, - ) { - final email = Email.dirty(event.email); - final isValid = Formz.validate([email, state.password]); - emit( - state.copyWith( - email: email, - status: isValid ? FormzSubmissionStatus.success : null, - ), - ); - } - - void _onPasswordChanged( - LoginPasswordChanged event, - Emitter emit, - ) { - final password = Password.dirty(event.password); - final isValid = Formz.validate([password, state.email]); - emit( - state.copyWith( - password: password, - status: isValid ? FormzSubmissionStatus.success : null, - ), - ); - } - - Future _onSubmitted( - LoginSubmitted event, - Emitter emit, - ) async { - if (state.isValid) { - emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); - try { - if (_validateTempCredentials( - email: state.email.value, - password: state.password.value, - )) { - await _authenticationRepository.signIn( - email: state.email.value, - password: state.password.value, - ); - - emit(state.copyWith(status: FormzSubmissionStatus.success)); - } else { - emit(state.copyWith(status: FormzSubmissionStatus.failure)); - } - } catch (_) { - emit(state.copyWith(status: FormzSubmissionStatus.failure)); - } - } - } - - bool _validateTempCredentials({ - required String email, - required String password, - }) { - return email == _TempConstants.email && password == _TempConstants.password; - } -} - -abstract class _TempConstants { - static const email = 'mail@example.com'; - static const password = 'MyPass123'; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_event.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_event.dart deleted file mode 100644 index 55d0c6d50e8..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_event.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'login_bloc.dart'; - -final class LoginEmailChanged extends LoginEvent { - final String email; - - const LoginEmailChanged(this.email); - - @override - List get props => [email]; -} - -abstract final class LoginEvent extends Equatable { - const LoginEvent(); - - @override - List get props => []; -} - -final class LoginPasswordChanged extends LoginEvent { - final String password; - - const LoginPasswordChanged(this.password); - - @override - List get props => [password]; -} - -final class LoginSubmitted extends LoginEvent { - const LoginSubmitted(); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_state.dart deleted file mode 100644 index 3f4bdf19f91..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/login/login_state.dart +++ /dev/null @@ -1,35 +0,0 @@ -part of 'login_bloc.dart'; - -final class LoginState extends Equatable with FormzMixin { - final FormzSubmissionStatus status; - final Email email; - final Password password; - - LoginState({ - this.status = FormzSubmissionStatus.initial, - this.email = const Email.pure(), - this.password = const Password.pure(), - }); - - @override - List> get inputs => [email, password]; - - @override - List get props => [ - status, - email, - password, - ]; - - LoginState copyWith({ - FormzSubmissionStatus? status, - Email? email, - Password? password, - }) { - return LoginState( - status: status ?? this.status, - email: email ?? this.email, - password: password ?? this.password, - ); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder.dart new file mode 100644 index 00000000000..15d346cda9d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder.dart @@ -0,0 +1,3 @@ +export 'proposal_builder_bloc.dart'; +export 'proposal_builder_event.dart'; +export 'proposal_builder_state.dart'; 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 new file mode 100644 index 00000000000..2d6c84e30c0 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart @@ -0,0 +1,117 @@ +import 'package:catalyst_voices_blocs/src/proposal_builder/proposal_builder_event.dart'; +import 'package:catalyst_voices_blocs/src/proposal_builder/proposal_builder_state.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +final class ProposalBuilderBloc + extends Bloc { + final CampaignService _campaignService; + + final _answers = {}; + final _guidances = >{}; + + String? proposalId; + SectionStepId? _activeStepId; + + ProposalBuilderBloc( + this._campaignService, + ) : super(const ProposalBuilderState()) { + on(_loadProposal); + on(_updateStepAnswer); + on(_handleActiveStepEvent); + } + + Future _loadProposal( + LoadProposalEvent event, + Emitter emit, + ) async { + _answers.clear(); + _guidances.clear(); + + final activeCampaign = await _campaignService.getActiveCampaign(); + if (activeCampaign == null) { + emit( + state.copyWith( + sections: [], + guidance: const ProposalGuidance(isNoneSelected: true), + ), + ); + return; + } + + final template = activeCampaign.proposalTemplate; + + final sections = template.sections.map(_mapProposalSection).toList(); + + for (final section in template.sections) { + for (final step in section.steps) { + final id = (sectionId: section.id, stepId: step.id); + _guidances[id] = step.guidances; + } + } + + final activeStepId = _activeStepId; + final guidances = _guidances[activeStepId] ?? []; + final guidance = ProposalGuidance( + isNoneSelected: activeStepId == null, + guidances: guidances, + ); + + emit( + state.copyWith( + sections: sections, + guidance: guidance, + ), + ); + } + + void _updateStepAnswer( + UpdateStepAnswerEvent event, + Emitter emit, + ) { + final answer = event.data; + if (answer != null) { + _answers[event.id] = answer; + } else { + _answers.remove(event.id); + } + } + + void _handleActiveStepEvent( + ActiveStepChangedEvent event, + Emitter emit, + ) { + _activeStepId = event.id; + + final activeStepId = _activeStepId; + final guidances = _guidances[activeStepId] ?? []; + final guidance = ProposalGuidance( + isNoneSelected: activeStepId == null, + guidances: guidances, + ); + + emit(state.copyWith(guidance: guidance)); + } + + WorkspaceSection _mapProposalSection(ProposalSection section) { + return WorkspaceSection( + id: section.id, + name: section.name, + steps: section.steps.map( + (step) { + final id = (sectionId: section.id, stepId: step.id); + + return RichTextStep( + id: step.id, + sectionId: section.id, + name: step.name, + description: step.description, + initialData: _answers[id] ?? step.answer, + ); + }, + ).toList(), + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_event.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_event.dart new file mode 100644 index 00000000000..7fad1af0ce7 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_event.dart @@ -0,0 +1,40 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +sealed class ProposalBuilderEvent extends Equatable { + const ProposalBuilderEvent(); +} + +final class LoadProposalEvent extends ProposalBuilderEvent { + final String id; + + const LoadProposalEvent({ + required this.id, + }); + + @override + List get props => [id]; +} + +final class UpdateStepAnswerEvent extends ProposalBuilderEvent { + final SectionStepId id; + final MarkdownData? data; + + const UpdateStepAnswerEvent({ + required this.id, + this.data, + }); + + @override + List get props => [id, data]; +} + +final class ActiveStepChangedEvent extends ProposalBuilderEvent { + final SectionStepId? id; + + const ActiveStepChangedEvent(this.id); + + @override + List get props => [id]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart new file mode 100644 index 00000000000..07eb3ce027b --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_state.dart @@ -0,0 +1,47 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +final class ProposalBuilderState extends Equatable { + final List
sections; + final ProposalGuidance guidance; + + const ProposalBuilderState({ + this.sections = const [], + this.guidance = const ProposalGuidance(), + }); + + ProposalBuilderState copyWith({ + List
? sections, + ProposalGuidance? guidance, + }) { + return ProposalBuilderState( + sections: sections ?? this.sections, + guidance: guidance ?? this.guidance, + ); + } + + @override + List get props => [ + sections, + guidance, + ]; +} + +final class ProposalGuidance extends Equatable { + final bool isNoneSelected; + final List guidances; + + const ProposalGuidance({ + this.isNoneSelected = false, + this.guidances = const [], + }); + + bool get showEmptyState => !isNoneSelected && guidances.isEmpty; + + @override + List get props => [ + isNoneSelected, + guidances, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals.dart new file mode 100644 index 00000000000..d073c3c941c --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals.dart @@ -0,0 +1,2 @@ +export 'proposals_cubit.dart'; +export 'proposals_state.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_cubit.dart new file mode 100644 index 00000000000..f5a3dea974a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_cubit.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Manages the proposals. +final class ProposalsCubit extends Cubit { + final CampaignService _campaignService; + final ProposalService _proposalService; + final AdminTools _adminTools; + + AdminToolsState _adminToolsState; + StreamSubscription? _adminToolsSub; + + ProposalsCubit( + this._campaignService, + this._proposalService, + this._adminTools, + ) : _adminToolsState = _adminTools.state, + super(const LoadingProposalsState()) { + _adminToolsSub = _adminTools.stream.listen(_onAdminToolsChanged); + } + + /// Loads the proposals. + Future load() async { + emit(const LoadingProposalsState()); + + final campaign = await _campaignService.getActiveCampaign(); + if (campaign == null) { + emit(const LoadedProposalsState()); + return; + } + + final proposals = await _loadProposals(campaign); + emit( + LoadedProposalsState( + proposals: proposals, + favoriteProposals: const [], + ), + ); + } + + /// Marks the proposal with [proposalId] as favorite. + Future onFavoriteProposal(String proposalId) async { + final loadedState = state; + if (loadedState is! LoadedProposalsState) return; + + final proposals = loadedState.proposals; + final favoriteProposal = + proposals.firstWhereOrNull((e) => e.id == proposalId); + if (favoriteProposal == null) return; + + emit( + LoadedProposalsState( + proposals: loadedState.proposals, + favoriteProposals: [ + ...loadedState.favoriteProposals, + favoriteProposal, + ], + ), + ); + } + + /// Unmarks the proposal with [proposalId] as favorite. + Future onUnfavoriteProposal(String proposalId) async { + final loadedState = state; + if (loadedState is! LoadedProposalsState) return; + + emit( + LoadedProposalsState( + proposals: loadedState.proposals, + favoriteProposals: loadedState.favoriteProposals + .whereNot((e) => e.id == proposalId) + .toList(), + ), + ); + } + + @override + Future close() async { + await _adminToolsSub?.cancel(); + _adminToolsSub = null; + + return super.close(); + } + + Future _onAdminToolsChanged(AdminToolsState adminTools) async { + _adminToolsState = adminTools; + await load(); + } + + Future> _loadProposals(Campaign campaign) { + if (_adminToolsState.enabled) { + return _loadMockedProposals(campaign); + } else { + return _loadRegularProposals( + campaignId: campaign.id, + campaignName: campaign.name, + campaignStage: CampaignStage.fromCampaign(campaign, DateTimeExt.now()), + ); + } + } + + Future> _loadMockedProposals( + Campaign campaign, + ) async { + switch (_adminToolsState.campaignStage) { + case CampaignStage.draft: + case CampaignStage.scheduled: + // no proposals yet at this stage + return []; + case CampaignStage.live: + case CampaignStage.completed: + return _loadRegularProposals( + campaignId: campaign.id, + campaignName: campaign.name, + campaignStage: _adminToolsState.campaignStage, + ); + } + } + + Future> _loadRegularProposals({ + required String campaignId, + required String campaignName, + required CampaignStage campaignStage, + }) async { + final proposals = await _proposalService.getProposals( + campaignId: campaignId, + ); + + return proposals + .map( + (proposal) => ProposalViewModel.fromProposalAtStage( + proposal: proposal, + campaignName: campaignName, + campaignStage: campaignStage, + ), + ) + .toList(); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_state.dart new file mode 100644 index 00000000000..d1a433e28f5 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposals/proposals_state.dart @@ -0,0 +1,29 @@ +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +/// The state of available proposals. +sealed class ProposalsState extends Equatable { + const ProposalsState(); +} + +/// The proposals are loading. +final class LoadingProposalsState extends ProposalsState { + const LoadingProposalsState(); + + @override + List get props => []; +} + +/// The loaded proposals. +final class LoadedProposalsState extends ProposalsState { + final List proposals; + final List favoriteProposals; + + const LoadedProposalsState({ + this.proposals = const [], + this.favoriteProposals = const [], + }); + + @override + List get props => [proposals, favoriteProposals]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart index df6bbbc3c6f..293531a2004 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/registration/cubits/recover_cubit.dart @@ -141,13 +141,15 @@ final class RecoverCubit extends Cubit } final lockFactor = PasswordLockFactor(password.value); - - await _registrationService.createKeychainFor( - account: account, + final masterKey = await _registrationService.deriveMasterKey( seedPhrase: seedPhrase, - lockFactor: lockFactor, ); + final keychain = account.keychain; + await keychain.setLock(lockFactor); + await keychain.unlock(lockFactor); + await keychain.setMasterKey(masterKey); + await _userService.useAccount(account); return true; @@ -162,7 +164,7 @@ final class RecoverCubit extends Cubit Future reset() async { final recoveredAccount = _recoveredAccount; if (recoveredAccount != null) { - await _userService.removeKeychain(recoveredAccount.keychainId); + await _userService.removeAccount(recoveredAccount); } _recoveredAccount = null; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart index 86adb2dbd5a..7c355ae7251 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_cubit.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:catalyst_voices_blocs/src/bloc_error_emitter_mixin.dart'; -import 'package:catalyst_voices_blocs/src/session/session_state.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -14,79 +14,71 @@ final class SessionCubit extends Cubit final UserService _userService; final RegistrationService _registrationService; final RegistrationProgressNotifier _registrationProgressNotifier; + final AccessControl _accessControl; + final AdminTools _adminTools; final _logger = Logger('SessionCubit'); - bool _hasKeychain = false; - bool _isUnlocked = false; Account? _account; + AdminToolsState _adminToolsState; - StreamSubscription? _keychainSub; StreamSubscription? _keychainUnlockedSub; StreamSubscription? _accountSub; - - final String _dummyKeychainId = 'TestUserKeychainID'; - static const LockFactor dummyUnlockFactor = PasswordLockFactor('Test1234'); - final _dummySeedPhrase = SeedPhrase.fromMnemonic( - 'few loyal swift champion rug peace dinosaur ' - 'erase bacon tone install universe', - ); + StreamSubscription? _adminToolsSub; SessionCubit( this._userService, this._registrationService, this._registrationProgressNotifier, - ) : super(const VisitorSessionState(isRegistrationInProgress: false)) { - _keychainSub = _userService.watchKeychain - .map((keychain) => keychain != null) - .distinct() - .listen(_onHasKeychainChanged); - - _keychainUnlockedSub = _userService.watchKeychain - .transform(KeychainToUnlockTransformer()) + this._accessControl, + this._adminTools, + ) : _adminToolsState = _adminTools.state, + super(const VisitorSessionState(isRegistrationInProgress: false)) { + _keychainUnlockedSub = _userService.watchAccount + .transform(AccountToKeychainUnlockTransformer()) .distinct() .listen(_onActiveKeychainUnlockChanged); _registrationProgressNotifier.addListener(_onRegistrationProgressChanged); _accountSub = _userService.watchAccount.listen(_onActiveAccountChanged); + + _adminToolsSub = _adminTools.stream.listen(_onAdminToolsChanged); } - Future unlock(LockFactor lockFactor) { - return _userService.keychain!.unlock(lockFactor); + Future unlock(LockFactor lockFactor) async { + final keychain = _userService.account?.keychain; + if (keychain == null) { + return false; + } + + return keychain.unlock(lockFactor); } Future lock() async { - await _userService.keychain!.lock(); + await _userService.account?.keychain.lock(); } - Future removeKeychain() { - return _userService.removeCurrentKeychain(); + Future removeKeychain() async { + final account = _userService.account; + if (account != null) { + await _userService.removeAccount(account); + } } Future switchToDummyAccount() async { - final keychains = await _userService.keychains; - final dummyKeychain = keychains - .firstWhereOrNull((keychain) => keychain.id == _dummyKeychainId); - if (dummyKeychain != null) { - await _userService.useKeychain(dummyKeychain.id); + final account = _userService.account; + if (account?.isDummy ?? false) { return; } - final account = await _registrationService.registerTestAccount( - keychainId: _dummyKeychainId, - seedPhrase: _dummySeedPhrase, - lockFactor: dummyUnlockFactor, - ); + final dummyAccount = await _getDummyAccount(); - await _userService.useAccount(account); + await _userService.useAccount(dummyAccount); } @override Future close() async { - await _keychainSub?.cancel(); - _keychainSub = null; - await _keychainUnlockedSub?.cancel(); _keychainUnlockedSub = null; @@ -96,50 +88,104 @@ final class SessionCubit extends Cubit await _accountSub?.cancel(); _accountSub = null; + await _adminToolsSub?.cancel(); + _adminToolsSub = null; + return super.close(); } - void _onHasKeychainChanged(bool hasKeychain) { - _logger.fine('Has keychain changed [$hasKeychain]'); + void _onActiveAccountChanged(Account? account) { + _logger.fine('Active account changed [$account]'); + + _account = account; - _hasKeychain = hasKeychain; _updateState(); } void _onActiveKeychainUnlockChanged(bool isUnlocked) { _logger.fine('Keychain unlock changed [$isUnlocked]'); - _isUnlocked = isUnlocked; _updateState(); } - void _onActiveAccountChanged(Account? account) { - _logger.fine('Active account changed [$account]'); - - _account = account; + void _onRegistrationProgressChanged() { _updateState(); } - void _onRegistrationProgressChanged() { + void _onAdminToolsChanged(AdminToolsState adminTools) { + _logger.fine('Admin tools changed: $adminTools'); + + _adminToolsState = adminTools; _updateState(); } void _updateState() { - final hasKeychain = _hasKeychain; - final isUnlocked = _isUnlocked; + if (_adminToolsState.enabled) { + unawaited( + _createMockedSessionState().then((value) { + if (!isClosed) { + emit(value); + } + }), + ); + } else { + emit(_createSessionState()); + } + } + + SessionState _createSessionState() { final account = _account; + final isUnlocked = _account?.keychain.lastIsUnlocked ?? false; - if (!hasKeychain) { + if (account == null) { final isEmpty = _registrationProgressNotifier.value.isEmpty; - emit(VisitorSessionState(isRegistrationInProgress: !isEmpty)); - return; + return VisitorSessionState(isRegistrationInProgress: !isEmpty); } if (!isUnlocked) { - emit(const GuestSessionState()); - return; + return const GuestSessionState(); } - emit(ActiveAccountSessionState(account: account)); + final spaces = _accessControl.spacesAccess(account); + final overallSpaces = _accessControl.overallSpaces(account); + final spacesShortcuts = _accessControl.spacesShortcutsActivators(account); + + return ActiveAccountSessionState( + account: account, + spaces: spaces, + overallSpaces: overallSpaces, + spacesShortcuts: spacesShortcuts, + ); + } + + Future _createMockedSessionState() async { + switch (_adminToolsState.sessionStatus) { + case SessionStatus.actor: + // TODO(damian-molinski): Limiting exposed Account so its not future. + final dummyAccount = await _getDummyAccount(); + + return ActiveAccountSessionState( + account: dummyAccount, + spaces: Space.values, + overallSpaces: Space.values, + spacesShortcuts: AccessControl.allSpacesShortcutsActivators, + ); + case SessionStatus.guest: + return const GuestSessionState(); + case SessionStatus.visitor: + return const VisitorSessionState(isRegistrationInProgress: false); + } + } + + Future _getDummyAccount() async { + final dummyAccount = + _userService.accounts.firstWhereOrNull((e) => e.isDummy); + + return dummyAccount ?? + await _registrationService.registerTestAccount( + keychainId: Account.dummyKeychainId, + seedPhrase: Account.dummySeedPhrase, + lockFactor: Account.dummyUnlockFactor, + ); } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_state.dart index e4578590986..08d97a3262a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_state.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/session/session_state.dart @@ -1,9 +1,17 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; /// Determines the state of the user session. sealed class SessionState extends Equatable { const SessionState(); + + /// Returns a list of all available spaces + /// corresponding to the current session state. + List get spaces; + + /// Returns a list of [spaces] that should be shown in overall spaces menu. + List get overallSpaces; } /// The user hasn't registered yet nor setup the keychain. @@ -14,6 +22,14 @@ final class VisitorSessionState extends SessionState { required this.isRegistrationInProgress, }); + @override + List get spaces => const [Space.discovery]; + + @override + List get overallSpaces => const [ + // not supported + ]; + @override List get props => [ isRegistrationInProgress, @@ -24,20 +40,47 @@ final class VisitorSessionState extends SessionState { final class GuestSessionState extends SessionState { const GuestSessionState(); + @override + List get spaces => const [Space.discovery]; + + @override + List get overallSpaces => const [ + // not supported + ]; + @override List get props => []; } /// The user has registered and unlocked the keychain. final class ActiveAccountSessionState extends SessionState { + // TODO(damian-molinski): Try limiting exposed Account to something smaller. final Account? account; + @override + final List spaces; + @override + final List overallSpaces; + final Map spacesShortcuts; const ActiveAccountSessionState({ this.account, + required this.spaces, + required this.overallSpaces, + required this.spacesShortcuts, }); @override List get props => [ account, + spaces, + overallSpaces, + spacesShortcuts, ]; } + +extension SessionStateExt on SessionState { + Account? get account => switch (this) { + ActiveAccountSessionState(:final account) => account, + _ => null, + }; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace.dart new file mode 100644 index 00000000000..e5b20e31001 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace.dart @@ -0,0 +1,3 @@ +export 'workspace_bloc.dart'; +export 'workspace_event.dart'; +export 'workspace_state.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart new file mode 100644 index 00000000000..1b0382bca93 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_bloc.dart @@ -0,0 +1,99 @@ +import 'dart:math'; + +import 'package:catalyst_voices_blocs/src/workspace/workspace_event.dart'; +import 'package:catalyst_voices_blocs/src/workspace/workspace_state.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +final class WorkspaceBloc extends Bloc { + // ignore: unused_field + final CampaignService _campaignService; + + // ignore: unused_field + final List _proposals = []; + + WorkspaceBloc( + this._campaignService, + ) : super(const WorkspaceState()) { + on(_loadProposals); + on(_handleTabChange); + on( + _handleQueryChange, + // TODO(damian-molinski): implement debounce + transformer: null, + ); + } + + Future createNewDraftProposal() async { + return 'new-draft-id'; + } + + Future _loadProposals( + LoadProposalsEvent event, + Emitter emit, + ) async { + emit( + state.copyWith( + isLoading: true, + draftProposalCount: 0, + finalProposalCount: 0, + proposals: const [], + error: const Optional.empty(), + ), + ); + + // TODO(damian-molinski): implement fetching proposals + // TODO(damian-molinski): implement filtering of _proposals + + final isSuccess = await Future.delayed( + const Duration(milliseconds: 300), + () => Random().nextBool(), + ); + if (isClosed) return; + + final proposals = isSuccess + ? List.generate( + 20, + (index) => WorkspaceProposalListItem( + id: '$index', + name: 'Proposal [${index + 1}]', + ), + ) + : const []; + + final LocalizedException? error = + isSuccess ? null : const LocalizedUnknownException(); + + final newState = state.copyWith( + isLoading: false, + draftProposalCount: isSuccess ? 2 : 0, + finalProposalCount: isSuccess ? 1 : 0, + proposals: proposals, + error: Optional(error), + ); + + emit(newState); + } + + Future _handleTabChange( + TabChangedEvent event, + Emitter emit, + ) async { + // TODO(damian-molinski): implement filtering of _proposals + + emit(state.copyWith(tab: event.tab)); + } + + Future _handleQueryChange( + SearchQueryChangedEvent event, + Emitter emit, + ) async { + // TODO(damian-molinski): implement filtering of _proposals + + final query = event.query; + + emit(state.copyWith(searchQuery: query)); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_event.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_event.dart new file mode 100644 index 00000000000..ead30f41e1e --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_event.dart @@ -0,0 +1,38 @@ +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +sealed class WorkspaceEvent extends Equatable { + const WorkspaceEvent(); +} + +final class LoadProposalsEvent extends WorkspaceEvent { + const LoadProposalsEvent(); + + @override + List get props => []; +} + +final class TabChangedEvent extends WorkspaceEvent { + final WorkspaceTabType tab; + + const TabChangedEvent(this.tab); + + @override + List get props => [tab]; +} + +final class SearchQueryChangedEvent extends WorkspaceEvent { + final String query; + final bool isSubmitted; + + const SearchQueryChangedEvent( + this.query, { + this.isSubmitted = false, + }); + + @override + List get props => [ + query, + isSubmitted, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_state.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_state.dart new file mode 100644 index 00000000000..a6198704495 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/workspace/workspace_state.dart @@ -0,0 +1,60 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:equatable/equatable.dart'; + +final class WorkspaceState extends Equatable { + final WorkspaceTabType tab; + final bool isLoading; + final int draftProposalCount; + final int finalProposalCount; + final String searchQuery; + final List proposals; + final LocalizedException? error; + + const WorkspaceState({ + this.tab = WorkspaceTabType.draftProposal, + this.isLoading = false, + this.draftProposalCount = 0, + this.finalProposalCount = 0, + this.searchQuery = '', + this.proposals = const [], + this.error, + }); + + bool get showProposals => !isLoading && proposals.isNotEmpty && error == null; + + bool get showEmptyState => !isLoading && proposals.isEmpty && error == null; + + bool get showError => !isLoading && error != null; + + WorkspaceState copyWith({ + WorkspaceTabType? tab, + bool? isLoading, + int? draftProposalCount, + int? finalProposalCount, + String? searchQuery, + List? proposals, + Optional? error, + }) { + return WorkspaceState( + tab: tab ?? this.tab, + isLoading: isLoading ?? this.isLoading, + draftProposalCount: draftProposalCount ?? this.draftProposalCount, + finalProposalCount: finalProposalCount ?? this.finalProposalCount, + searchQuery: searchQuery ?? this.searchQuery, + proposals: proposals ?? this.proposals, + error: error.dataOr(this.error), + ); + } + + @override + List get props => [ + tab, + isLoading, + draftProposalCount, + finalProposalCount, + searchQuery, + proposals, + error, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_blocs/pubspec.yaml index f420d231cf7..91f9da1cf2d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/pubspec.yaml @@ -42,3 +42,5 @@ dev_dependencies: sdk: flutter mocktail: ^1.0.1 plugin_platform_interface: ^2.1.8 + shared_preferences: ^2.3.3 + shared_preferences_platform_interface: ^2.4.1 diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/admin_tools/admin_tools_cubit_test.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/admin_tools/admin_tools_cubit_test.dart new file mode 100644 index 00000000000..fc402bd06c4 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/admin_tools/admin_tools_cubit_test.dart @@ -0,0 +1,54 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(AdminToolsCubit, () { + blocTest( + 'initial state is disabled', + build: AdminToolsCubit.new, + verify: (cubit) => expect(cubit.state.enabled, isFalse), + ); + + blocTest( + 'enable() enables admin tools', + build: AdminToolsCubit.new, + act: (cubit) => cubit.enable(), + verify: (cubit) => expect(cubit.state.enabled, isTrue), + ); + + blocTest( + 'enable() and disable() keeps admin tools disabled', + build: AdminToolsCubit.new, + act: (cubit) { + cubit + ..enable() + ..disable(); + }, + verify: (cubit) => expect(cubit.state.enabled, isFalse), + ); + + blocTest( + 'updateCampaignStage() update campaign stage', + build: AdminToolsCubit.new, + act: (cubit) { + cubit.updateCampaignStage(CampaignStage.completed); + }, + verify: (cubit) { + expect(cubit.state.campaignStage, equals(CampaignStage.completed)); + }, + ); + + blocTest( + 'updateSessionStatus() update session status', + build: AdminToolsCubit.new, + act: (cubit) { + cubit.updateSessionStatus(SessionStatus.visitor); + }, + verify: (cubit) { + expect(cubit.state.sessionStatus, equals(SessionStatus.visitor)); + }, + ); + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/campaign/info/campaign_info_cubit_test.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/campaign/info/campaign_info_cubit_test.dart new file mode 100644 index 00000000000..78b6561917a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/campaign/info/campaign_info_cubit_test.dart @@ -0,0 +1,112 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(CampaignInfoCubit, () { + final campaign = Campaign( + id: 'campaign-id', + name: 'name', + description: 'description', + startDate: DateTime.now(), + endDate: DateTime.now().plusDays(2), + proposalsCount: 0, + sections: const [], + publish: CampaignPublish.draft, + proposalTemplate: const ProposalTemplate(sections: []), + ); + + final campaignStage = CampaignStage.fromCampaign( + campaign, + DateTimeExt.now(), + ); + + late CampaignService campaignService; + late AdminToolsCubit adminToolsCubit; + + setUp(() { + campaignService = _FakeCampaignService(campaign); + adminToolsCubit = AdminToolsCubit(); + }); + + blocTest( + 'load should fetch the active campaign', + build: () => CampaignInfoCubit(campaignService, adminToolsCubit), + act: (cubit) async => cubit.load(), + verify: (cubit) { + expect(campaignStage, equals(CampaignStage.draft)); + expect( + cubit.state.campaign?.stage, + equals(campaignStage), + ); + }, + ); + + blocTest( + 'load should work when there is no active campaign', + build: () => CampaignInfoCubit( + _FakeCampaignService(null), + adminToolsCubit, + ), + act: (cubit) async => cubit.load(), + verify: (cubit) { + expect(cubit.state.campaign, isNull); + }, + ); + + blocTest( + 'given admin tools enabled should not override ' + 'campaign stage if campaign already has this stage', + build: () => CampaignInfoCubit(campaignService, adminToolsCubit), + act: (cubit) { + adminToolsCubit.emit( + AdminToolsState( + enabled: true, + campaignStage: campaignStage, + ), + ); + }, + verify: (cubit) { + expect(campaignStage, equals(CampaignStage.draft)); + expect( + cubit.state.campaign?.stage, + equals(campaignStage), + ); + }, + ); + + blocTest( + 'given admin tools enabled should override campaign stage', + build: () => CampaignInfoCubit(campaignService, adminToolsCubit), + act: (cubit) { + adminToolsCubit.emit( + const AdminToolsState( + enabled: true, + campaignStage: CampaignStage.completed, + ), + ); + }, + verify: (cubit) { + expect( + cubit.state.campaign?.stage, + equals(CampaignStage.completed), + ); + }, + ); + }); +} + +class _FakeCampaignService extends Fake implements CampaignService { + final Campaign? _campaign; + + _FakeCampaignService(this._campaign); + + @override + Future getActiveCampaign() async { + return _campaign; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart new file mode 100644 index 00000000000..bb7d31a76f0 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/proposals/proposals_cubit_test.dart @@ -0,0 +1,203 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group(ProposalsCubit, () { + final campaign = Campaign( + id: 'F14', + name: 'campaign', + description: 'description', + startDate: DateTime.now(), + endDate: DateTime.now().plusDays(1), + proposalsCount: 0, + sections: const [], + publish: CampaignPublish.published, + proposalTemplate: const ProposalTemplate(sections: []), + ); + + final proposal = Proposal( + id: '1', + title: 'Proposal 1', + description: 'Description 1', + category: '', + updateDate: DateTime.now(), + fundsRequested: const Coin(100000), + status: ProposalStatus.draft, + publish: ProposalPublish.draft, + access: ProposalAccess.private, + commentsCount: 0, + sections: List.generate(3, (index) { + return ProposalSection( + id: 'f14/0_$index', + name: 'Section_$index', + steps: [ + ProposalSectionStep( + id: 'f14/0_${index}_1', + name: 'Topic 1', + answer: index < 1 ? const MarkdownData('Ans') : null, + ), + ], + ); + }), + ); + + final pendingProposal = PendingProposal.fromProposal( + proposal, + campaignName: campaign.name, + ); + + late AdminToolsCubit adminToolsCubit; + + setUp(() { + adminToolsCubit = AdminToolsCubit(); + }); + + blocTest( + 'initial state is $LoadingProposalsState', + build: () { + return ProposalsCubit( + _FakeCampaignService(campaign), + _FakeProposalService([]), + adminToolsCubit, + ); + }, + verify: (cubit) { + expect(cubit.state, equals(const LoadingProposalsState())); + }, + ); + + blocTest( + 'load emits $LoadedProposalsState with proposals', + build: () { + return ProposalsCubit( + _FakeCampaignService(campaign), + _FakeProposalService([proposal]), + adminToolsCubit, + ); + }, + act: (cubit) async => cubit.load(), + expect: () => [ + const LoadingProposalsState(), + LoadedProposalsState( + proposals: [pendingProposal], + favoriteProposals: const [], + ), + ], + ); + + blocTest( + 'admin tools override proposals in draft campaign state', + build: () { + return ProposalsCubit( + _FakeCampaignService(campaign), + _FakeProposalService([proposal]), + adminToolsCubit, + ); + }, + act: (cubit) async { + adminToolsCubit.emit( + const AdminToolsState( + enabled: true, + campaignStage: CampaignStage.draft, + ), + ); + return Future.delayed(const Duration(microseconds: 50)); + }, + expect: () => [ + const LoadingProposalsState(), + const LoadedProposalsState( + proposals: [], + favoriteProposals: [], + ), + ], + ); + + blocTest( + 'admin tools override proposals in live campaign state', + build: () { + return ProposalsCubit( + _FakeCampaignService(campaign), + _FakeProposalService([proposal]), + adminToolsCubit, + ); + }, + act: (cubit) async { + adminToolsCubit.emit( + const AdminToolsState( + enabled: true, + campaignStage: CampaignStage.live, + ), + ); + return Future.delayed(const Duration(microseconds: 50)); + }, + expect: () => [ + const LoadingProposalsState(), + LoadedProposalsState( + proposals: [pendingProposal], + favoriteProposals: const [], + ), + ], + ); + + blocTest( + 'onFavoriteProposal / onUnfavoriteProposal adds/removes proposal from favorites', + build: () { + return ProposalsCubit( + _FakeCampaignService(campaign), + _FakeProposalService([proposal]), + adminToolsCubit, + ); + }, + act: (cubit) async { + await cubit.load(); + await cubit.onFavoriteProposal(proposal.id); + await cubit.onUnfavoriteProposal(proposal.id); + }, + expect: () => [ + const LoadingProposalsState(), + LoadedProposalsState( + proposals: [pendingProposal], + favoriteProposals: const [], + ), + LoadedProposalsState( + proposals: [pendingProposal], + favoriteProposals: [pendingProposal], + ), + LoadedProposalsState( + proposals: [pendingProposal], + favoriteProposals: const [], + ), + ], + ); + }); +} + +class _FakeCampaignService extends Fake implements CampaignService { + final Campaign? _campaign; + + _FakeCampaignService(this._campaign); + + @override + Future getActiveCampaign() async { + return _campaign; + } +} + +class _FakeProposalService extends Fake implements ProposalService { + final List _proposals; + + _FakeProposalService(this._proposals); + + @override + Future> getProposals({ + required String campaignId, + }) async { + return _proposals; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart index ae3569baa48..c4acaa934ba 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/test/session/session_cubit_test.dart @@ -1,52 +1,91 @@ -import 'package:catalyst_voices_blocs/src/session/session.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; void main() { late final KeychainProvider keychainProvider; - late final UserStorage userStorage; + late final UserRepository userRepository; late final UserService userService; late final RegistrationService registrationService; late final RegistrationProgressNotifier notifier; + late final AccessControl accessControl; + late AdminToolsCubit adminToolsCubit; late SessionCubit sessionCubit; setUpAll(() { - keychainProvider = VaultKeychainProvider(); - userStorage = SecureUserStorage(); + FlutterSecureStorage.setMockInitialValues({}); + + final store = InMemorySharedPreferencesAsync.empty(); + SharedPreferencesAsyncPlatform.instance = store; + + keychainProvider = VaultKeychainProvider( + secureStorage: const FlutterSecureStorage(), + sharedPreferences: SharedPreferencesAsync(), + cacheConfig: const CacheConfig(), + ); + userRepository = UserRepository( + SecureUserStorage(), + keychainProvider, + ); userService = UserService( - keychainProvider: keychainProvider, - userStorage: userStorage, + userRepository: userRepository, ); - registrationService = _MockRegistrationService(); + registrationService = _MockRegistrationService(keychainProvider); notifier = RegistrationProgressNotifier(); + accessControl = const AccessControl(); }); setUp(() { - FlutterSecureStorage.setMockInitialValues({}); - sessionCubit = SessionCubit(userService, registrationService, notifier); + // each test might emit using this cubit, therefore we reset it here + adminToolsCubit = AdminToolsCubit(); + + sessionCubit = SessionCubit( + userService, + registrationService, + notifier, + accessControl, + adminToolsCubit, + ); }); tearDown(() async { await sessionCubit.close(); + final user = await userService.getUser(); + for (final account in user.accounts) { + await userService.removeAccount(account); + } + + await const FlutterSecureStorage().deleteAll(); + await SharedPreferencesAsync().clear(); + reset(registrationService); }); group(SessionCubit, () { - test('when no keychain is found session is in Visitor state', () async { + test('when no account is found session is in Visitor state', () async { // Given // When - await userService.removeCurrentKeychain(); + final account = userService.account; + if (account != null) { + await userService.removeAccount(account); + } // Then - expect(userService.keychain, isNull); + expect(userService.account, isNull); expect(sessionCubit.state, isA()); }); @@ -54,13 +93,16 @@ void main() { // Given // When - await userService.removeCurrentKeychain(); + final account = userService.account; + if (account != null) { + await userService.removeAccount(account); + } // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); // Then - expect(userService.keychain, isNull); + expect(userService.account, isNull); expect(sessionCubit.state, isA()); expect( sessionCubit.state, @@ -80,13 +122,16 @@ void main() { // When notifier.value = RegistrationProgress(keychainProgress: keychainProgress); - await userService.removeCurrentKeychain(); + final account = userService.account; + if (account != null) { + await userService.removeAccount(account); + } // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); // Then - expect(userService.keychain, isNull); + expect(userService.account, isNull); expect(sessionCubit.state, isA()); expect( sessionCubit.state, @@ -104,13 +149,16 @@ void main() { await keychain.setLock(lockFactor); await keychain.lock(); - await userService.useKeychain(keychainId); + final account = Account.dummy(keychain: keychain); + + await userService.useAccount(account); // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); // Then - expect(userService.keychain, isNotNull); + expect(userService.account, isNotNull); + expect(userService.account?.id, account.id); expect(sessionCubit.state, isNot(isA())); expect(sessionCubit.state, isA()); }); @@ -124,19 +172,54 @@ void main() { final keychain = await keychainProvider.create(keychainId); await keychain.setLock(lockFactor); - await userService.useKeychain(keychainId); - await userService.keychain?.unlock(lockFactor); + final account = Account.dummy(keychain: keychain); + + await userService.useAccount(account); + await account.keychain.unlock(lockFactor); // Gives time for stream to emit. await Future.delayed(const Duration(milliseconds: 100)); // Then - expect(userService.keychain, isNotNull); + expect(userService.account, isNotNull); expect(sessionCubit.state, isNot(isA())); expect(sessionCubit.state, isNot(isA())); expect(sessionCubit.state, isA()); }); + + test('when admin tools enabled is in mocked state', () async { + adminToolsCubit.emit( + const AdminToolsState( + enabled: true, + campaignStage: CampaignStage.scheduled, + sessionStatus: SessionStatus.actor, + ), + ); + + // Gives time for stream to emit. + await Future.delayed(const Duration(milliseconds: 100)); + + expect(sessionCubit.state, isA()); + }); }); } -class _MockRegistrationService extends Mock implements RegistrationService {} +class _MockRegistrationService extends Mock implements RegistrationService { + final KeychainProvider keychainProvider; + + _MockRegistrationService(this.keychainProvider); + + @override + Future registerTestAccount({ + required String keychainId, + required SeedPhrase seedPhrase, + required LockFactor lockFactor, + }) async { + final keychain = await keychainProvider.create(keychainId); + + await keychain.setLock(lockFactor); + await keychain.unlock(lockFactor); + + return Account.dummy(keychain: keychain); + } +} 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 de5f9c1f9a1..8d8ee0acd67 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 @@ -48,18 +48,6 @@ "@passwordErrorText": { "description": "Text shown in password field when input is invalid" }, - "loginTitleText": "Login", - "@loginTitleText": { - "description": "Text shown in the login screen title" - }, - "loginButtonText": "Login", - "@loginButtonText": { - "description": "Text shown in the login screen for the login button" - }, - "loginScreenErrorMessage": "Wrong credentials", - "@loginScreenErrorMessage": { - "description": "Text shown in the login screen when the user enters wrong credentials" - }, "homeScreenText": "Catalyst Voices", "@homeScreenText": { "description": "Text shown in the home screen" @@ -151,28 +139,28 @@ "@proposalStatusReady": { "description": "Indicates to user that status is in ready mode" }, - "proposalStatusDraft": "Draft", - "@proposalStatusDraft": { + "draft": "Draft", + "@draft": { "description": "Indicates to user that status is in draft mode" }, - "proposalStatusInProgress": "In progress", - "@proposalStatusInProgress": { + "inProgress": "In progress", + "@inProgress": { "description": "Indicates to user that status is in progress" }, - "proposalStatusPrivate": "Private", - "@proposalStatusPrivate": { + "private": "Private", + "@private": { "description": "Indicates to user that status is in private mode" }, - "proposalStatusLive": "LIVE", - "@proposalStatusLive": { + "live": "Live", + "@live": { "description": "Indicates to user that status is in live mode" }, - "proposalStatusCompleted": "Completed", - "@proposalStatusCompleted": { + "completed": "Completed", + "@completed": { "description": "Indicates to user that status is completed" }, - "proposalStatusOpen": "Open", - "@proposalStatusOpen": { + "open": "Open", + "@open": { "description": "Indicates to user that status is in open mode" }, "fundedProposal": "Funded proposal", @@ -275,13 +263,53 @@ "@treasuryCampaignBuilderSegments": { "description": "Tab name in campaign builder panel" }, - "treasuryCampaignSetup": "Setup Campaign", - "@treasuryCampaignSetup": { + "treasuryCreateCampaign": "Create Campaign", + "@treasuryCreateCampaign": { "description": "Segment name" }, - "treasuryCampaignTitle": "Campaign title", - "@treasuryCampaignTitle": { - "description": "Campaign title" + "setupCampaignDetails": "Setup Campaign Details", + "@setupCampaignDetails": { + "description": "Segment step for entering campaign details." + }, + "setupCampaignStages": "Setup Campaign Stages", + "@setupCampaignStages": { + "description": "Segment step for entering campaign start and end dates." + }, + "setupCampaignStagesTimezone": "You are setting date and times using this Timezone:", + "@setupCampaignStagesTimezone": { + "description": "The description of a timezone in which the user sets campaign stages." + }, + "startAndEndDates": "Start & End Dates", + "@startAndEndDates": { + "description": "Refers to a date & time range (start & end)." + }, + "campaignStart": "Campaign Start", + "@campaignStart": { + "description": "Label for the campaign start date." + }, + "campaignEnd": "Campaign End", + "@campaignEnd": { + "description": "Label for the campaign end date." + }, + "campaignDates": "Campaign Dates", + "@campaignDates": { + "description": "Label for the campaign start & end date." + }, + "noDateTimeSelected": "No date & time selected", + "@noDateTimeSelected": { + "description": "Placeholder when date & time not selected." + }, + "setupBaseProposalTemplate": "Setup Base Proposal Template", + "@setupBaseProposalTemplate": { + "description": "Segment step for entering a proposal template for a campaign." + }, + "setupBaseQuestions": "Setup Base Questions", + "@setupBaseQuestions": { + "description": "Segment description for entering a proposal template for a campaign." + }, + "setupCategories": "Setup Categories", + "@setupCategories": { + "description": "Segment step for entering campaign categories." }, "stepEdit": "Edit", "@stepEdit": { @@ -392,6 +420,10 @@ "@visitor": { "description": "Refers to user that created keychain but is locked" }, + "actor": "Actor", + "@actor": { + "description": "Refers to user that created keychain and is unlocked." + }, "noConnectionBannerRefreshButtonText": "Refresh", "@noConnectionBannerRefreshButtonText": { "description": "Text shown in the No Internet Connection Banner widget for the refresh button." @@ -656,6 +688,10 @@ "accountCreationSplashNextButton": "Create your Keychain now", "accountInstructionsTitle": "Great! Your Catalyst Keychain \u2028has been created.", "accountInstructionsMessage": "On the next screen, you're going to see 12 words. \u2028This is called your \"seed phrase\". \u2028\u2028It's like a super secure password that only you know, \u2028that allows you to prove ownership of your keychain. \u2028\u2028You'll use it to login and recover your account on \u2028different devices, so be sure to put it somewhere safe!\n\nYou need to write this seed phrase down with pen and paper, so get this ready.", + "previous": "Previous", + "@previous": { + "description": "(Action) switch to the previous item." + }, "next": "Next", "@next": { "description": "For example in button that goes to next stage of registration" @@ -958,6 +994,18 @@ "@reviewRegistrationTransaction": { "description": "A button label to review the registration transaction in wallet detail panel." }, + "format": "Format", + "@format": { + "description": "A label for the format field in the date picker." + }, + "datePickerDateRangeError": "Please select a date within the range of today and one year from today.", + "@datePickerDateRangeError": { + "description": "Error message for the date picker when the selected date is outside the range of today and one year from today." + }, + "datePickerDaysInMonthError": "Entered day exceeds the maximum days for this month.", + "@datePickerDaysInMonthError": { + "description": "Error message for the date picker when the selected day is greater than the maximum days for the selected month." + }, "saveBeforeEditingErrorText": "Please save before editing something else", "mandatoryGuidanceType": "Mandatory", "@mandatoryGuidanceType": { @@ -986,5 +1034,149 @@ "noGuidanceForThisSection": "There is no guidance for this section", "@noGuidanceForThisSection": { "description": "Message when there is no guidance for this section" - } + }, + "noProposalStateDescription": "Discovery space will show draft proposals you can comment on, currently there are no draft proposals.", + "@noProposalStateDescription": { + "description": "Description shown when there are no proposals in the proposals tab" + }, + "noProposalStateTitle": "No draft proposals yet", + "@noProposalStateTitle": { + "description": "Title shown when there are no proposals in the proposals tab" + }, + "campaignIsLive": "Campaign Is Live (Published)", + "@campaignIsLive": { + "description": "Title of the campaign is live (published) space" + }, + "campaignStartingSoon": "Campaign Starting Soon (Ready to deploy)", + "@campaignStartingSoon": { + "description": "Title of the campaign starting soon space" + }, + "campaignConcluded": "Campaign Concluded, Result are in!", + "@campaignConcluded": { + "description": "Title of the campaign concluded space" + }, + "campaignBeginsOn": "Campaign begins on {date} at {time}", + "@campaignBeginsOn": { + "description": "Title of the campaign concluded space", + "placeholders": { + "date": { + "type": "String" + }, + "time": { + "type": "String" + } + } + }, + "campaignEndsOn": "Campaign ends on {date} at {time}", + "@campaignEndsOn": { + "description": "Title of the campaign concluded space", + "placeholders": { + "date": { + "type": "String" + }, + "time": { + "type": "String" + } + } + }, + "viewProposals": "View proposals", + "@viewProposals": { + "description": "Title of the view proposals space" + }, + "viewVotingResults": "View Voting Results", + "@viewVotingResults": { + "description": "Title of the view voting results space" + }, + "campaignDetails": "Campaign Details", + "description": "Description", + "startDate": "Start Date", + "endDate": "End Date", + "categories": "Categories", + "fundingCategories": "Funding categories", + "proposals": "Proposals", + "totalSubmitted": "Total submitted", + "inXDays": "{x, plural, =1{In {x} day} other{In {x} days}}", + "@inXDays": { + "placeholders": { + "x": { + "type": "int" + } + } + }, + "campaignCategories": "Campaign Categories", + "cardanoUseCases": "Cardano Use Cases", + "discoverySpaceTitle": "Boost Social Entrepreneurship", + "@discoverySpaceTitle": { + "description": "Title for the discovery space for actor (unlocked) user." + }, + "discoverySpaceDescription": "Project Catalyst is built on the ingenuity of our global network. Ideas can come from anyone, from any background, anywhere in the world. Proposers pitch their ideas to the community by submitting proposals onto the Catalyst collaboration platform.", + "@discoverySpaceDescription": { + "description": "Description for the discovery space for actor (unlocked) user." + }, + "discoverySpaceEmptyProposals": "Once this campaign launches draft proposals will be shared here.", + "@discoverySpaceEmptyProposals": { + "description": "Description for empty state on discovery space when there are no draft proposals." + }, + "campaign": "Campaign", + "campaignPreviewTitle": "Campaign Preview", + "@campaignPreviewTitle": { + "description": "Title for the campaign preview dialog (admin mode)." + }, + "campaignPreviewEvents": "Events", + "@campaignPreviewEvents": { + "description": "Tab label in campaign preview dialog for campaign events." + }, + "campaignPreviewViews": "Views", + "@campaignPreviewViews": { + "description": "Tab label in campaign preview dialog for campaign views." + }, + "campaignPreviewEventBefore": "Before Campaign", + "@campaignPreviewEventBefore": { + "description": "A name of the state of a campaign before it starts." + }, + "campaignPreviewEventDuring": "During Campaign", + "@campaignPreviewEventDuring": { + "description": "A name of the state of a campaign when it is active." + }, + "campaignPreviewEventAfter": "After Campaign", + "@campaignPreviewEventAfter": { + "description": "A name of the state of a campaign when it is active." + }, + "userAuthenticationState": "User Authentication State", + "@userAuthenticationState": { + "description": "The state of the user (keychain), actor, guest, visitor, etc." + }, + "active": "Active", + "@active": { + "description": "Activated state" + }, + "campaignManagement": "Campaign Management", + "published": "Published", + "status": "Status", + "myProposals": "My Proposals", + "workspaceDescription": "In this space you can manage your existing proposals, view previous and archived proposal as well as creating new proposals.", + "@workspaceDescription": { + "description": "Workspace page description" + }, + "newDraftProposal": "New Draft Proposal", + "draftProposalsX": "Draft Proposals ({count})", + "@draftProposalsX": { + "description": "Workspace page tab", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "finalProposalsX": "Final Proposals ({count})", + "@finalProposalsX": { + "description": "Workspace page tab", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "searchProposals": "Search Proposals", + "search": "Search…" } \ No newline at end of file diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/analysis_options.yaml b/catalyst_voices/packages/internal/catalyst_voices_models/analysis_options.yaml index 376b1c3947e..4e27ef0b03e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/analysis_options.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_models/analysis_options.yaml @@ -1,6 +1,6 @@ include: package:catalyst_analysis/analysis_options.yaml analyzer: - exclude: [build/**, lib/*.g.dart, lib/generated/**] + exclude: [build/**, lib/**.g.dart, lib/generated/**] errors: public_member_api_docs: ignore diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/build.yaml b/catalyst_voices/packages/internal/catalyst_voices_models/build.yaml new file mode 100644 index 00000000000..7cb5117d658 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + builders: + json_serializable: + options: + explicit_to_json: true diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app_config.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app_config.dart new file mode 100644 index 00000000000..fe3b073d6e0 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/app_config.dart @@ -0,0 +1,56 @@ +import 'package:equatable/equatable.dart'; + +final class AppConfig extends Equatable { + final SentryConfig sentry; + final CacheConfig cache; + + const AppConfig({ + this.sentry = const SentryConfig(), + this.cache = const CacheConfig(), + }); + + @override + List get props => [sentry, cache]; +} + +final class SentryConfig extends Equatable { + final String dns; + final String environment; + final String release; + + const SentryConfig({ + // TODO(damian-molinski): default values should be changed. + this.dns = 'https://example.com', + this.environment = 'dev', + this.release = '1.0.0', + }); + + @override + List get props => [ + dns, + environment, + release, + ]; +} + +final class CacheConfig extends Equatable { + final ExpiryDuration expiryDuration; + + const CacheConfig({ + this.expiryDuration = const ExpiryDuration(), + }); + + @override + List get props => [expiryDuration]; +} + +final class ExpiryDuration extends Equatable { + final Duration keychainUnlock; + + const ExpiryDuration({ + this.keychainUnlock = const Duration(hours: 1), + }); + + @override + List get props => [keychainUnlock]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/auth/authentication_status.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/auth/authentication_status.dart deleted file mode 100644 index eb34f04f373..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/auth/authentication_status.dart +++ /dev/null @@ -1,5 +0,0 @@ -enum AuthenticationStatus { - unknown, - authenticated, - unauthenticated; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign.dart new file mode 100644 index 00000000000..55570cc0cae --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign.dart @@ -0,0 +1,67 @@ +import 'package:catalyst_voices_models/src/campaign/campaign_publish.dart'; +import 'package:catalyst_voices_models/src/campaign/campaign_section.dart'; +import 'package:catalyst_voices_models/src/proposal/proposal_template.dart'; +import 'package:equatable/equatable.dart'; + +final class Campaign extends Equatable { + final String id; + final String name; + final String description; + final DateTime startDate; + final DateTime endDate; + final int proposalsCount; + final List sections; + final CampaignPublish publish; + final ProposalTemplate proposalTemplate; + + const Campaign({ + required this.id, + required this.name, + required this.description, + required this.startDate, + required this.endDate, + required this.proposalsCount, + required this.sections, + required this.publish, + required this.proposalTemplate, + }); + + int get categoriesCount => sections.map((e) => e.category).toSet().length; + + Campaign copyWith({ + String? id, + String? name, + String? description, + DateTime? startDate, + DateTime? endDate, + int? proposalsCount, + List? sections, + CampaignPublish? publish, + ProposalTemplate? proposalTemplate, + }) { + return Campaign( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + proposalsCount: proposalsCount ?? this.proposalsCount, + sections: sections ?? this.sections, + publish: publish ?? this.publish, + proposalTemplate: proposalTemplate ?? this.proposalTemplate, + ); + } + + @override + List get props => [ + id, + name, + description, + startDate, + endDate, + proposalsCount, + sections, + publish, + proposalTemplate, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart new file mode 100644 index 00000000000..fd2ca82d607 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +final class CampaignCategory extends Equatable { + final String id; + final String name; + + const CampaignCategory({ + required this.id, + required this.name, + }); + + @override + List get props => [id, name]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_publish.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_publish.dart new file mode 100644 index 00000000000..51c4923aeab --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_publish.dart @@ -0,0 +1,9 @@ +/// Enum representing the state of a campaign. +/// Draft: campaign is not published yet. +/// Published: campaign is published and can be seen by users. +enum CampaignPublish { + draft, + published; + + bool get isDraft => this == draft; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_section.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_section.dart new file mode 100644 index 00000000000..23fba579548 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_section.dart @@ -0,0 +1,24 @@ +import 'package:catalyst_voices_models/src/campaign/campaign_category.dart'; +import 'package:equatable/equatable.dart'; + +final class CampaignSection extends Equatable { + final String id; + final CampaignCategory category; + final String title; + final String body; + + const CampaignSection({ + required this.id, + required this.category, + required this.title, + required this.body, + }); + + @override + List get props => [ + id, + category, + title, + body, + ]; +} 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 1453a38cf44..91852ca68f3 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 @@ -1,19 +1,25 @@ library catalyst_voices_models; -export 'auth/authentication_status.dart'; +export 'app_config.dart'; export 'auth/password_strength.dart'; -export 'crypto/keychain_metadata.dart'; +export 'campaign/campaign.dart'; +export 'campaign/campaign_category.dart'; +export 'campaign/campaign_publish.dart'; +export 'campaign/campaign_section.dart'; export 'crypto/lock_factor.dart'; -export 'document/document_json.dart'; +export 'document_builder/document_builder.dart'; +export 'document_builder/document_definitions.dart'; +export 'document_builder/document_schema.dart'; export 'errors/errors.dart'; export 'file/voices_file.dart'; +export 'markdown_data.dart'; export 'optional.dart'; -export 'proposal/funded_proposal.dart'; -export 'proposal/pending_proposal.dart'; -export 'proposal/proposal_status.dart'; +export 'proposal/guidance.dart'; +export 'proposal/proposal.dart'; +export 'proposal/proposal_section.dart'; +export 'proposal/proposal_template.dart'; export 'registration/registration.dart'; export 'seed_phrase.dart'; -export 'session_data.dart'; export 'space.dart'; export 'user/account.dart'; export 'user/account_role.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/crypto/keychain_metadata.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/crypto/keychain_metadata.dart deleted file mode 100644 index 7ee654a641a..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/crypto/keychain_metadata.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:equatable/equatable.dart'; - -// TODO(damian-molinski): Migrate serialization to json_serializable. -final class KeychainMetadata extends Equatable { - final DateTime createdAt; - final DateTime updatedAt; - - const KeychainMetadata({ - required this.createdAt, - required this.updatedAt, - }); - - factory KeychainMetadata.fromJson(Map json) { - // Typo migration - if (!json.containsKey('createdAt') && json.containsKey('createAt')) { - json['createdAt'] = json['createAt']; - } - return KeychainMetadata( - createdAt: DateTime.parse(json['createdAt'] as String), - updatedAt: DateTime.parse(json['updatedAt'] as String), - ); - } - - KeychainMetadata copyWith({ - DateTime? createdAt, - DateTime? updatedAt, - }) { - return KeychainMetadata( - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ); - } - - Map toJson() { - return { - 'createdAt': createdAt.toIso8601String(), - 'updatedAt': updatedAt.toIso8601String(), - }; - } - - @override - List get props => [ - createdAt, - updatedAt, - ]; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_json.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_json.dart deleted file mode 100644 index d8e1af38e29..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_json.dart +++ /dev/null @@ -1 +0,0 @@ -extension type const DocumentJson(List value) {} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_builder.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_builder.dart new file mode 100644 index 00000000000..54ec501db8f --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_builder.dart @@ -0,0 +1,82 @@ +import 'package:catalyst_voices_models/src/document_builder/document_schema.dart'; +import 'package:equatable/equatable.dart'; + +class DocumentBuilder extends Equatable { + final String schema; + final List segments; + + const DocumentBuilder({ + required this.schema, + required this.segments, + }); + + factory DocumentBuilder.build(DocumentSchema schema) { + return DocumentBuilder( + schema: schema.propertiesSchema, + segments: schema.segments + .map( + (element) => DocumentBuilderSegment( + id: element.id, + sections: element.sections + .map( + (element) => DocumentBuilderSection( + id: element.id, + elements: element.elements + .map( + (e) => DocumentBuilderElement( + id: e.id, + value: e.ref.type.defaultValue, + ), + ) + .toList(), + ), + ) + .toList(), + ), + ) + .toList(), + ); + } + + @override + List get props => [schema, segments]; +} + +class DocumentBuilderSegment extends Equatable { + final String id; + final List sections; + + const DocumentBuilderSegment({ + required this.id, + required this.sections, + }); + + @override + List get props => [id, sections]; +} + +class DocumentBuilderSection extends Equatable { + final String id; + final List elements; + + const DocumentBuilderSection({ + required this.id, + required this.elements, + }); + + @override + List get props => [id, elements]; +} + +class DocumentBuilderElement extends Equatable { + final String id; + final dynamic value; + + const DocumentBuilderElement({ + required this.id, + required this.value, + }); + + @override + List get props => [id, value]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_definitions.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_definitions.dart new file mode 100644 index 00000000000..6ac7baa15ab --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_definitions.dart @@ -0,0 +1,567 @@ +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +enum DocumentDefinitionsObjectType { + string, + object, + integer, + boolean, + array, + unknown; + + const DocumentDefinitionsObjectType(); + + static DocumentDefinitionsObjectType fromString(String value) { + return DocumentDefinitionsObjectType.values.asNameMap()[value] ?? + DocumentDefinitionsObjectType.unknown; + } + + dynamic get defaultValue => switch (this) { + string => '', + integer => 0, + boolean => true, + array => [], + object => {}, + unknown => 'unknown', + }; +} + +enum DocumentDefinitionsContentMediaType { + textPlain('text/plain'), + markdown('text/markdown'), + unknown('unknown'); + + final String schemaValue; + + const DocumentDefinitionsContentMediaType(this.schemaValue); + + static DocumentDefinitionsContentMediaType fromString(String value) { + return DocumentDefinitionsContentMediaType.values + .firstWhereOrNull((e) => e.schemaValue.toLowerCase() == value) ?? + DocumentDefinitionsContentMediaType.unknown; + } +} + +enum DocumentDefinitionsFormat { + path('path'), + uri('uri'), + dropDownSingleSelect('dropDownSingleSelect'), + multiSelect('multiSelect'), + singleLineTextEntryList('singleLineTextEntryList'), + singleLineTextEntryListMarkdown('singleLineTextEntryListMarkdown'), + singleLineHttpsURLEntryList('singleLineHttpsURLEntryList'), + nestedQuestionsList('nestedQuestionsList'), + nestedQuestions('nestedQuestions'), + singleGroupedTagSelector('singleGroupedTagSelector'), + tagGroup('tagGroup'), + tagSelection('tagSelection'), + tokenCardanoADA('token:cardano:ada'), + durationInMonths('datetime:duration:months'), + yesNoChoice('yesNoChoice'), + agreementConfirmation('agreementConfirmation'), + spdxLicenseOrURL('spdxLicenseOrURL'), + unknown('unknown'); + + final String value; + + const DocumentDefinitionsFormat(this.value); + + static DocumentDefinitionsFormat fromString(String value) { + return DocumentDefinitionsFormat.values + .firstWhereOrNull((e) => e.value.toLowerCase() == value) ?? + DocumentDefinitionsFormat.unknown; + } +} + +sealed class BaseDocumentDefinition extends Equatable { + final DocumentDefinitionsObjectType type; + final String note; + + const BaseDocumentDefinition({ + required this.type, + required this.note, + }); + + @visibleForTesting + static final Map refPathToDefinitionType = { + 'segment': SegmentDefinition, + 'section': SectionDefinition, + 'singleLineTextEntry': SingleLineTextEntryDefinition, + 'singleLineHttpsURLEntry': SingleLineHttpsURLEntryDefinition, + 'multiLineTextEntry': MultiLineTextEntryDefinition, + 'multiLineTextEntryMarkdown': MultiLineTextEntryMarkdownDefinition, + 'dropDownSingleSelect': DropDownSingleSelectDefinition, + 'multiSelect': MultiSelectDefinition, + 'singleLineTextEntryList': SingleLineTextEntryListDefinition, + 'multiLineTextEntryListMarkdown': MultiLineTextEntryListMarkdownDefinition, + 'singleLineHttpsURLEntryList': SingleLineHttpsURLEntryListDefinition, + 'nestedQuestionsList': NestedQuestionsListDefinition, + 'nestedQuestions': NestedQuestionsDefinition, + 'singleGroupedTagSelector': SingleGroupedTagSelectorDefinition, + 'tagGroup': TagGroupDefinition, + 'tagSelection': TagSelectionDefinition, + 'tokenValueCardanoADA': TokenValueCardanoADADefinition, + 'durationInMonths': DurationInMonthsDefinition, + 'yesNoChoice': YesNoChoiceDefinition, + 'agreementConfirmation': AgreementConfirmationDefinition, + 'spdxLicenseOrURL': SPDXLicenceOrUrlDefinition, + }; + + static Type typeFromRefPath(String refPath) { + final ref = refPath.split('/').last; + return refPathToDefinitionType[ref] ?? + (throw ArgumentError('Unknown refPath: $refPath')); + } + + static bool isKnownType(String refPath) { + final ref = refPath.split('/').last; + return refPathToDefinitionType[ref] != null; + } +} + +extension BaseDocumentDefinitionListExt on List { + BaseDocumentDefinition getDefinition(String refPath) { + final definitionType = BaseDocumentDefinition.typeFromRefPath(refPath); + final classType = definitionType; + + return firstWhere((e) => e.runtimeType == classType); + } +} + +class SegmentDefinition extends BaseDocumentDefinition { + final bool additionalProperties; + + const SegmentDefinition({ + required super.type, + required super.note, + required this.additionalProperties, + }); + + @override + List get props => [ + type, + note, + additionalProperties, + ]; +} + +class SectionDefinition extends BaseDocumentDefinition { + final bool additionalProperties; + + const SectionDefinition({ + required super.type, + required super.note, + required this.additionalProperties, + }); + + @override + List get props => [ + additionalProperties, + type, + note, + ]; +} + +class SingleLineTextEntryDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsContentMediaType contentMediaType; + final String pattern; + + const SingleLineTextEntryDefinition({ + required super.type, + required super.note, + required this.contentMediaType, + required this.pattern, + }); + + @override + List get props => [ + contentMediaType, + pattern, + type, + note, + ]; +} + +class SingleLineHttpsURLEntryDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final String pattern; + + const SingleLineHttpsURLEntryDefinition({ + required super.type, + required super.note, + required this.format, + required this.pattern, + }); + + @override + List get props => [ + format, + pattern, + type, + note, + ]; +} + +class MultiLineTextEntryDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsContentMediaType contentMediaType; + final String pattern; + + const MultiLineTextEntryDefinition({ + required super.type, + required super.note, + required this.contentMediaType, + required this.pattern, + }); + + @override + List get props => [ + contentMediaType, + pattern, + type, + note, + ]; +} + +class MultiLineTextEntryMarkdownDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsContentMediaType contentMediaType; + final String pattern; + + const MultiLineTextEntryMarkdownDefinition({ + required super.type, + required super.note, + required this.contentMediaType, + required this.pattern, + }); + + @override + List get props => [ + contentMediaType, + pattern, + type, + note, + ]; +} + +class DropDownSingleSelectDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final DocumentDefinitionsContentMediaType contentMediaType; + final String pattern; + + const DropDownSingleSelectDefinition({ + required super.type, + required super.note, + required this.format, + required this.contentMediaType, + required this.pattern, + }); + + @override + List get props => [ + format, + contentMediaType, + pattern, + type, + note, + ]; +} + +class MultiSelectDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool uniqueItems; + + const MultiSelectDefinition({ + required super.type, + required super.note, + required this.format, + required this.uniqueItems, + }); + + @override + List get props => [ + format, + uniqueItems, + type, + note, + ]; +} + +class SingleLineTextEntryListDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool uniqueItems; + final List defaultValues; + final Map items; + + const SingleLineTextEntryListDefinition({ + required super.type, + required super.note, + required this.format, + required this.uniqueItems, + required this.defaultValues, + required this.items, + }); + + @override + List get props => [ + format, + uniqueItems, + type, + note, + defaultValues, + items, + ]; +} + +class MultiLineTextEntryListMarkdownDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool uniqueItems; + final List defaultValue; + final Map items; + + const MultiLineTextEntryListMarkdownDefinition({ + required super.type, + required super.note, + required this.format, + required this.uniqueItems, + required this.defaultValue, + required this.items, + }); + + @override + List get props => [ + format, + uniqueItems, + type, + note, + defaultValue, + items, + ]; +} + +class SingleLineHttpsURLEntryListDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool uniqueItems; + final List defaultValue; + final Map items; + + const SingleLineHttpsURLEntryListDefinition({ + required super.type, + required super.note, + required this.format, + required this.uniqueItems, + required this.defaultValue, + required this.items, + }); + + @override + List get props => [ + format, + uniqueItems, + type, + note, + defaultValue, + items, + ]; +} + +class NestedQuestionsListDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool uniqueItems; + final List defaultValue; + + const NestedQuestionsListDefinition({ + required super.type, + required super.note, + required this.format, + required this.uniqueItems, + required this.defaultValue, + }); + + @override + List get props => [ + format, + uniqueItems, + type, + note, + defaultValue, + ]; +} + +class NestedQuestionsDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool additionalProperties; + + const NestedQuestionsDefinition({ + required super.type, + required super.note, + required this.format, + required this.additionalProperties, + }); + + @override + List get props => [ + format, + additionalProperties, + type, + note, + ]; +} + +class SingleGroupedTagSelectorDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool additionalProperties; + + const SingleGroupedTagSelectorDefinition({ + required super.type, + required super.note, + required this.format, + required this.additionalProperties, + }); + + @override + List get props => [ + format, + additionalProperties, + type, + note, + ]; +} + +class TagGroupDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final String pattern; + + const TagGroupDefinition({ + required super.type, + required super.note, + required this.format, + required this.pattern, + }); + + @override + List get props => [ + format, + pattern, + type, + note, + ]; +} + +class TagSelectionDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final String pattern; + + const TagSelectionDefinition({ + required super.type, + required super.note, + required this.format, + required this.pattern, + }); + + @override + List get props => [ + format, + pattern, + type, + note, + ]; +} + +class TokenValueCardanoADADefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + + const TokenValueCardanoADADefinition({ + required super.type, + required super.note, + required this.format, + }); + + @override + List get props => [ + format, + type, + note, + ]; +} + +class DurationInMonthsDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + + const DurationInMonthsDefinition({ + required super.type, + required super.note, + required this.format, + }); + + @override + List get props => [ + type, + note, + format, + ]; +} + +class YesNoChoiceDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool defaultValue; + + const YesNoChoiceDefinition({ + required super.type, + required super.note, + required this.format, + required this.defaultValue, + }); + + @override + List get props => [ + format, + defaultValue, + type, + note, + ]; +} + +class AgreementConfirmationDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final bool defaultValue; + final bool constValue; + + const AgreementConfirmationDefinition({ + required super.type, + required super.note, + required this.format, + required this.defaultValue, + required this.constValue, + }); + + @override + List get props => [ + format, + defaultValue, + constValue, + type, + note, + ]; +} + +class SPDXLicenceOrUrlDefinition extends BaseDocumentDefinition { + final DocumentDefinitionsFormat format; + final String pattern; + final DocumentDefinitionsContentMediaType contentMediaType; + + const SPDXLicenceOrUrlDefinition({ + required super.type, + required super.note, + required this.format, + required this.pattern, + required this.contentMediaType, + }); + + @override + List get props => [ + format, + pattern, + type, + note, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_schema.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_schema.dart new file mode 100644 index 00000000000..53e64c7d217 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document_builder/document_schema.dart @@ -0,0 +1,121 @@ +import 'package:catalyst_voices_models/src/document_builder/document_definitions.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:equatable/equatable.dart'; + +class DocumentSchema extends Equatable { + final String schema; + final String title; + final String description; + final List segments; + final List order; + final String propertiesSchema; + + const DocumentSchema({ + required this.schema, + required this.title, + required this.description, + required this.segments, + required this.order, + required this.propertiesSchema, + }); + + @override + List get props => [ + schema, + title, + description, + segments, + order, + propertiesSchema, + ]; +} + +class DocumentSchemaSegment extends Equatable { + final BaseDocumentDefinition ref; + final String id; + final String title; + final String description; + final List sections; + + const DocumentSchemaSegment({ + required this.ref, + required this.id, + required this.title, + required this.description, + required this.sections, + }); + + @override + List get props => [ + id, + title, + description, + sections, + ]; +} + +class DocumentSchemaSection extends Equatable { + final BaseDocumentDefinition ref; + final String id; + final String title; + final String description; + final List elements; + final bool isRequired; + + const DocumentSchemaSection({ + required this.ref, + required this.id, + required this.title, + required this.description, + required this.elements, + required this.isRequired, + }); + + @override + List get props => [ + ref, + id, + title, + description, + elements, + isRequired, + ]; +} + +class DocumentSchemaElement extends Equatable { + final BaseDocumentDefinition ref; + final String id; + final String title; + final String description; + + final String? defaultValue; + final String guidance; + final List enumValues; + final Range? range; + final Range? itemsRange; + + const DocumentSchemaElement({ + required this.ref, + required this.id, + required this.title, + required this.description, + required this.defaultValue, + required this.guidance, + this.enumValues = const [], + required this.range, + required this.itemsRange, + }); + + @override + List get props => [ + ref, + id, + title, + description, + defaultValue, + guidance, + enumValues, + range, + itemsRange, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/markdown_data.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/markdown_data.dart new file mode 100644 index 00000000000..6c5bebcfef8 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/markdown_data.dart @@ -0,0 +1 @@ +extension type const MarkdownData(String data) implements Object {} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/funded_proposal.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/funded_proposal.dart deleted file mode 100644 index 1fad937efcc..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/funded_proposal.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; -import 'package:equatable/equatable.dart'; - -/// Defines the already funded proposal. -final class FundedProposal extends Equatable { - final String id; - final String fund; - final String category; - final String title; - final DateTime fundedDate; - final Coin fundsRequested; - final int commentsCount; - final String description; - - const FundedProposal({ - required this.id, - required this.fund, - required this.category, - required this.title, - required this.fundedDate, - required this.fundsRequested, - required this.commentsCount, - required this.description, - }); - - @override - List get props => [ - id, - fund, - category, - title, - fundedDate, - fundsRequested, - commentsCount, - description, - ]; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart similarity index 71% rename from catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance.dart rename to catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart index 7cf394c461f..f51cc667d37 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart @@ -1,22 +1,36 @@ -import 'package:catalyst_voices_view_models/src/proposal/guidance/guidance_type.dart'; +import 'dart:core'; + import 'package:equatable/equatable.dart'; +enum GuidanceType { + mandatory(priority: 0), + education(priority: 1), + tips(priority: 2); + + final int priority; + + const GuidanceType({ + required this.priority, + }); +} + final class Guidance extends Equatable implements Comparable { + final String id; final String title; final String description; final GuidanceType type; - final int? weight; // This represents how important the guidance is in - //specific [GuidanceType]. + + /// This represents how important the guidance is in specific [GuidanceType]. + final int? weight; const Guidance({ + required this.id, required this.title, required this.description, required this.type, this.weight, }); - String get weightText => weight?.toString() ?? ''; - @override int compareTo(Guidance other) { final typeComparison = type.priority.compareTo(other.type.priority); @@ -31,9 +45,11 @@ final class Guidance extends Equatable implements Comparable { @override List get props => [ + id, title, description, type, + weight, ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/pending_proposal.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/pending_proposal.dart deleted file mode 100644 index 3d16e11938c..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/pending_proposal.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; -import 'package:equatable/equatable.dart'; - -/// Defines the pending proposal that is not funded yet. -final class PendingProposal extends Equatable { - final String id; - final String fund; - final String category; - final String title; - final DateTime lastUpdateDate; - final Coin fundsRequested; - final int commentsCount; - final String description; - final int completedSegments; - final int totalSegments; - - const PendingProposal({ - required this.id, - required this.fund, - required this.category, - required this.title, - required this.lastUpdateDate, - required this.fundsRequested, - required this.commentsCount, - required this.description, - required this.completedSegments, - required this.totalSegments, - }); - - @override - List get props => [ - id, - fund, - category, - title, - lastUpdateDate, - fundsRequested, - commentsCount, - description, - completedSegments, - totalSegments, - ]; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart new file mode 100644 index 00000000000..68995d67524 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal.dart @@ -0,0 +1,65 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_models/src/proposal/proposal_section.dart'; +import 'package:equatable/equatable.dart'; + +// Note. This enum may be deleted later. Its here for backwards compatibility. +enum ProposalStatus { ready, draft, inProgress, private, open, live, completed } + +enum ProposalPublish { draft, published } + +enum ProposalAccess { private, public } + +final class Proposal extends Equatable { + final String id; + final String title; + final String description; + final DateTime updateDate; + final DateTime? fundedDate; + final Coin fundsRequested; + final ProposalStatus status; + final ProposalPublish publish; + final ProposalAccess access; + final List sections; + + // This may be a reference to class + final String category; + + // Those may be getters. + final int commentsCount; + + const Proposal({ + required this.id, + required this.title, + required this.description, + required this.updateDate, + this.fundedDate, + required this.fundsRequested, + required this.status, + required this.publish, + required this.access, + required this.sections, + required this.category, + required this.commentsCount, + }); + + int get totalSegments => sections.length; + + int get completedSegments { + return sections.where((element) => element.isCompleted).length; + } + + @override + List get props => [ + id, + title, + description, + updateDate, + fundedDate, + fundsRequested.value, + publish, + access, + sections, + category, + commentsCount, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_section.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_section.dart new file mode 100644 index 00000000000..069fc44ebb9 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_section.dart @@ -0,0 +1,78 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; + +final class ProposalSection extends Equatable { + final String id; + final String name; + final List steps; + + const ProposalSection({ + required this.id, + required this.name, + required this.steps, + }); + + bool get isCompleted => steps.every((element) => element.hasAnswer); + + ProposalSection copyWith({ + String? id, + String? name, + List? steps, + }) { + return ProposalSection( + id: id ?? this.id, + name: name ?? this.name, + steps: steps ?? this.steps, + ); + } + + @override + List get props => [ + id, + name, + steps, + ]; +} + +final class ProposalSectionStep extends Equatable { + final String id; + final String name; + final String? description; + final List guidances; + final MarkdownData? answer; + + const ProposalSectionStep({ + required this.id, + required this.name, + this.description, + this.guidances = const [], + this.answer, + }); + + bool get hasAnswer => answer != null; + + ProposalSectionStep copyWith({ + String? id, + String? name, + Optional? description, + List? guidances, + Optional? answer, + }) { + return ProposalSectionStep( + id: id ?? this.id, + name: name ?? this.name, + description: description.dataOr(this.description), + guidances: guidances ?? this.guidances, + answer: answer.dataOr(this.answer), + ); + } + + @override + List get props => [ + id, + name, + description, + guidances, + answer, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_status.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_status.dart deleted file mode 100644 index e57594441d1..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_status.dart +++ /dev/null @@ -1,9 +0,0 @@ -enum ProposalStatus { - ready, - draft, - inProgress, - private, - open, - live, - completed; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart new file mode 100644 index 00000000000..afd4aa586be --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart @@ -0,0 +1,15 @@ +import 'package:catalyst_voices_models/src/proposal/proposal_section.dart'; +import 'package:equatable/equatable.dart'; + +final class ProposalTemplate extends Equatable { + final List sections; + + const ProposalTemplate({ + required this.sections, + }); + + @override + List get props => [ + sections, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/session_data.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/session_data.dart deleted file mode 100644 index c2ef7fb8e47..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/session_data.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; -import 'package:meta/meta.dart'; - -@immutable -final class SessionData extends Equatable { - final String email; - final String password; - - const SessionData({ - required this.email, - required this.password, - }); - - factory SessionData.fromJson(String source) => SessionData.fromMap( - json.decode( - source, - ) as Map, - ); - - factory SessionData.fromMap(Map map) { - return SessionData( - email: map['email'] as String? ?? '', - password: map['password'] as String? ?? '', - ); - } - - @override - List get props => [email, password]; - - @override - bool get stringify => true; - - String toJson() => json.encode(toMap()); - - Map toMap() { - return { - 'email': email, - 'password': password, - }; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/account.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/account.dart index 243732ac985..7e623e21517 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/account.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/account.dart @@ -1,26 +1,94 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:equatable/equatable.dart'; /// Defines singular account used by [User] (physical person). /// One [User] may have multiple [Account]'s. final class Account extends Equatable { - final String keychainId; + final Keychain keychain; final Set roles; final WalletInfo walletInfo; + /// Whether this account is being used. + final bool isActive; + + /// When account registration transaction is posted on chain account is + /// "provisional". This means backend does not yet know about it because + /// transaction was not yet read. + final bool isProvisional; + + static const dummyKeychainId = 'TestUserKeychainID'; + static const dummyUnlockFactor = PasswordLockFactor('Test1234'); + static final dummySeedPhrase = SeedPhrase.fromMnemonic( + 'few loyal swift champion rug peace dinosaur ' + 'erase bacon tone install universe', + ); + const Account({ - required this.keychainId, + required this.keychain, required this.roles, required this.walletInfo, + this.isActive = false, + this.isProvisional = true, }); + factory Account.dummy({ + required Keychain keychain, + bool isActive = false, + }) { + return Account( + keychain: keychain, + roles: const { + AccountRole.voter, + AccountRole.proposer, + }, + walletInfo: WalletInfo( + metadata: const WalletMetadata(name: 'Dummy Wallet', icon: null), + balance: Coin.fromAda(10), + /* cSpell:disable */ + address: ShelleyAddress.fromBech32( + 'addr_test1vzpwq95z3xyum8vqndgdd' + '9mdnmafh3djcxnc6jemlgdmswcve6tkw', + ), + /* cSpell:enable */ + ), + isActive: isActive, + isProvisional: true, + ); + } + + String get id => keychain.id; + // Note. this is not defined yet what we will show here. String get acronym => 'A'; + bool get isAdmin => true; + + bool get isDummy => keychain.id == dummyKeychainId; + + Account copyWith({ + Keychain? keychain, + Set? roles, + WalletInfo? walletInfo, + bool? isActive, + bool? isProvisional, + }) { + return Account( + keychain: keychain ?? this.keychain, + roles: roles ?? this.roles, + walletInfo: walletInfo ?? this.walletInfo, + isActive: isActive ?? this.isActive, + isProvisional: isProvisional ?? this.isProvisional, + ); + } + @override List get props => [ - keychainId, + keychain.id, roles, walletInfo, + isActive, + isProvisional, ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/user.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/user.dart index a7e946b72ff..1a815a469e2 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/user.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/user/user.dart @@ -1,4 +1,5 @@ -import 'package:catalyst_voices_models/src/user/account.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; /// Defines user or the app. @@ -9,12 +10,46 @@ final class User extends Equatable { required this.accounts, }); - /// Just syntax sugar for [activeAccount]. - Account get account => activeAccount; + Account? get activeAccount { + return accounts.singleWhereOrNull((account) => account.isActive); + } - // Note. At the moment we support only single profile Users but later - // this may change and this implementation with it. - Account get activeAccount => accounts.single; + User useAccount({required String id}) { + if (this.accounts.none((e) => e.id == id)) { + throw ArgumentError('Account[$id] is not on the list'); + } + + final accounts = [...this.accounts] + .map((e) => e.copyWith(isActive: e.id == id)) + .toList(); + + return copyWith(accounts: accounts); + } + + bool hasAccount({required String id}) { + return accounts.any((element) => element.id == id); + } + + User addAccount(Account account) { + final accounts = [...this.accounts, account]; + + return copyWith(accounts: accounts); + } + + User removeAccount({required String id}) { + final accounts = + [...this.accounts].where((element) => element.id != id).toList(); + + return copyWith(accounts: accounts); + } + + User copyWith({ + List? accounts, + }) { + return User( + accounts: accounts ?? this.accounts, + ); + } @override List get props => [ diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_models/pubspec.yaml index e83c92fefdd..6cd4eb2eac8 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_models/pubspec.yaml @@ -11,12 +11,17 @@ dependencies: catalyst_cardano: ^0.3.0 catalyst_cardano_serialization: ^0.4.0 catalyst_cardano_web: ^0.3.0 + catalyst_voices_shared: + path: ../catalyst_voices_shared collection: ^1.18.0 convert: ^3.1.1 equatable: ^2.0.7 + json_annotation: ^4.9.0 meta: ^1.10.0 password_strength: ^0.2.0 dev_dependencies: + build_runner: ^2.4.12 catalyst_analysis: ^2.0.0 - test: ^1.24.9 \ No newline at end of file + json_serializable: ^6.9.0 + test: ^1.24.9 diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/test/crypto/keychain_metadata_test.dart b/catalyst_voices/packages/internal/catalyst_voices_models/test/crypto/keychain_metadata_test.dart deleted file mode 100644 index 17d5d94b777..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/test/crypto/keychain_metadata_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:test/test.dart'; - -void main() { - group('Serialization', () { - test('json createAt to createdAt', () { - // Given - final createdAt = DateTime.timestamp(); - - // When - final json = { - 'createAt': createdAt.toIso8601String(), - 'updatedAt': DateTime.timestamp().toIso8601String(), - }; - - // Then - final metadata = KeychainMetadata.fromJson(json); - - expect(metadata.createdAt, createdAt); - }); - }); - - group('Equality', () { - test('same source dates equals', () { - // Given - final now = DateTime.now(); - - // When - final metadataOne = KeychainMetadata(createdAt: now, updatedAt: now); - final metadataTwo = KeychainMetadata(createdAt: now, updatedAt: now); - - // Then - expect(metadataOne, metadataTwo); - }); - - test('different source dates equals', () { - // Given - final now = DateTime.now(); - final isPast = now.subtract(const Duration(days: 1)); - - // When - final metadataOne = KeychainMetadata(createdAt: now, updatedAt: now); - final metadataTwo = KeychainMetadata( - createdAt: isPast, - updatedAt: isPast, - ); - - // Then - expect(metadataOne, isNot(metadataTwo)); - }); - }); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/README.md b/catalyst_voices/packages/internal/catalyst_voices_repositories/README.md index 5316d7d4865..90add939367 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/README.md +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/README.md @@ -1 +1,32 @@ # Catalyst Voices Repositories + +## Automated Code Generation + +This package is used for the code generation from the OpenAPI specifications. +It leverages `swagger_dart_code_generator` library and the artifacts generated +for the documentation of the `catalyst-gateway` backend. +The process consists in 3 simple steps: + +1. The OpenAPI specification is picked from the artifact generated in the + `Earthfile` of `catalyst-gateway`. +2. The code is generated and saved as an artifact in the `Earthfile` of + `catalyst_voices` +3. Generated code is placed in the proper location within the `catalyst_voices` + project (`packages/internal/catalyst_voices_repository/lib/generated/api`) + and it's ready for local usage. + +This process can be achieved by executing from the `catalyst_voices` root +folder: + +```sh +earthly +code-generator --platform=linux/amd64 --save_locally=true +``` + +The `--platform=linux/amd64` flag is necessary only when running the command from +a different platform such as **Windows** or **macOS**. +It ensures that the code generation process is compatible with the target platform. +If you are running the command on a **Linux** platform, you can omit this flag. + +In this way it's possible to locally generate the code using the same version of +OpenAPI specs defined in the backend code and developers have full control of +what should be committed. diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/analysis_options.yaml b/catalyst_voices/packages/internal/catalyst_voices_repositories/analysis_options.yaml index 376b1c3947e..4e27ef0b03e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/analysis_options.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/analysis_options.yaml @@ -1,6 +1,6 @@ include: package:catalyst_analysis/analysis_options.yaml analyzer: - exclude: [build/**, lib/*.g.dart, lib/generated/**] + exclude: [build/**, lib/**.g.dart, lib/generated/**] errors: public_member_api_docs: ignore diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/build.yaml b/catalyst_voices/packages/internal/catalyst_voices_repositories/build.yaml new file mode 100644 index 00000000000..ceb38a39b0a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/build.yaml @@ -0,0 +1,26 @@ +targets: + $default: + sources: + - lib/** + - openapi/** + - $package$ + builders: + chopper_generator: + options: + header: "// Generated code" + swagger_dart_code_generator: + options: + input_folder: "openapi/" + output_folder: "lib/generated/api" + separate_models: true + overriden_models: + - file_name: "vitss-openapi" + import_url: "../../src/api_models/overriden_models.dart" + overriden_models: + - SimpleProposal$ProposalCategory + - SimpleProposal$Proposer + - CommunityChoiceProposal$ProposalCategory + - CommunityChoiceProposal$Proposer + json_serializable: + options: + explicit_to_json: true diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/generated/api/client_index.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/generated/api/client_index.dart new file mode 100644 index 00000000000..098d50371d2 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/generated/api/client_index.dart @@ -0,0 +1,2 @@ +export 'cat_gateway.swagger.dart' show CatGateway; +export 'vit.swagger.dart' show Vit; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway/client_mapping.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/generated/api/client_mapping.dart similarity index 100% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway/client_mapping.dart rename to catalyst_voices/packages/internal/catalyst_voices_repositories/lib/generated/api/client_mapping.dart diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/api_models/overriden_models.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/api_models/overriden_models.dart new file mode 100644 index 00000000000..b6085b8d862 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/api_models/overriden_models.dart @@ -0,0 +1,100 @@ +import 'package:catalyst_voices_repositories/generated/api/vit.models.swagger.dart'; + +/// For some reason VitSS openapi spec does not play nice with generating +/// sub classes for Proposal while extending more then 2 level. +/// +/// As temporary solution we're overriding not generated classes because +/// we're remove VitSS integration later. + +// SimpleProposal + +class SimpleProposal$ProposalCategory extends Proposal$ProposalCategory { + SimpleProposal$ProposalCategory({ + super.categoryId, + super.categoryName, + super.categoryDescription, + }); + + factory SimpleProposal$ProposalCategory.fromJson(Map json) { + return Proposal$ProposalCategory.fromJson(json)._toSimple(); + } +} + +class SimpleProposal$Proposer extends Proposal$Proposer { + SimpleProposal$Proposer({ + super.proposerName, + super.proposerEmail, + super.proposerUrl, + }); + + factory SimpleProposal$Proposer.fromJson(Map json) { + return Proposal$Proposer.fromJson(json)._toSimple(); + } +} + +// CommunityChoiceProposal + +class CommunityChoiceProposal$ProposalCategory + extends Proposal$ProposalCategory { + CommunityChoiceProposal$ProposalCategory({ + super.categoryId, + super.categoryName, + super.categoryDescription, + }); + + factory CommunityChoiceProposal$ProposalCategory.fromJson( + Map json, + ) { + return Proposal$ProposalCategory.fromJson(json)._toCommunityChoice(); + } +} + +class CommunityChoiceProposal$Proposer extends Proposal$Proposer { + CommunityChoiceProposal$Proposer({ + super.proposerName, + super.proposerEmail, + super.proposerUrl, + }); + + factory CommunityChoiceProposal$Proposer.fromJson(Map json) { + return Proposal$Proposer.fromJson(json)._toCommunityChoice(); + } +} + +// Private extension + +extension _Proposal$ProposalCategoryExt on Proposal$ProposalCategory { + SimpleProposal$ProposalCategory _toSimple() { + return SimpleProposal$ProposalCategory( + categoryId: categoryId, + categoryName: categoryName, + categoryDescription: categoryDescription, + ); + } + + CommunityChoiceProposal$ProposalCategory _toCommunityChoice() { + return CommunityChoiceProposal$ProposalCategory( + categoryId: categoryId, + categoryName: categoryName, + categoryDescription: categoryDescription, + ); + } +} + +extension _Proposal$ProposerExt on Proposal$Proposer { + SimpleProposal$Proposer _toSimple() { + return SimpleProposal$Proposer( + proposerName: proposerName, + proposerEmail: proposerEmail, + proposerUrl: proposerUrl, + ); + } + + CommunityChoiceProposal$Proposer _toCommunityChoice() { + return CommunityChoiceProposal$Proposer( + proposerName: proposerName, + proposerEmail: proposerEmail, + proposerUrl: proposerUrl, + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/authentication_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/authentication_repository.dart deleted file mode 100644 index b3b5afda62e..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/authentication_repository.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:async'; - -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; - -final class AuthenticationRepository { - final CredentialsStorageRepository credentialsStorageRepository; - final _streamController = StreamController(); - - AuthenticationRepository({required this.credentialsStorageRepository}); - - Stream get status async* { - try { - final sessionData = await credentialsStorageRepository.getSessionData(); - - if (sessionData.isSuccess) { - yield AuthenticationStatus.authenticated; - } else { - yield AuthenticationStatus.unauthenticated; - } - } catch (error) { - yield AuthenticationStatus.unknown; - } - - yield* _streamController.stream; - } - - Future dispose() async => _streamController.close(); - - Future getSessionData() async { - try { - final sessionData = await credentialsStorageRepository.getSessionData(); - - if (sessionData.isSuccess) { - return sessionData.success; - } else { - return null; - } - } catch (error) { - return null; - } - } - - void logOut() { - credentialsStorageRepository.clearSessionData; - _streamController.add(AuthenticationStatus.unauthenticated); - } - - Future signIn({ - required String email, - required String password, - }) async { - await credentialsStorageRepository.storeSessionData( - SessionData( - email: email, - password: password, - ), - ); - - // TODO(minikin): remove this delay after implementing real auth flow. - await Future.delayed( - const Duration(milliseconds: 300), - () => _streamController.add(AuthenticationStatus.authenticated), - ); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart new file mode 100644 index 00000000000..67785d512cc --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart @@ -0,0 +1,153 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; + +class CampaignRepository { + Future getCampaign({ + required String id, + }) async { + final now = DateTime.now(); + + const sections = [ + CampaignSection( + id: '1', + category: CampaignCategory(id: '1', name: 'Concept'), + title: 'Introduction', + body: _tmpBody, + ), + CampaignSection( + id: '2', + category: CampaignCategory(id: '2', name: 'Product'), + title: 'Motivation', + body: 'Different body here\n$_tmpBody', + ), + ]; + + return Campaign( + id: id, + name: 'Boost Social Entrepreneurship', + description: 'We are currently only decentralizing our technology, ' + 'failing to rethink how interactions play out in novel ' + 'web3/p2p Action networks.', + startDate: now.add(const Duration(days: 10)), + endDate: now.add(const Duration(days: 92)), + proposalsCount: 0, + sections: sections, + publish: CampaignPublish.draft, + proposalTemplate: ProposalTemplate( + sections: [ + ProposalSection( + id: '${id}_1', + name: 'Proposal Setup', + steps: [ + ProposalSectionStep( + id: '${id}_1_1', + name: 'Title', + guidances: List.from(_mockGuidance), + ), + ], + ), + ProposalSection( + id: '${id}_2', + name: 'Proposal Summary', + steps: [ + ProposalSectionStep( + id: '${id}_2_1', + name: 'Problem Statement', + guidances: List.from(_mockGuidance), + ), + ProposalSectionStep( + id: '${id}_2_2', + name: 'Solution Statement', + guidances: List.from(_mockGuidance), + ), + ], + ), + ProposalSection( + id: '${id}_3', + name: 'Proposal Setup', + steps: [ + ProposalSectionStep( + id: '${id}_3_1', + name: 'Topic 1', + guidances: List.from(_mockGuidance), + ), + ProposalSectionStep( + id: '${id}_3_2', + name: 'Topic 2', + guidances: List.from(_mockGuidance), + ), + ProposalSectionStep( + id: '${id}_3_3', + name: 'Topic 3', + guidances: List.from(_mockGuidance), + ), + ProposalSectionStep( + id: '${id}_3_4', + name: 'Topic 4', + guidances: List.from(_mockGuidance), + ), + ], + ), + ], + ), + ); + } +} + +const _tmpBody = ''' +Open source software, hardware and data solutions encourage +greater transparency and security, and help reduce costs by +developing, collaborating, and fixing in the open. +More information on open source can be found here. + +Cardano Open: Developers category supports developers and +engineers to contribute to or develop open source technology +centered around enabling and improving the Cardano developer +experience. + +The goal of this category is to create developer-friendly +tooling and approaches that streamline an integrated +development environment, help to create code more +efficiently, and provide an ease of use for developers to +build on Cardano. + +Details of the selected open source license must be +submitted by the applicants as part of their proposal. + +As part of their deliverables, projects will also be +required to submit open source, high quality documentation +for their technology that can be used as a +learning resource by the rest of the community.'''; + +const List _mockGuidance = [ + Guidance( + id: 'g_1', + title: 'Use a Compelling Hook or Unique Angle', + description: + '''Adding an element of intrigue or a unique approach can make your title stand out. For example, “Revolutionizing Urban Mobility with Eco-Friendly Innovation” not only describes the proposal but also piques curiosity.''', + type: GuidanceType.tips, + weight: 1, + ), + Guidance( + id: 'g_1', + title: 'Be Specific and Solution-Oriented', + description: + '''Use keywords that pinpoint the problem you’re solving or the opportunity you’re capitalizing on. A title like “Streamlining Supply Chains for Cost-Effective and Rapid Delivery” instantly tells the reader what the proposal aims to achieve.''', + type: GuidanceType.mandatory, + weight: 2, + ), + Guidance( + id: 'g_1', + title: 'Highlight the Benefit or Outcome', + description: + '''Make sure the reader can immediately see the value or the end result of your proposal. A title like “Boosting Engagement and Growth through Targeted Digital Strategies” puts the focus on the positive outcomes.''', + type: GuidanceType.mandatory, + weight: 1, + ), + Guidance( + id: 'g_1', + title: 'Education', + description: 'Use keywords that pinpoint the problem yo', + type: GuidanceType.education, + weight: 1, + ), +]; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart index e5729b5689a..c3a83f1d2c1 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/catalyst_voices_repositories.dart @@ -1,3 +1,6 @@ -export 'authentication_repository.dart'; -export 'credentials_storage_repository.dart'; +export 'campaign/campaign_repository.dart'; +export 'config/config_repository.dart' show ConfigRepository; +export 'proposal/proposal_repository.dart'; export 'transaction/transaction_config_repository.dart'; +export 'user/user_repository.dart' show UserRepository; +export 'user/user_storage.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/config/config_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/config/config_repository.dart new file mode 100644 index 00000000000..cc4e6c7a32b --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/config/config_repository.dart @@ -0,0 +1,23 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; + +// ignore: one_member_abstracts +abstract interface class ConfigRepository { + factory ConfigRepository() { + return ConfigRepositoryImpl(); + } + + Future getAppConfig(); +} + +final class ConfigRepositoryImpl implements ConfigRepository { + ConfigRepositoryImpl(); + + @override + Future getAppConfig() { + // TODO(damian-molinski): should be api call here. + return Future.delayed( + const Duration(milliseconds: 200), + AppConfig.new, + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/credentials_storage_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/credentials_storage_repository.dart deleted file mode 100644 index 25beccc74bd..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/credentials_storage_repository.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'dart:async'; - -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; -import 'package:result_type/result_type.dart'; - -/// This is a temporary implementation of CredentialsStorageRepository -/// It's only used to set-up state management for now. -/// It will be replaced by a proper implementation as soon as authentication -/// flow will be defined. -final class CredentialsStorageRepository { - final DummyAuthStorage _storage; - - const CredentialsStorageRepository({ - required DummyAuthStorage storage, - }) : _storage = storage; - - Future get clearSessionData async => _storage.clear(); - - Future> getSessionData() async { - try { - final email = await _storage.readEmail(); - final password = await _storage.readPassword(); - - if (email == null || password == null) { - return Success(null); - } - - return Success( - SessionData( - email: email, - password: password, - ), - ); - } on SecureStorageError catch (_) { - return Failure(SecureStorageError.canNotReadData); - } - } - - Future> storeSessionData( - SessionData sessionData, - ) async { - try { - await _storage.writeEmail(sessionData.email); - await _storage.writePassword(sessionData.password); - return Success(null); - } on SecureStorageError catch (_) { - return Failure(SecureStorageError.canNotSaveData); - } - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_config_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_config_dto.dart new file mode 100644 index 00000000000..66ae3bdfb7d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/app_config_dto.dart @@ -0,0 +1,121 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/utils/json_converters.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'app_config_dto.g.dart'; + +@JsonSerializable() +final class AppConfigDto { + @JsonKey(includeToJson: false, includeFromJson: false) + final SentryConfigDto? sentry; + final CacheConfigDto cache; + + const AppConfigDto({ + this.sentry, + this.cache = const CacheConfigDto(), + }); + + AppConfigDto.fromModel(AppConfig data) + : this( + sentry: SentryConfigDto.fromModel(data.sentry), + cache: CacheConfigDto.fromModel(data.cache), + ); + + factory AppConfigDto.fromJson(Map json) { + return _$AppConfigDtoFromJson(json); + } + + Map toJson() => _$AppConfigDtoToJson(this); + + AppConfig toModel() { + return AppConfig( + sentry: sentry?.toModel() ?? const SentryConfig(), + cache: cache.toModel(), + ); + } +} + +@JsonSerializable() +final class SentryConfigDto { + final String dns; + final String environment; + final String release; + + const SentryConfigDto({ + required this.dns, + required this.environment, + required this.release, + }); + + SentryConfigDto.fromModel(SentryConfig data) + : this( + dns: data.dns, + environment: data.environment, + release: data.release, + ); + + factory SentryConfigDto.fromJson(Map json) { + return _$SentryConfigDtoFromJson(json); + } + + Map toJson() => _$SentryConfigDtoToJson(this); + + SentryConfig toModel() { + return SentryConfig( + dns: dns, + environment: environment, + release: release, + ); + } +} + +@JsonSerializable() +final class CacheConfigDto { + final ExpiryDurationDto expiryDuration; + + const CacheConfigDto({ + this.expiryDuration = const ExpiryDurationDto(), + }); + + CacheConfigDto.fromModel(CacheConfig data) + : this( + expiryDuration: ExpiryDurationDto.fromModel(data.expiryDuration), + ); + + factory CacheConfigDto.fromJson(Map json) { + return _$CacheConfigDtoFromJson(json); + } + + Map toJson() => _$CacheConfigDtoToJson(this); + + CacheConfig toModel() { + return CacheConfig( + expiryDuration: expiryDuration.toModel(), + ); + } +} + +@JsonSerializable() +final class ExpiryDurationDto { + @DurationConverter() + final Duration keychainUnlock; + + const ExpiryDurationDto({ + this.keychainUnlock = const Duration(hours: 1), + }); + + ExpiryDurationDto.fromModel(ExpiryDuration data) + : this(keychainUnlock: data.keychainUnlock); + + factory ExpiryDurationDto.fromJson(Map json) { + return _$ExpiryDurationDtoFromJson(json); + } + + Map toJson() => _$ExpiryDurationDtoToJson(this); + + ExpiryDuration toModel() { + return ExpiryDuration( + keychainUnlock: keychainUnlock, + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_builder_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_builder_dto.dart new file mode 100644 index 00000000000..0c3fe59f955 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_builder_dto.dart @@ -0,0 +1,227 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'document_builder_dto.g.dart'; + +@JsonSerializable() +class DocumentBuilderDto extends Equatable { + @JsonKey(name: r'$schema') + final String schema; + @JsonKey(fromJson: _fromJsonSegments, toJson: _toJsonSegments) + final List segments; + + const DocumentBuilderDto({ + required this.schema, + required this.segments, + }); + + factory DocumentBuilderDto.fromJson(Map json) { + json['segments'] = Map.from(json)..remove(r'$schema'); + return _$DocumentBuilderDtoFromJson(json); + } + + factory DocumentBuilderDto.fromModel(DocumentBuilder model) { + return DocumentBuilderDto( + schema: model.schema, + segments: + model.segments.map(DocumentBuilderSegmentDto.fromModel).toList(), + ); + } + + Map toJson() { + final segments = {}..addAll({r'$schema': schema}); + for (final segment in this.segments) { + segments.addAll(segment.toJson()); + } + return segments; + } + + DocumentBuilder toModel() { + return DocumentBuilder( + schema: schema, + segments: segments.map((e) => e.toModel()).toList(), + ); + } + + static Map _toJsonSegments( + List segments, + ) { + final map = {}; + for (final segment in segments) { + map[segment.id] = segment.toJson(); + } + return map; + } + + static List _fromJsonSegments( + Map json, + ) { + final listOfSegments = json.convertMapToListWithIds(); + return listOfSegments.map(DocumentBuilderSegmentDto.fromJson).toList(); + } + + @override + List get props => [schema, segments]; +} + +@JsonSerializable() +class DocumentBuilderSegmentDto extends Equatable { + final String id; + @JsonKey(fromJson: _fromJsonSections, toJson: _toJsonSections) + final List sections; + + const DocumentBuilderSegmentDto({ + required this.id, + required this.sections, + }); + + factory DocumentBuilderSegmentDto.fromJson(Map json) { + json['sections'] = Map.from(json)..remove('id'); + return _$DocumentBuilderSegmentDtoFromJson(json); + } + + factory DocumentBuilderSegmentDto.fromModel(DocumentBuilderSegment model) { + return DocumentBuilderSegmentDto( + id: model.id, + sections: + model.sections.map(DocumentBuilderSectionDto.fromModel).toList(), + ); + } + + Map toJson() { + final sections = {}; + for (final section in this.sections) { + sections.addAll(section.toJson()); + } + return { + id: sections, + }; + } + + DocumentBuilderSegment toModel() { + return DocumentBuilderSegment( + id: id, + sections: sections.map((e) => e.toModel()).toList(), + ); + } + + static Map _toJsonSections( + List sections, + ) { + final map = {}; + for (final section in sections) { + map[section.id] = section.toJson(); + } + return map; + } + + static List _fromJsonSections( + Map json, + ) { + final listOfSections = json.convertMapToListWithIds(); + return listOfSections.map(DocumentBuilderSectionDto.fromJson).toList(); + } + + @override + List get props => [id, sections]; +} + +@JsonSerializable() +class DocumentBuilderSectionDto extends Equatable { + final String id; + @JsonKey(fromJson: _fromJsonElements, toJson: _toJsonElements) + final List elements; + + const DocumentBuilderSectionDto({ + required this.id, + required this.elements, + }); + + factory DocumentBuilderSectionDto.fromJson(Map json) { + json['elements'] = Map.from(json)..remove('id'); + return _$DocumentBuilderSectionDtoFromJson(json); + } + + factory DocumentBuilderSectionDto.fromModel(DocumentBuilderSection model) { + return DocumentBuilderSectionDto( + id: model.id, + elements: + model.elements.map(DocumentBuilderElementDto.fromModel).toList(), + ); + } + + Map toJson() { + final map = {}; + for (final element in elements) { + map.addAll(element.toJson()); + } + return { + id: map, + }; + } + + DocumentBuilderSection toModel() { + return DocumentBuilderSection( + id: id, + elements: elements.map((e) => e.toModel()).toList(), + ); + } + + static Map _toJsonElements( + List elements, + ) { + final map = {}; + for (final element in elements) { + map[element.id] = element.value; + } + return map; + } + + static List _fromJsonElements( + Map json, + ) { + final listOfElements = json.convertMapToListWithIdsAndValues(); + return listOfElements.map(DocumentBuilderElementDto.fromJson).toList(); + } + + @override + List get props => [id, elements]; +} + +@JsonSerializable() +class DocumentBuilderElementDto extends Equatable { + final String id; + final dynamic value; + + const DocumentBuilderElementDto({ + required this.id, + required this.value, + }); + + factory DocumentBuilderElementDto.fromJson(Map json) { + return _$DocumentBuilderElementDtoFromJson(json); + } + + factory DocumentBuilderElementDto.fromModel(DocumentBuilderElement model) { + return DocumentBuilderElementDto( + id: model.id, + value: model.value, + ); + } + + Map toJson() => { + id: value, + }; + + DocumentBuilderElement toModel() { + return DocumentBuilderElement( + id: id, + value: value, + ); + } + + @override + List get props => [id, value]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_definitions_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_definitions_dto.dart new file mode 100644 index 00000000000..9b8d064108a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_definitions_dto.dart @@ -0,0 +1,928 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'document_definitions_dto.g.dart'; + +@JsonSerializable() +class DocumentDefinitionsDto extends Equatable { + final SegmentDto segment; + final SectionDto section; + final SingleLineTextEntryDto singleLineTextEntry; + final SingleLineHttpsURLEntryDto singleLineHttpsURLEntry; + final MultiLineTextEntryDto multiLineTextEntry; + final MultiLineTextEntryMarkdownDto multiLineTextEntryMarkdown; + final DropDownSingleSelectDto dropDownSingleSelect; + final MultiSelectDto multiSelect; + final SingleLineTextEntryListDto singleLineTextEntryList; + final MultiLineTextEntryListMarkdownDto multiLineTextEntryListMarkdown; + final SingleLineHttpsURLEntryListDto singleLineHttpsURLEntryList; + final NestedQuestionsListDto nestedQuestionsList; + final NestedQuestionsDto nestedQuestions; + final SingleGroupedTagSelectorDto singleGroupedTagSelector; + final TagGroupDto tagGroup; + final TagSelectionDto tagSelection; + @JsonKey(name: 'tokenValueCardanoADA') + final TokenValueCardanoAdaDto tokenValueCardanoAda; + final DurationInMonthsDto durationInMonths; + final YesNoChoiceDto yesNoChoice; + final AgreementConfirmationDto agreementConfirmation; + @JsonKey(name: 'spdxLicenseOrURL') + final SPDXLicenceOrUrlDto spdxLicenceOrUrl; + + const DocumentDefinitionsDto({ + required this.segment, + required this.section, + required this.singleLineTextEntry, + required this.singleLineHttpsURLEntry, + required this.multiLineTextEntry, + required this.multiLineTextEntryMarkdown, + required this.dropDownSingleSelect, + required this.multiSelect, + required this.singleLineTextEntryList, + required this.multiLineTextEntryListMarkdown, + required this.singleLineHttpsURLEntryList, + required this.nestedQuestionsList, + required this.nestedQuestions, + required this.singleGroupedTagSelector, + required this.tagGroup, + required this.tagSelection, + required this.tokenValueCardanoAda, + required this.durationInMonths, + required this.yesNoChoice, + required this.agreementConfirmation, + required this.spdxLicenceOrUrl, + }); + + factory DocumentDefinitionsDto.fromJson(Map json) => + _$DocumentDefinitionsDtoFromJson(json); + + Map toJson() => _$DocumentDefinitionsDtoToJson(this); + + List get definitionsModels => [ + segment.toModel(), + section.toModel(), + singleLineTextEntry.toModel(), + multiLineTextEntry.toModel(), + multiLineTextEntryMarkdown.toModel(), + dropDownSingleSelect.toModel(), + multiSelect.toModel(), + singleLineTextEntryList.toModel(), + multiLineTextEntryListMarkdown.toModel(), + singleLineHttpsURLEntry.toModel(), + singleLineHttpsURLEntryList.toModel(), + nestedQuestionsList.toModel(), + nestedQuestions.toModel(), + singleGroupedTagSelector.toModel(), + tagGroup.toModel(), + tagSelection.toModel(), + tokenValueCardanoAda.toModel(), + durationInMonths.toModel(), + yesNoChoice.toModel(), + agreementConfirmation.toModel(), + spdxLicenceOrUrl.toModel(), + ]; + + @override + List get props => [ + section, + segment, + singleLineTextEntry, + singleLineHttpsURLEntry, + multiLineTextEntry, + multiLineTextEntryMarkdown, + dropDownSingleSelect, + multiSelect, + singleLineTextEntryList, + multiLineTextEntryListMarkdown, + singleLineHttpsURLEntryList, + nestedQuestionsList, + nestedQuestions, + singleGroupedTagSelector, + tagGroup, + tagSelection, + tokenValueCardanoAda, + durationInMonths, + yesNoChoice, + agreementConfirmation, + spdxLicenceOrUrl, + ]; +} + +@JsonSerializable() +class SegmentDto extends Equatable { + final String type; + final bool additionalProperties; + @JsonKey(name: 'x-note') + final String note; + + const SegmentDto({ + required this.type, + required this.additionalProperties, + required this.note, + }); + + factory SegmentDto.fromJson(Map json) => + _$SegmentDtoFromJson(json); + + Map toJson() => _$SegmentDtoToJson(this); + + SegmentDefinition toModel() => SegmentDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + additionalProperties: additionalProperties, + ); + + @override + List get props => [ + type, + additionalProperties, + note, + ]; +} + +@JsonSerializable() +class SectionDto extends Equatable { + final String type; + final bool additionalProperties; + @JsonKey(name: 'x-note') + final String note; + + const SectionDto({ + required this.type, + required this.additionalProperties, + required this.note, + }); + + factory SectionDto.fromJson(Map json) => + _$SectionDtoFromJson(json); + + Map toJson() => _$SectionDtoToJson(this); + + SectionDefinition toModel() => SectionDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + additionalProperties: additionalProperties, + ); + + @override + List get props => [ + type, + additionalProperties, + note, + ]; +} + +@JsonSerializable() +class SingleLineTextEntryDto extends Equatable { + final String type; + final String contentMediaType; + final String pattern; + @JsonKey(name: 'x-note') + final String note; + + const SingleLineTextEntryDto({ + required this.type, + required this.contentMediaType, + required this.pattern, + required this.note, + }); + + factory SingleLineTextEntryDto.fromJson(Map json) => + _$SingleLineTextEntryDtoFromJson(json); + + Map toJson() => _$SingleLineTextEntryDtoToJson(this); + + SingleLineTextEntryDefinition toModel() => SingleLineTextEntryDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + contentMediaType: + DocumentDefinitionsContentMediaType.fromString(contentMediaType), + pattern: pattern, + ); + + @override + List get props => [ + type, + contentMediaType, + pattern, + note, + ]; +} + +@JsonSerializable() +class SingleLineHttpsURLEntryDto extends Equatable { + final String type; + final String format; + final String pattern; + @JsonKey(name: 'x-note') + final String note; + + const SingleLineHttpsURLEntryDto({ + required this.type, + required this.format, + required this.pattern, + required this.note, + }); + + factory SingleLineHttpsURLEntryDto.fromJson(Map json) => + _$SingleLineHttpsURLEntryDtoFromJson(json); + + Map toJson() => _$SingleLineHttpsURLEntryDtoToJson(this); + + SingleLineHttpsURLEntryDefinition toModel() { + return SingleLineHttpsURLEntryDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + pattern: pattern, + ); + } + + @override + List get props => [ + type, + format, + pattern, + note, + ]; +} + +@JsonSerializable() +class MultiLineTextEntryDto extends Equatable { + final String type; + final String contentMediaType; + final String pattern; + @JsonKey(name: 'x-note') + final String note; + + const MultiLineTextEntryDto({ + required this.type, + required this.contentMediaType, + required this.pattern, + required this.note, + }); + + factory MultiLineTextEntryDto.fromJson(Map json) => + _$MultiLineTextEntryDtoFromJson(json); + + Map toJson() => _$MultiLineTextEntryDtoToJson(this); + + MultiLineTextEntryDefinition toModel() { + return MultiLineTextEntryDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + contentMediaType: + DocumentDefinitionsContentMediaType.fromString(contentMediaType), + pattern: pattern, + ); + } + + @override + List get props => [ + type, + contentMediaType, + pattern, + note, + ]; +} + +@JsonSerializable() +class MultiLineTextEntryMarkdownDto extends Equatable { + final String type; + final String contentMediaType; + final String pattern; + @JsonKey(name: 'x-note') + final String note; + + const MultiLineTextEntryMarkdownDto({ + required this.type, + required this.contentMediaType, + required this.pattern, + required this.note, + }); + + factory MultiLineTextEntryMarkdownDto.fromJson(Map json) => + _$MultiLineTextEntryMarkdownDtoFromJson(json); + + Map toJson() => _$MultiLineTextEntryMarkdownDtoToJson(this); + + MultiLineTextEntryMarkdownDefinition toModel() { + return MultiLineTextEntryMarkdownDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + contentMediaType: + DocumentDefinitionsContentMediaType.fromString(contentMediaType), + pattern: pattern, + ); + } + + @override + List get props => [ + type, + contentMediaType, + pattern, + note, + ]; +} + +@JsonSerializable() +class DropDownSingleSelectDto extends Equatable { + final String type; + final String contentMediaType; + final String pattern; + final String format; + @JsonKey(name: 'x-note') + final String note; + + const DropDownSingleSelectDto({ + required this.type, + required this.contentMediaType, + required this.pattern, + required this.format, + required this.note, + }); + + factory DropDownSingleSelectDto.fromJson(Map json) => + _$DropDownSingleSelectDtoFromJson(json); + + Map toJson() => _$DropDownSingleSelectDtoToJson(this); + + DropDownSingleSelectDefinition toModel() { + return DropDownSingleSelectDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + contentMediaType: + DocumentDefinitionsContentMediaType.fromString(contentMediaType), + pattern: pattern, + format: DocumentDefinitionsFormat.fromString(format), + ); + } + + @override + List get props => [ + type, + contentMediaType, + pattern, + format, + note, + ]; +} + +@JsonSerializable() +class MultiSelectDto extends Equatable { + final String type; + final bool uniqueItems; + final String format; + @JsonKey(name: 'x-note') + final String note; + + const MultiSelectDto({ + required this.type, + required this.uniqueItems, + required this.format, + required this.note, + }); + + factory MultiSelectDto.fromJson(Map json) => + _$MultiSelectDtoFromJson(json); + + Map toJson() => _$MultiSelectDtoToJson(this); + + MultiSelectDefinition toModel() { + return MultiSelectDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + uniqueItems: uniqueItems, + ); + } + + @override + List get props => [ + type, + uniqueItems, + note, + format, + ]; +} + +@JsonSerializable() +class SingleLineTextEntryListDto extends Equatable { + final String type; + final String format; + final bool uniqueItems; + @JsonKey(name: 'default') + final List defaultValue; + final Map items; + @JsonKey(name: 'x-note') + final String note; + + const SingleLineTextEntryListDto({ + required this.type, + required this.format, + required this.uniqueItems, + required this.defaultValue, + required this.items, + required this.note, + }); + + factory SingleLineTextEntryListDto.fromJson(Map json) => + _$SingleLineTextEntryListDtoFromJson(json); + + Map toJson() => _$SingleLineTextEntryListDtoToJson(this); + + SingleLineTextEntryListDefinition toModel() => + SingleLineTextEntryListDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + uniqueItems: uniqueItems, + defaultValues: defaultValue, + items: items, + ); + + @override + List get props => [ + type, + note, + format, + uniqueItems, + defaultValue, + items, + ]; +} + +@JsonSerializable() +class MultiLineTextEntryListMarkdownDto extends Equatable { + final String type; + final String format; + final bool uniqueItems; + @JsonKey(name: 'default') + final List defaultValue; + final Map items; + @JsonKey(name: 'x-note') + final String note; + + const MultiLineTextEntryListMarkdownDto({ + required this.type, + required this.format, + required this.uniqueItems, + required this.defaultValue, + required this.items, + required this.note, + }); + + factory MultiLineTextEntryListMarkdownDto.fromJson( + Map json, + ) => + _$MultiLineTextEntryListMarkdownDtoFromJson(json); + + Map toJson() => + _$MultiLineTextEntryListMarkdownDtoToJson(this); + + MultiLineTextEntryListMarkdownDefinition toModel() => + MultiLineTextEntryListMarkdownDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + uniqueItems: uniqueItems, + defaultValue: defaultValue, + items: items, + ); + + @override + List get props => [ + type, + note, + format, + uniqueItems, + defaultValue, + items, + ]; +} + +@JsonSerializable() +class SingleLineHttpsURLEntryListDto extends Equatable { + final String type; + final String format; + final bool uniqueItems; + @JsonKey(name: 'default') + final List defaultValue; + final Map items; + @JsonKey(name: 'x-note') + final String note; + + const SingleLineHttpsURLEntryListDto({ + required this.type, + required this.format, + required this.uniqueItems, + required this.defaultValue, + required this.items, + required this.note, + }); + + factory SingleLineHttpsURLEntryListDto.fromJson(Map json) => + _$SingleLineHttpsURLEntryListDtoFromJson(json); + + Map toJson() => _$SingleLineHttpsURLEntryListDtoToJson(this); + + SingleLineHttpsURLEntryListDefinition toModel() => + SingleLineHttpsURLEntryListDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + uniqueItems: uniqueItems, + defaultValue: defaultValue, + items: items, + ); + + @override + List get props => [ + type, + note, + format, + uniqueItems, + defaultValue, + items, + ]; +} + +@JsonSerializable() +class NestedQuestionsListDto extends Equatable { + final String type; + final String format; + final bool uniqueItems; + @JsonKey(name: 'default') + final List defaultValue; + @JsonKey(name: 'x-note') + final String note; + + const NestedQuestionsListDto({ + required this.type, + required this.format, + required this.uniqueItems, + required this.defaultValue, + required this.note, + }); + + factory NestedQuestionsListDto.fromJson(Map json) => + _$NestedQuestionsListDtoFromJson(json); + + Map toJson() => _$NestedQuestionsListDtoToJson(this); + + NestedQuestionsListDefinition toModel() => NestedQuestionsListDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + uniqueItems: uniqueItems, + defaultValue: defaultValue, + ); + + @override + List get props => [ + type, + note, + format, + uniqueItems, + defaultValue, + ]; +} + +@JsonSerializable() +class NestedQuestionsDto extends Equatable { + final String type; + final String format; + final bool additionalProperties; + @JsonKey(name: 'x-note') + final String note; + + const NestedQuestionsDto({ + required this.type, + required this.format, + required this.additionalProperties, + required this.note, + }); + + factory NestedQuestionsDto.fromJson(Map json) => + _$NestedQuestionsDtoFromJson(json); + + Map toJson() => _$NestedQuestionsDtoToJson(this); + + NestedQuestionsDefinition toModel() { + return NestedQuestionsDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + additionalProperties: additionalProperties, + ); + } + + @override + List get props => [ + type, + format, + note, + additionalProperties, + ]; +} + +@JsonSerializable() +class SingleGroupedTagSelectorDto extends Equatable { + final String type; + final String format; + final bool additionalProperties; + @JsonKey(name: 'x-note') + final String note; + + const SingleGroupedTagSelectorDto({ + required this.type, + required this.format, + required this.additionalProperties, + required this.note, + }); + + factory SingleGroupedTagSelectorDto.fromJson(Map json) => + _$SingleGroupedTagSelectorDtoFromJson(json); + + Map toJson() => _$SingleGroupedTagSelectorDtoToJson(this); + + SingleGroupedTagSelectorDefinition toModel() { + return SingleGroupedTagSelectorDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + additionalProperties: additionalProperties, + ); + } + + @override + List get props => [ + type, + format, + note, + additionalProperties, + ]; +} + +@JsonSerializable() +class TagGroupDto extends Equatable { + final String type; + final String format; + final String pattern; + @JsonKey(name: 'x-note') + final String note; + + const TagGroupDto({ + required this.type, + required this.format, + required this.pattern, + required this.note, + }); + + factory TagGroupDto.fromJson(Map json) => + _$TagGroupDtoFromJson(json); + + Map toJson() => _$TagGroupDtoToJson(this); + + TagGroupDefinition toModel() { + return TagGroupDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + pattern: pattern, + ); + } + + @override + List get props => [ + type, + format, + pattern, + note, + ]; +} + +@JsonSerializable() +class TagSelectionDto extends Equatable { + final String type; + final String format; + final String pattern; + @JsonKey(name: 'x-note') + final String note; + + const TagSelectionDto({ + required this.type, + required this.format, + required this.pattern, + required this.note, + }); + + factory TagSelectionDto.fromJson(Map json) => + _$TagSelectionDtoFromJson(json); + + Map toJson() => _$TagSelectionDtoToJson(this); + + TagSelectionDefinition toModel() { + return TagSelectionDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + pattern: pattern, + ); + } + + @override + List get props => [ + type, + format, + pattern, + note, + ]; +} + +@JsonSerializable() +class TokenValueCardanoAdaDto extends Equatable { + final String type; + final String format; + @JsonKey(name: 'x-note') + final String note; + + const TokenValueCardanoAdaDto({ + required this.type, + required this.format, + required this.note, + }); + + factory TokenValueCardanoAdaDto.fromJson(Map json) => + _$TokenValueCardanoAdaDtoFromJson(json); + + Map toJson() => _$TokenValueCardanoAdaDtoToJson(this); + + TokenValueCardanoADADefinition toModel() => TokenValueCardanoADADefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + ); + + @override + List get props => [ + type, + note, + format, + ]; +} + +@JsonSerializable() +class DurationInMonthsDto extends Equatable { + final String type; + final String format; + @JsonKey(name: 'x-note') + final String note; + + const DurationInMonthsDto({ + required this.type, + required this.format, + required this.note, + }); + + factory DurationInMonthsDto.fromJson(Map json) => + _$DurationInMonthsDtoFromJson(json); + + Map toJson() => _$DurationInMonthsDtoToJson(this); + + DurationInMonthsDefinition toModel() { + return DurationInMonthsDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + ); + } + + @override + List get props => [ + type, + format, + note, + ]; +} + +@JsonSerializable() +class YesNoChoiceDto extends Equatable { + final String type; + final String format; + @JsonKey(name: 'default') + final bool defaultValue; + @JsonKey(name: 'x-note') + final String note; + + const YesNoChoiceDto({ + required this.type, + required this.format, + required this.defaultValue, + required this.note, + }); + + factory YesNoChoiceDto.fromJson(Map json) => + _$YesNoChoiceDtoFromJson(json); + + Map toJson() => _$YesNoChoiceDtoToJson(this); + + YesNoChoiceDefinition toModel() => YesNoChoiceDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + defaultValue: defaultValue, + ); + + @override + List get props => [ + type, + format, + note, + defaultValue, + ]; +} + +@JsonSerializable() +class AgreementConfirmationDto extends Equatable { + final String type; + final String format; + @JsonKey(name: 'default') + final bool defaultValue; + @JsonKey(name: 'const') + final bool constValue; + @JsonKey(name: 'x-note') + final String note; + + const AgreementConfirmationDto({ + required this.type, + required this.format, + required this.defaultValue, + required this.constValue, + required this.note, + }); + + factory AgreementConfirmationDto.fromJson(Map json) => + _$AgreementConfirmationDtoFromJson(json); + + Map toJson() => _$AgreementConfirmationDtoToJson(this); + + AgreementConfirmationDefinition toModel() => AgreementConfirmationDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + defaultValue: defaultValue, + constValue: constValue, + ); + + @override + List get props => [ + type, + format, + note, + defaultValue, + constValue, + ]; +} + +@JsonSerializable() +class SPDXLicenceOrUrlDto extends Equatable { + final String type; + final String contentMediaType; + final String pattern; + final String format; + @JsonKey(name: 'x-note') + final String note; + + const SPDXLicenceOrUrlDto({ + required this.type, + required this.contentMediaType, + required this.pattern, + required this.format, + required this.note, + }); + + factory SPDXLicenceOrUrlDto.fromJson(Map json) => + _$SPDXLicenceOrUrlDtoFromJson(json); + + Map toJson() => _$SPDXLicenceOrUrlDtoToJson(this); + + SPDXLicenceOrUrlDefinition toModel() => SPDXLicenceOrUrlDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + format: DocumentDefinitionsFormat.fromString(format), + pattern: pattern, + contentMediaType: + DocumentDefinitionsContentMediaType.fromString(contentMediaType), + ); + + @override + List get props => [ + type, + format, + note, + pattern, + contentMediaType, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_schema_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_schema_dto.dart new file mode 100644 index 00000000000..6ad7a9d6542 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document_schema_dto.dart @@ -0,0 +1,353 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/document_definitions_dto.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'document_schema_dto.g.dart'; + +@JsonSerializable() +class DocumentSchemaDto extends Equatable implements Identifiable { + @JsonKey(name: r'$schema') + final String schema; + @override + @JsonKey(name: r'$id') + final String id; + final String title; + final String description; + final DocumentDefinitionsDto definitions; + final String type; + final bool additionalProperties; + @JsonKey( + toJson: _toJsonProperties, + fromJson: _fromJsonProperties, + name: 'properties', + ) + final List segments; + @JsonKey(name: 'x-order') + final List order; + @JsonKey(includeToJson: false) + final String propertiesSchema; + + const DocumentSchemaDto({ + required this.schema, + required this.id, + required this.title, + required this.description, + required this.definitions, + this.type = 'object', + this.additionalProperties = false, + required this.segments, + required this.order, + required this.propertiesSchema, + }); + factory DocumentSchemaDto.fromJson(Map json) { + final segmentsMap = json['properties'] as Map; + json['propertiesSchema'] = + (segmentsMap[r'$schema'] as Map)['const']; + + return _$DocumentSchemaDtoFromJson(json); + } + + Map toJson() => _$DocumentSchemaDtoToJson(this); + + DocumentSchema toModel() { + final sortedProperties = List.from(this.segments) + ..sortByOrder(order); + final segments = sortedProperties + .where((e) => e.ref.contains('segment')) + .map((e) => e.toModel(definitions.definitionsModels)) + .toList(); + return DocumentSchema( + schema: schema, + title: title, + description: description, + segments: segments, + order: order, + propertiesSchema: propertiesSchema, + ); + } + + @override + List get props => [ + schema, + title, + description, + definitions, + type, + additionalProperties, + segments, + order, + ]; + + static Map _toJsonProperties( + List segments, + ) { + final map = {}; + for (final property in segments) { + map[property.id] = property.toJson(); + } + return map; + } + + static List _fromJsonProperties( + Map json, + ) { + final listOfSegments = json.convertMapToListWithIds(); + return listOfSegments.map(DocumentSchemaSegmentDto.fromJson).toList(); + } +} + +@JsonSerializable() +class DocumentSchemaSegmentDto extends Equatable implements Identifiable { + @JsonKey(name: r'$ref') + final String ref; + @override + final String id; + final String title; + final String description; + @JsonKey( + toJson: _toJsonProperties, + fromJson: _fromJsonProperties, + name: 'properties', + ) + final List sections; + final List required; + @JsonKey(name: 'x-order') + final List order; + + const DocumentSchemaSegmentDto({ + required this.ref, + required this.id, + this.title = '', + this.description = '', + required this.sections, + this.required = const [], + this.order = const [], + }); + + factory DocumentSchemaSegmentDto.fromJson(Map json) => + _$DocumentSchemaSegmentDtoFromJson(json); + + Map toJson() => _$DocumentSchemaSegmentDtoToJson(this); + + DocumentSchemaSegment toModel(List definitions) { + final sortedProperties = List.from(this.sections) + ..sortByOrder(order); + + final sections = sortedProperties + .where((element) => element.ref.contains('section')) + .map((e) => e.toModel(definitions, isRequired: required.contains(e.id))) + .toList(); + return DocumentSchemaSegment( + ref: definitions.getDefinition(ref), + id: id, + title: title, + description: description, + sections: sections, + ); + } + + @override + List get props => [ + ref, + id, + title, + description, + sections, + required, + order, + ]; + + static Map _toJsonProperties( + List sections, + ) { + final map = {}; + for (final property in sections) { + map[property.id] = property.toJson(); + } + return map; + } + + static List _fromJsonProperties( + Map json, + ) { + final listOfSections = json.convertMapToListWithIds(); + return listOfSections.map(DocumentSchemaSectionDto.fromJson).toList(); + } +} + +@JsonSerializable() +class DocumentSchemaSectionDto extends Equatable implements Identifiable { + @JsonKey(name: r'$ref') + final String ref; + @override + final String id; + final String title; + final String description; + @JsonKey( + toJson: _toJsonProperties, + fromJson: _fromJsonProperties, + name: 'properties', + ) + final List elements; + final List required; + @JsonKey(name: 'x-order') + final List order; + @JsonKey(name: 'if') + final Map ifs; + final Map then; // Return to this + @JsonKey(name: 'open_source') + final Map openSource; // Return to this + + const DocumentSchemaSectionDto({ + required this.ref, + required this.id, + this.title = '', + this.description = '', + required this.elements, + this.required = const [], + this.order = const [], + this.ifs = const {}, + this.then = const {}, + this.openSource = const {}, + }); + + factory DocumentSchemaSectionDto.fromJson(Map json) => + _$DocumentSchemaSectionDtoFromJson(json); + + Map toJson() => _$DocumentSchemaSectionDtoToJson(this); + + DocumentSchemaSection toModel( + List definitions, { + required bool isRequired, + }) { + final sortedElements = List.from(this.elements) + ..sortByOrder(order); + final elements = sortedElements + .where((element) => BaseDocumentDefinition.isKnownType(element.ref)) + .map((e) => e.toModel(definitions)) + .toList(); + return DocumentSchemaSection( + ref: definitions.getDefinition(ref), + id: id, + title: title, + description: description, + elements: elements, + isRequired: isRequired, + ); + } + + @override + List get props => [ + ref, + id, + title, + description, + elements, + required, + order, + ifs, + then, + ]; + + static Map _toJsonProperties( + List properties, + ) { + final map = {}; + for (final property in properties) { + map[property.id] = property.toJson(); + } + return map; + } + + static List _fromJsonProperties( + Map json, + ) { + final listOfProperties = json.convertMapToListWithIds(); + return listOfProperties.map(DocumentSchemaElementDto.fromJson).toList(); + } +} + +@JsonSerializable() +class DocumentSchemaElementDto extends Equatable implements Identifiable { + @JsonKey(name: r'$ref') + final String ref; + @override + final String id; + final String title; + final String description; + @JsonKey(includeIfNull: false) + final int? minLength; + @JsonKey(includeIfNull: false) + final int? maxLength; + @JsonKey(name: 'default') + final String defaultValue; + @JsonKey(name: 'x-guidance') + final String guidance; + @JsonKey(name: 'enum') + final List enumValues; + @JsonKey(includeIfNull: false) + final int? maxItems; + @JsonKey(includeIfNull: false) + final int? minItems; + @JsonKey(includeIfNull: false) + final int? minimum; + @JsonKey(includeIfNull: false) + final int? maximum; + // TODO(ryszard-schossler): return to this + final Map items; + + const DocumentSchemaElementDto({ + this.ref = '', + required this.id, + this.title = '', + this.description = '', + required this.minLength, + required this.maxLength, + this.defaultValue = '', + this.guidance = '', + this.enumValues = const [], + required this.maxItems, + required this.minItems, + required this.minimum, + required this.maximum, + this.items = const {}, + }); + + factory DocumentSchemaElementDto.fromJson(Map json) => + _$DocumentSchemaElementDtoFromJson(json); + + Map toJson() => _$DocumentSchemaElementDtoToJson(this); + + DocumentSchemaElement toModel(List definitions) { + return DocumentSchemaElement( + ref: definitions.getDefinition(ref), + id: id, + title: title, + description: description, + defaultValue: defaultValue, + guidance: guidance, + enumValues: enumValues, + range: Range.optionalRangeOf(min: minimum, max: maximum), + itemsRange: Range.optionalRangeOf(min: minItems, max: maxItems), + ); + } + + @override + List get props => [ + ref, + id, + title, + description, + minLength, + maxLength, + defaultValue, + guidance, + enumValues, + maxItems, + minItems, + minimum, + maximum, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/user_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/user_dto.dart new file mode 100644 index 00000000000..14236050005 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/user_dto.dart @@ -0,0 +1,159 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/utils/json_converters.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'user_dto.g.dart'; + +@JsonSerializable() +final class UserDto { + final List accounts; + final String? activeKeychainId; + + UserDto({ + this.accounts = const [], + this.activeKeychainId, + }); + + UserDto.fromModel(User data) + : this( + accounts: data.accounts.map(AccountDto.fromModel).toList(), + activeKeychainId: data.activeAccount?.keychain.id, + ); + + factory UserDto.fromJson(Map json) { + return _$UserDtoFromJson(json); + } + + Map toJson() => _$UserDtoToJson(this); + + Future toModel({ + required KeychainProvider keychainProvider, + }) async { + final accounts = []; + + for (final accountDto in this.accounts) { + final account = await accountDto.toModel( + activeKeychainId: activeKeychainId, + keychainProvider: keychainProvider, + ); + + accounts.add(account); + } + + return User( + accounts: accounts, + ); + } +} + +@JsonSerializable() +final class AccountDto { + final String keychainId; + final Set roles; + final AccountWalletInfoDto walletInfo; + final bool isProvisional; + + AccountDto({ + required this.keychainId, + required this.roles, + required this.walletInfo, + this.isProvisional = true, + }); + + AccountDto.fromModel(Account data) + : this( + keychainId: data.keychain.id, + roles: data.roles, + walletInfo: AccountWalletInfoDto.fromModel(data.walletInfo), + isProvisional: data.isProvisional, + ); + + factory AccountDto.fromJson(Map json) { + return _$AccountDtoFromJson(json); + } + + Map toJson() => _$AccountDtoToJson(this); + + Future toModel({ + required String? activeKeychainId, + required KeychainProvider keychainProvider, + }) async { + final keychain = await keychainProvider.get(keychainId); + + return Account( + keychain: keychain, + roles: roles, + walletInfo: walletInfo.toModel(), + isActive: keychainId == activeKeychainId, + isProvisional: isProvisional, + ); + } +} + +@JsonSerializable() +final class AccountWalletInfoDto { + final AccountWalletMetadataDto metadata; + @CoinConverter() + final Coin balance; + @ShelleyAddressConverter() + final ShelleyAddress address; + + AccountWalletInfoDto({ + required this.metadata, + required this.balance, + required this.address, + }); + + AccountWalletInfoDto.fromModel(WalletInfo data) + : this( + metadata: AccountWalletMetadataDto.fromModel(data.metadata), + balance: data.balance, + address: data.address, + ); + + factory AccountWalletInfoDto.fromJson(Map json) { + return _$AccountWalletInfoDtoFromJson(json); + } + + Map toJson() => _$AccountWalletInfoDtoToJson(this); + + WalletInfo toModel() { + return WalletInfo( + metadata: metadata.toModel(), + balance: balance, + address: address, + ); + } +} + +@JsonSerializable() +final class AccountWalletMetadataDto { + final String name; + final String? icon; + + AccountWalletMetadataDto({ + required this.name, + this.icon, + }); + + AccountWalletMetadataDto.fromModel(WalletMetadata data) + : this( + name: data.name, + icon: data.icon, + ); + + factory AccountWalletMetadataDto.fromJson(Map json) { + return _$AccountWalletMetadataDtoFromJson(json); + } + + Map toJson() => _$AccountWalletMetadataDtoToJson(this); + + WalletMetadata toModel() { + return WalletMetadata( + name: name, + icon: icon, + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart new file mode 100644 index 00000000000..01dfd972975 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/proposal/proposal_repository.dart @@ -0,0 +1,102 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +class ProposalRepository { + const ProposalRepository(); + + /// Fetches all proposals. + Future> getProposals({ + required String campaignId, + }) async { + // simulate network delay + await Future.delayed(const Duration(seconds: 1)); + // optionally filter by status. + return _proposals; + } +} + +final _proposalDescription = """ +Zanzibar is becoming one of the hotspots for DID's through +World Mobile and PRISM, but its potential is only barely exploited. +Zanzibar is becoming one of the hotspots for DID's through World Mobile +and PRISM, but its potential is only barely exploited. +""" + .replaceAll('\n', ' '); + +final _proposals = [ + Proposal( + id: 'f14/0', + category: 'Cardano Use Cases / MVP', + title: 'Proposal Title that rocks the world', + updateDate: DateTime.now().minusDays(2), + fundsRequested: Coin.fromAda(100000), + status: ProposalStatus.draft, + publish: ProposalPublish.draft, + access: ProposalAccess.private, + commentsCount: 0, + description: _proposalDescription, + sections: List.generate(13, (index) { + return ProposalSection( + id: 'f14/0_$index', + name: 'Section_$index', + steps: [ + ProposalSectionStep( + id: 'f14/0_${index}_1', + name: 'Topic 1', + ), + ], + ); + }), + ), + Proposal( + id: 'f14/1', + category: 'Cardano Use Cases / MVP', + title: 'Proposal Title that rocks the world', + updateDate: DateTime.now().minusDays(2), + fundsRequested: Coin.fromAda(100000), + status: ProposalStatus.draft, + publish: ProposalPublish.draft, + access: ProposalAccess.private, + commentsCount: 0, + description: _proposalDescription, + sections: List.generate(13, (index) { + return ProposalSection( + id: 'f14/0_$index', + name: 'Section_$index', + steps: [ + ProposalSectionStep( + id: 'f14/0_${index}_1', + name: 'Topic 1', + answer: index < 7 ? const MarkdownData('Ans') : null, + ), + ], + ); + }), + ), + Proposal( + id: 'f14/2', + category: 'Cardano Use Cases / MVP', + title: 'Proposal Title that rocks the world', + updateDate: DateTime.now().minusDays(2), + fundsRequested: Coin.fromAda(100000), + status: ProposalStatus.draft, + publish: ProposalPublish.draft, + access: ProposalAccess.private, + commentsCount: 0, + description: _proposalDescription, + sections: List.generate(13, (index) { + return ProposalSection( + id: 'f14/0_$index', + name: 'Section_$index', + steps: [ + ProposalSectionStep( + id: 'f14/0_${index}_1', + name: 'Topic 1', + answer: const MarkdownData('Ans'), + ), + ], + ); + }), + ), +]; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_repository.dart new file mode 100644 index 00000000000..49ecb3dd5d7 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_repository.dart @@ -0,0 +1,46 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/user_dto.dart'; +import 'package:catalyst_voices_repositories/src/user/user_storage.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +abstract interface class UserRepository { + factory UserRepository( + UserStorage storage, + KeychainProvider keychainProvider, + ) { + return UserRepositoryImpl( + storage, + keychainProvider, + ); + } + + Future getUser(); + + Future saveUser(User user); +} + +final class UserRepositoryImpl implements UserRepository { + final UserStorage _storage; + final KeychainProvider _keychainProvider; + + UserRepositoryImpl( + this._storage, + this._keychainProvider, + ); + + @override + Future getUser() async { + final dto = await _storage.readUser(); + + final user = await dto?.toModel(keychainProvider: _keychainProvider); + + return user ?? const User(accounts: []); + } + + @override + Future saveUser(User user) { + final dto = UserDto.fromModel(user); + + return _storage.writeUser(dto); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_storage.dart new file mode 100644 index 00000000000..bf403a0c2fe --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/user/user_storage.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_repositories/src/dto/user_dto.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +const _userKey = 'User'; + +abstract interface class UserStorage { + Future readUser(); + + Future writeUser(UserDto user); + + Future deleteUser(); +} + +final class SecureUserStorage extends SecureStorage implements UserStorage { + SecureUserStorage({ + super.secureStorage, + }); + + @override + Future readUser() async { + final encoded = await readString(key: _userKey); + if (encoded == null) { + return null; + } + + final decoded = json.decode(encoded) as Map; + + return UserDto.fromJson(decoded); + } + + @override + Future writeUser(UserDto user) async { + final encoded = json.encode(user.toJson()); + await writeString(encoded, key: _userKey); + } + + @override + Future deleteUser() async { + await delete(key: _userKey); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart new file mode 100644 index 00000000000..069d2ff7313 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart @@ -0,0 +1,33 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:json_annotation/json_annotation.dart'; + +final class DurationConverter implements JsonConverter { + const DurationConverter(); + + @override + Duration fromJson(int json) => Duration(seconds: json); + + @override + int toJson(Duration object) => object.inSeconds; +} + +final class CoinConverter implements JsonConverter { + const CoinConverter(); + + @override + Coin fromJson(int json) => Coin(json); + + @override + int toJson(Coin object) => object.value; +} + +final class ShelleyAddressConverter + implements JsonConverter { + const ShelleyAddressConverter(); + + @override + ShelleyAddress fromJson(String json) => ShelleyAddress.fromBech32(json); + + @override + String toJson(ShelleyAddress object) => object.toBech32(); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/openapi-filters.json b/catalyst_voices/packages/internal/catalyst_voices_repositories/openapi-filters.json similarity index 100% rename from catalyst_voices/packages/internal/catalyst_voices_services/openapi-filters.json rename to catalyst_voices/packages/internal/catalyst_voices_repositories/openapi-filters.json diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/openapi/vit.yaml b/catalyst_voices/packages/internal/catalyst_voices_repositories/openapi/vit.yaml new file mode 100644 index 00000000000..6f42e873198 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/openapi/vit.yaml @@ -0,0 +1,1313 @@ +openapi: 3.0.3 +info: + title: VIT as a Service Rest API + description: |- + Voting Implementation Testnet Rest API v0 + + ## This API is *DEPRECATED* + + ## This API will no longer be available following Catalyst Fund 10. + version: 0.2.2 + contact: + url: 'http://github.com/input-output-hk/vit-servicing-station' +tags: + - name: fund + description: Information on treasury fund campaigns. + - name: challenge + description: 'Information on challenges, structuring proposals within a fund.' + - name: proposal + description: Information on funding proposals. + - name: reviews + description: Information on reviews. + - name: snapshot + description: Continuous snapshot related information. + - name: vote + description: Historic votes related information. + - name: search + description: Search challenges and proposals information. +servers: + - url: 'http://localhost' +paths: + /api/v0/fund: + get: + operationId: getCurrentFund + summary: Get available fund + tags: + - fund + description: | + Retrieves information on the current treasury fund campaign. + responses: + '200': + description: Valid response + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/Fund' + - $ref: '#/components/schemas/NextFundInfo' + deprecated: true + '/api/v0/fund/{id}': + parameters: + - in: path + name: id + schema: + type: integer + required: true + get: + operationId: getFund + summary: Get fund by id + tags: + - fund + description: | + Retrieves information on the identified treasury fund campaign. + responses: + '200': + description: Valid response + content: + application/json: + schema: + $ref: '#/components/schemas/Fund' + '404': + description: The requested fund was not found + deprecated: true + /api/v0/funds: + get: + operationId: getFunds + summary: Get list of the fund id + tags: + - fund + description: | + Get list of all the funds in the db. + responses: + '200': + description: Valid response + content: + application/json: + schema: + type: array + items: + properties: + id: + type: integer + format: int32 + description: Identifier of the fund campaign. + '404': + description: The requested fund was not found + deprecated: true + /api/v0/proposals: + post: + summary: Get proposals by chain id + operationId: getProposalsByChainInfo + tags: + - proposal + description: | + Retrieves queried proposals. + requestBody: + description: List of voteplan id and indexes query + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProposalsByVoteplanIdAndIndexQuery' + responses: + '200': + description: Valid response + content: + application/json: + schema: + $ref: '#/components/schemas/ProposalWithChallengeInfo' + x-internal: false + deprecated: true + '/api/v0/proposals/{voter_group_id}': + parameters: + - in: path + name: voter_group_id + description: Get proposals only for the specified vote group. + schema: + type: string + required: true + get: + operationId: getAllProposals + summary: Get all available proposals + tags: + - proposal + description: | + Lists all available proposals. + responses: + '200': + description: Valid response + content: + application/json: + schema: + items: + $ref: '#/components/schemas/ProposalWithChallengeInfo' + deprecated: true + '/api/v0/proposal/{id}/{voter_group_id}': + parameters: + - in: path + name: id + schema: + type: integer + required: true + - in: path + name: voter_group_id + schema: + type: string + required: true + get: + operationId: getProposal + summary: Get proposal by id filtered by group + tags: + - proposal + description: | + Retrieves information on the identified proposal if it belongs to the provided group. + responses: + '200': + description: Valid response + content: + application/json: + schema: + $ref: '#/components/schemas/ProposalWithChallengeInfo' + '404': + description: The requested proposal was not found + deprecated: true + /api/v0/challenges: + get: + operationId: getAllChallenges + summary: Get all available challenges + tags: + - challenge + description: | + Lists all available challenges following insertion order. + responses: + '200': + description: Valid response + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Challenge' + '404': + description: The requested challenge was not found + deprecated: true + '/api/v0/challenges/{id}': + parameters: + - in: path + name: id + schema: + type: integer + required: true + get: + operationId: getChallengeById + summary: Get challenge by id + tags: + - challenge + description: | + Retrieves information on the identified challenge, + including the proposals submitted for it. + responses: + '200': + description: Valid response + content: + application/json: + schema: + $ref: '#/components/schemas/ChallengeWithProposals' + '404': + description: The requested challenge was not found + deprecated: true + '/api/v0/challenges/{id}/{voter_group_id}': + parameters: + - in: path + name: id + schema: + type: integer + required: true + - in: path + name: voter_group_id + description: Get proposals only for the specified vote group. + schema: + type: string + required: true + get: + operationId: getChallengeByIdAndVoterGroupId + summary: Get challenge by id and voter group id + tags: + - challenge + description: | + Retrieves information on the identified challenge, + including the proposals submitted for it filtered by the provided voter group id. + responses: + '200': + description: Valid response + content: + application/json: + schema: + $ref: '#/components/schemas/ChallengeWithProposals' + '404': + description: The requested challenge was not found + deprecated: true + '/api/v0/reviews/{proposal_id}': + parameters: + - in: path + name: proposal_id + schema: + type: integer + required: true + get: + operationId: getProposalReviews + summary: Get reviews related to a proposal + tags: + - reviews + description: | + Retrieves advisor reviews information for the provided proposal id. + responses: + '200': + description: Valid response + content: + application/json: + schema: + $ref: '#/components/schemas/AdvisorReviews' + deprecated: true + /api/v0/search: + post: + summary: Search various resources with various constraints + operationId: search + tags: + - search + description: Search various resources especially challenges and proposals with various constraints like contains some string etc. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SearchQuery' + responses: + '200': + description: Success + content: + application/json: + schema: + oneOf: + - type: array + items: + $ref: '#/components/schemas/Challenge' + - type: array + items: + $ref: '#/components/schemas/ProposalWithChallengeInfo' + '400': + description: Invalid combination of table/column (e.g. using funds column on challenges table) + deprecated: true + /api/v0/search_count: + post: + summary: Returns count of the result set of search operation + description: Search various resources with various constraints and returns count of the result set + operationId: searchCount + tags: + - search + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SearchCountQuery' + responses: + '200': + description: Success + content: + application/json: + schema: + type: integer + format: i32 + description: Count of the result set + '400': + description: Invalid combination of table/column (e.g. using funds column on challenges table) + deprecated: true + '/api/v0/snapshot/voter/{event}/{voting_key}': + parameters: + - schema: + type: string + name: event + in: path + required: true + description: 'The event id to get voting info for, can be `latest` or the name of the event, or its unique id.' + - schema: + type: string + name: voting_key + in: path + required: true + description: The voters voting key to query. + get: + operationId: getVoterInfo + summary: Get voter's info by voting key + tags: + - snapshot + description: | + Get voter's current voting power by voting key + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/VotersInfo' + '400': + description: There is an error with the request. + '404': + description: Voting Key not found for requested event. + deprecated: true + x-internal: true + '/api/v0/snapshot/delegator/{event}/{stake_public_key}': + parameters: + - in: path + name: event + schema: + type: string + example: latest + required: true + description: 'The event id to get voting info for, can be `latest` or the name of the event, or its unique id.' + - in: path + name: stake_public_key + schema: + type: string + required: true + description: The voters staked public key. + get: + operationId: getDelegatorInfo + summary: Get voters info by stake public key + tags: + - snapshot + description: | + Get voters delegation info by stake public key + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/DelegatorInfo' + '400': + description: An error occured + '404': + description: Stake Address for the event was not found in the snapshot. + deprecated: true + x-internal: true + /api/v0/snapshot: + get: + operationId: getSnapshotTags + summary: Get list of available versions + tags: + - snapshot + description: | + Get list of available snapshots, which can be used to retrieve + voting power (used in the `event` path parameter). + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + x-examples: + Example 1: + - latest + - Fund-11 + - '1' + - Fund-10 + - '0' + minItems: 1 + uniqueItems: true + description: |- + An array of snapshots which can be retrieved.
+ `"latest"` = The latest snapshot of the latest running event.
+ `""` = The raw ID of a particular event, eg "7".
+ `""` = The name of a particular event, eg "Fund-10". + items: + type: string + example: latest + deprecated: true + x-internal: true + '/api/v0/admin/snapshot/snapshot_info/{tag}': + put: + operationId: updateSnapshotFromSnapshotInfo + summary: Replace the snapshot data for the given tag + tags: + - snapshot + description: | + Replace the snapshot data for the given tag + parameters: + - in: path + name: tag + schema: + type: string + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SnapshotInfoUpdate' + responses: + '200': + description: Success + x-internal: true + deprecated: true + '/api/v0/admin/snapshot/raw_snapshot/{tag}': + put: + operationId: updateSnapshotFromRawSnapshot + summary: Replace the snapshot data for the given tag + tags: + - snapshot + description: | + Replace the snapshot data for the given tag + parameters: + - in: path + name: tag + schema: + type: string + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RawSnapshotUpdate' + responses: + '200': + description: Success + x-internal: true + deprecated: true + /api/v0/admin/proposals: + put: + operationId: putProposals + summary: Submit proposals + tags: + - proposal + description: | + Submit a new proposals data, replace an existing entries + requestBody: + content: + application/json: + schema: + type: array + description: List of Proposals entries in json format + items: + $ref: '#/components/schemas/Proposal' + responses: + '200': + description: Success + '400': + description: The input is malformed. + x-internal: true + deprecated: true + /api/v0/admin/fund: + put: + operationId: putFund + summary: Update or create fund + tags: + - fund + description: | + Update or replace the fund in the db with the one provided. + responses: + '200': + description: Valid response + '400': + description: The input is malformed. + x-internal: true + deprecated: true + /api/v0/votes: + post: + summary: Get voted by chain id + operationId: getVotesByChainIdAndCaster + tags: + - proposal + description: | + Retrieves queried proposals. + requestBody: + description: List of votes by voteplan id and caster (wallet) + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VotesByVoteCasterAndVoteplanId' + responses: + '200': + description: Valid response + content: + application/json: + schema: + $ref: '#/components/schemas/VoteInfo' + x-internal: true + deprecated: true +components: + schemas: + Fund: + description: |- + Note, ANY date-time formatted field in this API may report: + "1970-01-01T00:00:00Z" + Which is The unix Epoch. This date is to be interpreted that the date-time for the event the field represents is not yet known. + The UI may either not display anything for this field, or TBD or an equivalent message. + properties: + id: + type: integer + format: int32 + description: Identifier of the fund campaign. + fund_name: + type: string + description: Human-readable name of the fund campaign. + fund_goal: + type: string + description: Description of the campaign's goals. + voting_power_info: + type: string + deprecated: true + description: 'Deprecated, same as registration_snapshot_time.' + voting_power_threshold: + type: integer + format: int64 + description: | + Minimal amount of funds required for a valid voter registration. + This amount is in lovelace. + rewards_info: + type: string + fund_start_time: + type: string + format: date-time + description: Date and time for the start of the current voting period. + fund_end_time: + type: string + format: date-time + description: Date and time for the end of the current voting period. + next_fund_start_time: + type: string + format: date-time + description: Date and time for the start of the next voting period. + registration_snapshot_time: + type: string + format: date-time + description: Date and time for blockchain state snapshot capturing voter registrations. + next_registration_snapshot_time: + type: string + format: date-time + description: Date and time for next blockchain state snapshot capturing voter registrations. + chain_vote_plans: + type: array + items: + $ref: '#/components/schemas/VotePlan' + description: Vote plans registered for voting in this fund campaign. + groups: + type: array + items: + $ref: '#/components/schemas/VoterGroup' + challenges: + type: array + items: + $ref: '#/components/schemas/Challenge' + description: A list of campaign challenges structuring the proposals. + goals: + type: array + items: + $ref: '#/components/schemas/Goal' + description: The list of campaign goals for this fund. + insight_sharing_start: + type: string + format: date-time + proposal_submission_start: + type: string + format: date-time + refine_proposals_start: + type: string + format: date-time + finalize_proposals_start: + type: string + format: date-time + proposal_assessment_start: + type: string + format: date-time + assessment_qa_start: + type: string + format: date-time + snapshot_start: + type: string + format: date-time + voting_start: + type: string + format: date-time + voting_end: + type: string + format: date-time + tallying_end: + type: string + format: date-time + VotePlan: + properties: + id: + type: integer + format: int32 + description: API identifier of the vote plan. + chain_voteplan_id: + type: string + format: hash + description: Blockchain ID of the vote plan transaction. + chain_vote_start_time: + type: string + format: date-time + description: Date and time for the start of voting on this vote plan. + chain_vote_end_time: + type: string + format: date-time + description: Date and time for the end of voting on this vote plan. + chain_committee_end_time: + type: string + format: date-time + description: Date and time for the end of tallying on this vote plan. + chain_voteplan_payload: + type: string + description: | + Whether the voting is done using the public or the privacy-preserving protocol. + fund_id: + type: integer + format: int32 + description: The fund ID this vote plan belongs to. + voting_token: + type: string + pattern: '[0-9a-f]{56}(\.[0-9a-f]{1,64})?' + description: | + The identifier of voting power token used withing this plan. + Proposal: + properties: + internal_id: + type: integer + format: int32 + description: The API identifier for this proposal. + proposal_id: + type: string + description: Unique identifier for this proposal. + proposal_category: + type: object + properties: + category_id: + type: string + category_name: + type: string + category_description: + type: string + proposal_title: + type: string + description: Short title of the proposal. + proposal_summary: + type: string + description: Brief description of the proposal. + proposal_public_key: + type: string + format: binary + proposal_funds: + type: integer + format: int64 + description: The amount of funds requested by this proposal. + proposal_url: + type: string + description: URL to a web page with details on this proposal. + proposal_files: + type: string + proposal_files_url: + type: string + description: proposals files url. + proposal_impact_score: + type: integer + format: int64 + description: Impact score of this proposal. + proposer: + type: object + properties: + proposer_name: + type: string + description: Name of the author of the proposal. + proposer_email: + type: string + description: Email address of the author of the proposal. + proposer_url: + type: string + description: URL to a web resource with details about the author of the proposal. + chain_proposal_id: + type: string + description: Identifier of the proposal on the blockchain. + chain_vote_options: + description: Map of named vote options to choice indices. + type: object + chain_vote_start_time: + type: string + format: date-time + description: Date and time for the start of voting on this proposal's vote plan. + chain_vote_end_time: + type: string + format: date-time + description: Date and time for the start of voting on this proposal's vote plan. + chain_committee_end_time: + type: string + format: date-time + description: Date and time for the end of tallying on this proposal's vote plan. + chain_voteplan_payload: + type: string + description: | + Whether the voting is done using the public or the privacy-preserving protocol. + chain_vote_encryption_key: + type: string + description: Encryption key for this proposal's vote plan. + fund_id: + type: integer + format: int32 + description: The fund ID this proposal belongs to. + challenge_id: + type: integer + format: int32 + reviews_count: + type: integer + format: int32 + description: Total amount of individual reviews per assessor + extra: + type: object + description: Additional information. + ChallengeType: + type: string + enum: + - simple + - community-choice + ProposalWithChallengeInfo: + discriminator: + propertyName: challenge_type + mapping: + simple: '#/components/schemas/SimpleProposal' + community-choice: '#/components/schemas/CommunityChoiceProposal' + allOf: + - $ref: '#/components/schemas/Proposal' + - type: object + properties: + chain_voteplan_id: + type: string + description: Identifier of the vote plan a proposal belongs to. + chain_proposal_index: + type: integer + format: int64 + description: Index of a proposal in the vote plan. + group_id: + type: string + description: Group identifier. + challenge_type: + $ref: '#/components/schemas/ChallengeType' + SimpleProposal: + allOf: + - $ref: '#/components/schemas/ProposalWithChallengeInfo' + - type: object + properties: + proposal_solution: + type: string + example: + internal_id: 22 + proposal_id: 4af0e6b3452cd4ee822b2ec1859fd57b5512f85c14875f408081aa9b796dfc6e + proposal_title: Authentication for DeepFake Defense + proposal_summary: Deepfake videos are dangerous. + proposal_solution: We will create a cryptographic proof on Cardano that verifies videos are real by connecting their blockchain ID. + proposal_public_key: Fvd8zI3DH85qnaChQE6Aymt1diMJP32LB0AdpheZh/Q= + proposal_funds: 12000 + proposal_url: 'http://ideascale.com/t/UM5UZBd2t' + proposal_files_url: '' + proposal_impact_score: 0 + proposer: + proposer_name: Community Member + proposer_email: example@vit.iohk.io + proposer_url: '' + proposer_relevant_experience: 'Cryptography student, website development, blockchain technologist.' + chain_proposal_id: 4af0e6b3452cd4ee822b2ec1859fd57b5512f85c14875f408081aa9b796dfc6e + chain_vote_options: + blank: 0 + 'yes': 1 + 'no': 2 + chain_vote_start_time: '2021-02-10T14:40:27+00:00' + chain_vote_end_time: '2021-02-11T10:10:27+00:00' + chain_committee_end_time: '2021-02-11T11:40:27+00:00' + chain_voteplan_payload: public + chain_vote_encryption_key: '' + fund_id: 20 + challenge_id: 2 + challenge_type: simple + CommunityChoiceProposal: + allOf: + - $ref: '#/components/schemas/ProposalWithChallengeInfo' + - type: object + properties: + proposal_brief: + type: string + proposal_importance: + type: string + proposal_goal: + type: string + proposal_metrics: + type: string + example: + internal_id: 31 + proposal_id: 494d8d685e3b195eb5610494f1721db7747df0517cb1b6a705bb3cebfef3c998 + proposal_title: A for ADA Cryptoalphabet 4 children + proposal_summary: |- + How to increase general awareness about Cardano and cryptocurrencies? + How to make fun community-building incentives? + proposal_brief: A for ADA + proposal_importance: We need to get them while they're young. + proposal_goal: Nebulous. + proposal_metrics: \- Number of people engaged into the creation of Cryptoalphabet + proposal_public_key: zqUCWwguCt6+NHYjkpvasvccuA7l2SuabE+1C0bzf3Y= + proposal_funds: 4800 + proposal_url: 'http://ideascale.com/t/UM5UZBd1p' + proposal_files_url: '' + proposal_impact_score: 133 + proposer: + proposer_name: Community Member + proposer_email: example@vit.iohk.io + proposer_url: '' + proposer_relevant_experience: '' + chain_proposal_id: 494d8d685e3b195eb5610494f1721db7747df0517cb1b6a705bb3cebfef3c998 + chain_proposal_index: 9 + chain_vote_options: + 'no': 2 + 'yes': 1 + blank: 0 + chain_voteplan_id: b1eeb620baf1445672f6c9422481aff0f6babaf775760d187a7703027e098166 + chain_vote_start_time: '2021-02-10T14:40:27+00:00' + chain_vote_end_time: '2021-02-11T10:10:27+00:00' + chain_committee_end_time: '2021-02-11T11:40:27+00:00' + chain_voteplan_payload: public + chain_vote_encryption_key: '' + fund_id: 20 + challenge_id: 1 + challenge_type: community-choice + Challenge: + properties: + id: + type: integer + format: int32 + challenge_type: + $ref: '#/components/schemas/ChallengeType' + title: + type: string + description: + type: string + rewards_total: + type: integer + format: int64 + fund_id: + type: integer + format: int32 + challenge_url: + type: string + highlights: + $ref: '#/components/schemas/ChallengeHighlights' + ChallengeWithProposals: + allOf: + - $ref: '#/components/schemas/Challenge' + - type: object + properties: + proposals: + type: array + items: + $ref: '#/components/schemas/Proposal' + AdvisorReview: + properties: + id: + type: integer + format: i32 + proposal_id: + type: integer + format: i32 + assessor: + type: string + impact_alignment_rating_given: + $ref: '#/components/schemas/Rating' + impact_alignment_note: + type: string + feasibility_rating_given: + $ref: '#/components/schemas/Rating' + feasibility_note: + type: string + auditability_rating_given: + $ref: '#/components/schemas/Rating' + auditability_note: + type: string + ranking: + description: Measure of quality of this review according to veteran community advisors + type: string + enum: + - Excellent + - Good + - FilteredOut + - NA + Rating: + type: integer + format: i32 + minimum: 0 + maximum: 500 + description: 'Rating in range [0, 500] (0 stars to 5 stars)' + AdvisorReviews: + type: array + items: + $ref: '#/components/schemas/AdvisorReview' + example: + - id: 1 + proposal_id: 1234 + rating_given: 0 + assessor: za_assessor_432 + note: foo bar + tag: Alignment + ChallengeHighlights: + properties: + sponsor: + type: string + ProposalsByVoteplanIdAndIndexQuery: + type: array + items: + $ref: '#/components/schemas/ProposalVoteplanIdAndIndex' + ProposalVoteplanIdAndIndex: + properties: + voteplan_id: + type: string + indexes: + type: array + items: + type: integer + format: i64 + VotesByVoteCasterAndVoteplanId: + properties: + vote_plan_id: + type: string + format: hash + description: Blockchain ID of the vote plan transaction. + caster: + type: string + format: hash + description: public key of caster wallet + VoteInfo: + properties: + fragment_id: + type: string + format: hash + description: Blockchain ID of the vote plan transaction + caster: + type: string + format: hash + description: public key of caster wallet + proposal: + type: integer + format: int32 + description: proposal index within voteplan + voteplan_id: + type: string + format: hash + description: Blockchain ID of the vote plan transaction + time: + type: number + format: f32 + description: block date in format epoch.slot_no + choice: + type: string + format: byte + description: vote choice (only visible for public voting) + raw_fragment: + type: string + format: hash + description: raw bytes of transaction + SearchQuery: + properties: + table: + $ref: '#/components/schemas/SearchConstraint' + filter: + type: array + items: + $ref: '#/components/schemas/SearchConstraint' + order-by: + type: array + items: + $ref: '#/components/schemas/SearchOrderBy' + limit: + type: integer + format: i32 + description: Sets the limit clause of the search query. + offset: + type: integer + format: i32 + description: Sets the offset clause of the search query. + required: + - table + SearchCountQuery: + properties: + table: + $ref: '#/components/schemas/SearchConstraint' + filter: + type: array + items: + $ref: '#/components/schemas/SearchConstraint' + order-by: + type: array + items: + $ref: '#/components/schemas/SearchOrderBy' + required: + - table + SearchConstraint: + properties: + column: + $ref: '#/components/schemas/SearchColumn' + search: + type: string + description: Text which must be present in the given column (case insensitive) + required: + - column + - search + SearchOrderBy: + properties: + column: + $ref: '#/components/schemas/SearchColumn' + descending: + type: boolean + default: false + required: + - column + SearchTable: + type: string + enum: + - challenges + - proposals + SearchColumn: + type: string + enum: + - title + - type + - desc + - author + - funds + NextFundInfo: + properties: + next: + description: |- + Note, ANY date-time formatted field in this API may report: + "1970-01-01T00:00:00Z" + Which is The unix Epoch. This date is to be interpreted that the date-time for the event the field represents is not yet known. + The UI may either not display anything for this field, or TBD or an equivalent message. + properties: + id: + type: integer + format: int32 + description: Identifier of the fund campaign. + fund_name: + type: string + description: Human-readable name of the fund campaign. + insight_sharing_start: + type: string + format: date-time + proposal_submission_start: + type: string + format: date-time + refine_proposals_start: + type: string + format: date-time + finalize_proposals_start: + type: string + format: date-time + proposal_assessment_start: + type: string + format: date-time + assessment_qa_start: + type: string + format: date-time + snapshot_start: + type: string + format: date-time + voting_start: + type: string + format: date-time + voting_end: + type: string + format: date-time + tallying_end: + type: string + format: date-time + Goal: + properties: + id: + type: integer + format: int32 + goal_name: + type: string + fund_id: + type: integer + format: int32 + SnapshotInfoUpdate: + properties: + snapshot: + type: array + description: list of SnapshotInfo entries in json format + items: + description: SnapshotInfo entry in json format + update_timestamp: + type: string + format: date-time + description: Date and time for the latest update to this snapshot information. + RawSnapshotUpdate: + required: + - snapshot + - min_stake_threshold + - voting_power_cap + properties: + snapshot: + type: object + description: RawSnapshot in json format + min_stake_threshold: + type: integer + description: Registrations voting power threshold for eligibility + voting_power_cap: + type: string + description: Voting power cap for each account + direct_voters_group: + type: string + description: 'Voter group to assign direct voters to. If empty, defaults to "voter"' + representatives_group: + type: string + description: 'Voter group to assign representatives to. If empty, defaults to "rep"' + update_timestamp: + type: string + format: date-time + description: Date and time for the latest update to this snapshot information. + VoterGroupId: + title: VoterGroupId + x-stoplight: + id: zm2mm5do7yqyr + type: string + default: direct + example: rep + x-examples: + Direct Voter: direct + Registered Delegated Representative: rep + enum: + - direct + - rep + description: Voters Group ID.
`direct` = Direct voter.
`rep` = Delegated Representative. + VoterGroup: + type: object + properties: + id: + $ref: '#/components/schemas/VoterGroupId' + voting_token: + type: string + pattern: '[0-9a-f]{56}(\.[0-9a-f]{1,64})?' + description: | + The identifier of voting power token used withing this group. All vote plans within a + group are guaranteed to use the same token. + VotersInfo: + type: object + description: The voting power for the requested voting key. + properties: + voter_info: + type: array + items: + $ref: '#/components/schemas/VoterInfo' + last_updated: + type: string + format: date-time + description: Date and time for the latest update to this snapshot information. + as_at: + type: string + format: date-time + description: Date and time the latest snapshot represents. + final: + type: boolean + description: '`True` = this is the final snapshot which will be used for voting power in the event.
`False` =This is an interim snapshot, subject to change.' + required: + - voter_info + - last_updated + VoterInfo: + type: object + description: voter's info + example: + voting_power: 1000 + voting_group: rep + delegations_power: 400 + delegations_count: 200 + voting_power_saturation: 0.5 + properties: + voting_power: + description: voter's voting power + type: integer + format: u64 + voting_group: + $ref: '#/components/schemas/VoterGroupId' + delegations_power: + description: 'voter''s delegation''s power, which represents total voting power which was delegated to this voter' + type: integer + format: u64 + delegations_count: + description: 'amount of delegators, who was delegated to this voter' + type: integer + format: u64 + voting_power_saturation: + description: voting power's share of the total voting power corresponds to the current voting group + type: number + format: float + minimum: 0 + maximum: 100 + required: + - voting_power + - voting_group + - delegations_power + - delegations_count + - voting_power_saturation + DelegationInfo: + type: object + description: voters delegation info + example: + voting_key: f5285eeead8b5885a1420800de14b0d1960db1a990a6c2f7b517125bedc000db + group: rep + weight: 5 + value: 12345 + properties: + voting_key: + type: string + pattern: '[0-9a-f]{64}' + description: Hex encoded voting key + group: + $ref: '#/components/schemas/VoterGroupId' + weight: + type: number + format: u64 + description: Relative weight assigned to this voting key. + value: + type: number + format: u64 + description: Raw voting power distributed to this voting key. + required: + - voting_key + - group + - weight + - value + x-stoplight: + id: 68d7e039c9974 + DelegatorInfo: + title: DelegatorInfo + x-stoplight: + id: tyhr3mvj1qnxi + type: object + properties: + delegations: + type: array + minItems: 1 + uniqueItems: true + items: + $ref: '#/components/schemas/DelegationInfo' + raw_power: + type: number + description: Raw total voting power from stake address + total_power: + type: number + description: 'Total voting power, across all registered voters.' + last_updated: + type: string + format: date-time + description: Date and time for the latest update to this snapshot information. + as_at: + type: string + format: date-time + description: Date and time the latest snapshot represents. + final: + type: boolean + description: '`True` = this is the final snapshot which will be used for voting power in the event.
`False` =This is an interim snapshot, subject to change.' diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_repositories/pubspec.yaml index a9f17992966..60fe81f45ca 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/pubspec.yaml @@ -11,17 +11,22 @@ dependencies: catalyst_cardano_serialization: ^0.4.0 catalyst_voices_models: path: ../catalyst_voices_models - catalyst_voices_services: - path: ../catalyst_voices_services - chopper: ^7.2.0 + catalyst_voices_shared: + path: ../catalyst_voices_shared + chopper: ^8.0.3 + equatable: ^2.0.7 flutter: sdk: flutter - http: ^1.2.1 + http: ^1.2.2 + json_annotation: ^4.8.1 result_type: ^0.2.0 rxdart: ^0.27.7 dev_dependencies: build_runner: ^2.4.12 catalyst_analysis: ^2.0.0 + chopper_generator: ^8.0.3 + json_serializable: ^6.7.1 mockito: ^5.4.4 + swagger_dart_code_generator: ^3.0.1 test: ^1.24.9 diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json new file mode 100644 index 00000000000..fbf5d568942 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -0,0 +1,1067 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cardano.org/schemas/catalyst/f14/proposal", + "title": "F14 Submission Form", + "description": "Schema for the F14 Catalyst Proposal Submission Form", + "definitions": { + "schemaReferenceNonUI": { + "$comment": "NOT UI: used to identify the kind of template document used.", + "type": "string", + "format": "path", + "readOnly": true + }, + "segment": { + "$comment": "UI - Logical Document Section Break.", + "type": "object", + "additionalProperties": false, + "x-note": "Major sections of the proposal. Each segment contains sections of information grouped together." + }, + "section": { + "$comment": "UI - Logical Document Sub-Section Break.", + "type": "object", + "additionalProperties": false, + "x-note": "Subsections containing specific details about the proposal." + }, + "singleLineTextEntry": { + "$comment": "UI - Single Line text entry without any markup or rich text capability.", + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "x-note": "Enter a single line of text. No formatting, line breaks, or special characters are allowed." + }, + "singleLineHttpsURLEntry": { + "$comment": "UI - Single Line text entry for HTTPS Urls.", + "type": "string", + "format": "uri", + "pattern": "^https:.*", + "x-note": "Enter a valid HTTPS URL. Must start with 'https://' and be a complete, working web address." + }, + "multiLineTextEntry": { + "$comment": "UI - Multiline text entry without any markup or rich text capability.", + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^[\\S\\s]*$", + "x-note": "Enter multiple lines of plain text. You can use line breaks but no special formatting." + }, + "multiLineTextEntryMarkdown": { + "$comment": "UI - Multiline text entry with Markdown content.", + "type": "string", + "contentMediaType": "text/markdown", + "pattern": "^[\\S\\s]*$", + "x-note": "Use Markdown formatting for rich text. Available formatting:\n- Headers: # for h1, ## for h2, etc.\n- Lists: * or - for bullets, 1. for numbered\n- Emphasis: *italic* or **bold**\n- Links: [text](url)\n- Code: `inline` or ```block```" + }, + "dropDownSingleSelect": { + "$comment": "UI - Drop Down Selection of a single entry from the defined enum.", + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "format": "dropDownSingleSelect", + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." + }, + "multiSelect": { + "$comment": "UI - Multiselect from the given items.", + "type": "array", + "uniqueItems": true, + "format": "multiSelect", + "x-note": "Select multiple options from the dropdown menu. Multiple choices are allowed." + }, + "singleLineTextEntryList": { + "$comment": "UI - A Growable List of single line text (no markup or richtext).", + "type": "array", + "format": "singleLineTextEntryList", + "uniqueItems": true, + "default": [], + "items": { + "$ref": "#/definitions/singleLineTextEntry", + "maxLength": 1024 + }, + "x-note": "Add multiple single-line text entries. Each entry should be unique and under 1024 characters." + }, + "multiLineTextEntryListMarkdown": { + "$comment": "UI - A Growable List of markdown formatted text fields.", + "type": "array", + "format": "multiLineTextEntryListMarkdown", + "uniqueItems": true, + "default": [], + "items": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "maxLength": 10240 + }, + "x-note": "Add multiple markdown-formatted text entries. Each entry can include rich formatting and should be unique." + }, + "singleLineHttpsURLEntryList": { + "$comment": "UI - A Growable List of HTTPS URLs.", + "type": "array", + "format": "singleLineHttpsURLEntryList", + "uniqueItems": true, + "default": [], + "items": { + "$ref": "#/definitions/singleLineHttpsURLEntry", + "maxLength": 1024 + }, + "x-note": "Enter multiple HTTPS URLs. Each URL should be unique and under 1024 characters." + }, + "nestedQuestionsList": { + "$comment": "UI - A Growable List of Questions. The contents are an object, that can have any UI elements within.", + "type": "array", + "format": "nestedQuestionsList", + "uniqueItems": true, + "default": [], + "x-note": "Add multiple questions. Each question should be unique." + }, + "nestedQuestions": { + "$comment": "UI - The container for a nested question set.", + "type": "object", + "format": "nestedQuestions", + "additionalProperties": false, + "x-note": "Add multiple questions. Each question should be unique." + }, + "singleGroupedTagSelector": { + "$comment": "UI - A selector where a top level selection, gives a single choice from a list of tags.", + "type": "object", + "format": "singleGroupedTagSelector", + "additionalProperties": true, + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." + }, + "tagGroup": { + "$comment": "UI - An individual group within a singleGroupedTagSelector.", + "type": "string", + "format": "tagGroup", + "pattern": "^.*$", + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." + }, + "tagSelection": { + "$comment": "UI - An individual tag within the group of a singleGroupedTagSelector.", + "type": "string", + "format": "tagSelection", + "pattern": "^.*$", + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." + }, + "tokenValueCardanoADA": { + "$comment": "UI - A Token Value denominated in Cardano ADA.", + "type": "integer", + "format": "token:cardano:ada", + "x-note": "Enter the amount of Cardano ADA to be used in the proposal." + }, + "durationInMonths": { + "$comment": "UI - A Duration represented in total months.", + "type": "integer", + "format": "datetime:duration:months", + "x-note": "Enter the duration of the proposal in months." + }, + "yesNoChoice": { + "$comment": "UI - A Boolean choice, represented as a Yes/No selection. Yes = true.", + "type": "boolean", + "format": "yesNoChoice", + "default": false, + "x-note": "Select Yes or No." + }, + "agreementConfirmation": { + "$comment": "UI - A Boolean choice, defaults to `false` but its invalid if its not set to `true`.", + "type": "boolean", + "format": "agreementConfirmation", + "default": false, + "const": true, + "x-note": "Select Yes or No." + }, + "spdxLicenseOrURL": { + "$comment": "UI - Drop Down Selection of any valid SPDX Identifier. This is a complex type, it should let the user select one of the valid SPDX licenses, or enter a URL of the license if its proprietary. In the form its just a string.", + "type": "string", + "contentMediaType": "text/plain", + "pattern": "^.*$", + "format": "spdxLicenseOrURL", + "x-note": "Select one option from the dropdown menu. Only one choice is allowed." + } + }, + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "$ref": "#/definitions/schemaReferenceNonUI", + "default": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", + "const": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json" + }, + "setup": { + "$ref": "#/definitions/segment", + "title": "proposal setup", + "description": "Proposal title", + "properties": { + "title": { + "$ref": "#/definitions/section", + "title": "proposal setup", + "description": "Proposal title", + "properties": { + "title": { + "$ref": "#/definitions/singleLineTextEntry", + "title": "Proposal Title", + "description": "

Proposal title

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.

", + "minLength": 1, + "maxLength": 60, + "x-guidance": "

The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

" + } + }, + "required": [ + "title" + ] + }, + "proposer": { + "$ref": "#/definitions/section", + "properties": { + "applicant": { + "$ref": "#/definitions/singleLineTextEntry", + "title": "Name and surname of main applicant", + "description": "Name and surname of main applicant", + "x-guidance": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", + "minLength": 2, + "maxLength": 100 + }, + "type": { + "$ref": "#/definitions/dropDownSingleSelect", + "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", + "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", + "x-guidance": "

Please select from one of the following:

  1. Individual
  2. Entity (Incorporated)
  3. Entity (Not Incorporated)
", + "enum": [ + "Individual", + "Entity (Incorporated)", + "Entity (Not Incorporated)" + ], + "default": "Individual" + }, + "coproposers": { + "$ref": "#/definitions/singleLineTextEntryList", + "title": "Co-proposers and additional applicants", + "description": "Co-proposers and additional applicants", + "x-guidance": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 14 rules for added detail.

", + "maxItems": 5, + "minItems": 0 + } + }, + "required": [ + "applicant", + "type" + ], + "x-order": [ + "applicant", + "type", + "coproposers" + ] + } + }, + "required": [ + "title", + "proposer" + ], + "x-order": [ + "title", + "proposer" + ] + }, + "summary": { + "$ref": "#/definitions/segment", + "title": "Proposal Summary", + "description": "Key information about your proposal", + "properties": { + "budget": { + "$ref": "#/definitions/section", + "title": "Budget Information", + "properties": { + "requestedFunds": { + "$ref": "#/definitions/tokenValueCardanoADA", + "title": "Requested funds in ADA", + "description": "The amount of funding requested for your proposal", + "x-guidance": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:

Minimum Funding Amount per proposal:

Cardano Open: A15,000

Cardano Uses Cases: A15,000

Cardano Partners: A500,000

Maximum Funding Amount per proposal:

Cardano Open:

  • Developers (technical): A200,000
  • Ecosystem (non-technical): A100,000

Cardano Uses Cases:

  • Concept A150,000
  • Product: A500,000

Cardano Partners:

  • Enterprise R&D A2,000,000
  • Growth & Acceleration: A2,000,000
", + "minimum": 15000, + "maximum": 2000000 + } + }, + "required": [ + "requestedFunds" + ], + "x-order": [ + "requestedFunds" + ] + }, + "time": { + "$ref": "#/definitions/section", + "properties": { + "duration": { + "$ref": "#/definitions/durationInMonths", + "title": "Project Duration in Months", + "description": "Specify the expected duration of your project. Projects must be completable within 2-12 months.", + "x-guidance": "

Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", + "minimum": 2, + "maximum": 12 + } + }, + "required": [ + "duration" + ] + }, + "translation": { + "$ref": "#/definitions/section", + "title": "Translation Information", + "description": "Information about the proposal's language and translation status", + "properties": { + "isTranslated": { + "$ref": "#/definitions/yesNoChoice", + "title": "Auto-translated Status", + "description": "Indicate if your proposal has been auto-translated into English from another language", + "x-guidance": "

Tick YES if your proposal has been auto-translated into English from another language so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections. Tick NO if your proposal has not been auto-translated into English from another language

" + }, + "originalLanguage": { + "$ref": "#/definitions/singleLineTextEntry", + "title": "Original Language", + "description": "If auto-translated, specify the original language of your proposal", + "enum": [ + "Arabic", + "Chinese", + "French", + "German", + "Indonesian", + "Italian", + "Japanese", + "Korean", + "Portuguese", + "Russian", + "Spanish", + "Turkish", + "Vietnamese", + "Other" + ] + }, + "originalDocumentLink": { + "$ref": "#/definitions/singleLineHttpsURLEntry", + "title": "Original Document Link", + "description": "Provide a link to the original proposal document in its original language" + } + }, + "if": { + "properties": { + "isTranslated": { + "const": true + } + } + }, + "then": { + "required": [ + "originalLanguage", + "originalDocumentLink" + ], + "properties": { + "originalLanguage": { + "description": "Original language is required when the proposal is translated" + }, + "originalDocumentLink": { + "description": "Link to the original document is required when the proposal is translated" + } + } + }, + "else": { + "properties": { + "originalLanguage": { + "not": {} + }, + "originalDocumentLink": { + "not": {} + } + } + }, + "required": [ + "isTranslated" + ] + }, + "problem": { + "$ref": "#/definitions/section", + "title": "Problem Statement", + "description": "Define the problem your proposal aims to solve", + "properties": { + "statement": { + "$ref": "#/definitions/multiLineTextEntry", + "title": "Problem Description", + "description": "Clearly define the problem you aim to solve. This will be visible in the Catalyst voting app.", + "minLength": 10, + "maxLength": 200, + "x-guidance": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly. This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

" + }, + "impact": { + "$ref": "#/definitions/multiSelect", + "title": "Impact Areas", + "description": "Select the areas that will be most impacted by solving this problem", + "items": { + "$ref": "#/definitions/singleLineTextEntry", + "enum": [ + "Technical Infrastructure", + "User Experience", + "Developer Tooling", + "Community Growth", + "Economic Sustainability", + "Interoperability", + "Security", + "Scalability", + "Education", + "Adoption" + ] + }, + "minItems": 1, + "maxItems": 3 + } + }, + "required": [ + "statement", + "impact" + ] + }, + "solution": { + "$ref": "#/definitions/section", + "title": "Solution Overview", + "description": "Describe your proposed solution to the problem", + "properties": { + "summary": { + "$ref": "#/definitions/multiLineTextEntry", + "title": "Solution Summary", + "description": "Briefly describe your solution. Focus on what you will do or create to solve the problem.", + "minLength": 10, + "maxLength": 200, + "x-guidance": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how' This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

" + }, + "approach": { + "$ref": "#/definitions/multiLineTextEntry", + "title": "Technical Approach", + "description": "Outline the technical approach or methodology you will use", + "maxLength": 500 + }, + "innovationAspects": { + "$ref": "#/definitions/singleLineTextEntryList", + "title": "Innovation Aspects", + "description": "Key innovative aspects of your solution", + "minItems": 1, + "maxItems": 5 + } + }, + "required": [ + "summary", + "approach" + ] + }, + "supportingLinks": { + "$ref": "#/definitions/section", + "title": "Supporting Documentation", + "description": "Additional resources and documentation for your proposal", + "x-guidance": "

Here, provide links to yours or your partner organization's website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal. Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere. If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include https. Without these steps, the form will not be submittable and show errors

", + "properties": { + "mainRepository": { + "$ref": "#/definitions/singleLineHttpsURLEntry", + "title": "Main Code Repository", + "description": "Primary repository where the project's code will be hosted" + }, + "documentation": { + "$ref": "#/definitions/singleLineHttpsURLEntry", + "title": "Documentation URL", + "description": "Main documentation site or resource for the project" + }, + "other": { + "$ref": "#/definitions/singleLineHttpsURLEntryList", + "title": "Resource Links", + "description": "Links to any other relevant documentation, code repositories, or marketing materials. All links must use HTTPS.", + "minItems": 0, + "maxItems": 5 + } + } + }, + "dependencies": { + "$ref": "#/definitions/section", + "title": "Project Dependencies", + "description": "External dependencies and requirements for project success", + "x-guidance": "

If your project has any dependencies and prerequisites for your project's success, list them here. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", + "properties": { + "details": { + "$ref": "#/definitions/nestedQuestionsList", + "title": "Dependency Details", + "description": "List and describe each dependency", + "items": { + "$ref": "#/definitions/nestedQuestions", + "properties": { + "name": { + "$ref": "#/definitions/singleLineTextEntry", + "title": "Dependency Name", + "description": "Name of the organization, technology, or resource", + "maxLength": 100 + }, + "type": { + "$ref": "#/definitions/dropDownSingleSelect", + "title": "Dependency Type", + "description": "Type of dependency", + "enum": [ + "Technical", + "Organizational", + "Legal", + "Financial", + "Other" + ] + }, + "description": { + "$ref": "#/definitions/multiLineTextEntry", + "title": "Description", + "description": "Explain why this dependency is essential and how it affects your project", + "maxLength": 500 + }, + "mitigationPlan": { + "$ref": "#/definitions/multiLineTextEntry", + "title": "Mitigation Plan", + "description": "How will you handle potential issues with this dependency", + "maxLength": 300 + } + }, + "required": [ + "name", + "type", + "description" + ] + }, + "minItems": 0, + "maxItems": 10 + } + } + }, + "open_source": { + "$ref": "#/definitions/section", + "title": "Project Open Source", + "description": "Will your project's output be fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", + "x-guidance": "

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

", + "properties": { + "source_code": { + "$ref": "#/definitions/spdxLicenseOrURL" + }, + "documentation": { + "$ref": "#/definitions/spdxLicenseOrURL" + }, + "note": { + "$ref": "#/definitions/multiLineTextEntry", + "title": "More Information", + "description": "Please provide here more information on the open source status of your project outputs", + "maxLength": 500, + "x-guidance": "

If you did not answer PROPRIETARY to the above questions, the project should be open source available throughout the entire lifecycle of the project with a declared open-source repository. Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs If only certain elements of your code will be open source please clarify which elements will be open source here. If you answered NO to the above question, please give further details as to why your projects outputs will not be open source METADATA

" + } + }, + "required": [ + "source_code", + "documentation" + ], + "x-order": [ + "source_code", + "documentation", + "note" + ] + } + }, + "x-order": [ + "budget", + "time", + "translation", + "problem", + "solution", + "supportingLinks", + "dependencies", + "open_source" + ] + }, + "horizons": { + "$ref": "#/definitions/segment", + "title": "Horizons", + "properties": { + "theme": { + "$ref": "#/definitions/section", + "title": "Horizons", + "description": "Long-term vision and categorization of your project", + "properties": { + "grouped_tag": { + "$ref": "#/definitions/singleGroupedTagSelector", + "oneOf": [ + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Governance" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Governance", + "DAO" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Education" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Education", + "Learn to Earn", + "Training", + "Translation" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Community & Outreach" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Connected Community", + "Community", + "Community Outreach", + "Social Media" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Development & Tools" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Developer Tools", + "L2", + "Infrastructure", + "Analytics", + "AI", + "Research", + "UTXO", + "P2P" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Identity & Security" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Identity & Verification", + "Cybersecurity", + "Security", + "Authentication", + "Privacy" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "DeFi" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "DeFi", + "Payments", + "Stablecoin", + "Risk Management", + "Yield", + "Staking", + "Lending" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Real World Applications" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Wallet", + "Marketplace", + "Manufacturing", + "IoT", + "Financial Services", + "E-commerce", + "Business Services", + "Supply Chain", + "Real Estate", + "Healthcare", + "Tourism", + "Entertainments", + "RWA", + "Music", + "Tokenization" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Events & Marketing" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Events", + "Marketing", + "Hackathons", + "Accelerator", + "Incubator" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Interoperability" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Cross-chain", + "Interoperability", + "Off-chain", + "Legal", + "Policy Advocacy", + "Standards" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Sustainability" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Sustainability", + "Environment", + "Agriculture" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "Smart Contracts" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Smart Contract", + "Smart Contracts", + "Audit", + "Oracles" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "GameFi" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "Gaming", + "Gaming (GameFi)", + "Entertainment", + "Metaverse" + ] + } + } + }, + { + "properties": { + "group": { + "$ref": "#/definitions/tagGroup", + "const": "NFT" + }, + "tag": { + "$ref": "#/definitions/tagSelection", + "enum": [ + "NFT", + "CNFT", + "Collectibles", + "Digital Twin" + ] + } + } + } + ] + } + }, + "x-order": [ + "theme" + ] + } + }, + "x-order": [ + "theme" + ] + }, + "details": { + "$ref": "#/definitions/segment", + "title": "Your Project and Solution", + "properties": { + "solution": { + "$ref": "#/definitions/section", + "title": "Solution", + "description": "

How you write this section will depend on what type of proposal you are writing. You might want to include details on:


  • How do you perceive the problem you are solving?
  • What are your reasons for approaching it in the way that you have?
  • Who will your project engage?
  • How will you demonstrate or prove your impact?


Explain what is unique about your solution, who will benefit, and why this is important to Cardano.

", + "properties": { + "solution": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "minLength": 1, + "maxLength": 10240 + } + } + }, + "impact": { + "$ref": "#/definitions/section", + "title": "Impact", + "description": "

Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:


  • In what way will the success of your project bring value to the Cardano Community? 
  • How will you measure this impact? 
  • How will you share the outputs and opportunities that result from your project?
", + "properties": { + "impact": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "minLength": 1, + "maxLength": 10240 + } + } + }, + "feasibility": { + "$ref": "#/definitions/section", + "title": "Capabilities & Feasibility", + "description": "

Please describe your existing capabilities that demonstrate how and why you believe you’re best suited to deliver this project?

Please include the steps or processes that demonstrate that you can be trusted to manage funds properly.

", + "properties": { + "feasibility": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "minLength": 1, + "maxLength": 10240 + } + } + } + }, + "x-order": [ + "solution", + "impact", + "feasibility" + ] + }, + "milestones": { + "$ref": "#/definitions/segment", + "title": "Milestones", + "properties": { + "milestones": { + "$ref": "#/definitions/section", + "title": "Project Milestones", + "description": "

Each milestone must declare:

  • A: Milestone outputs
  • B: Acceptance criteria
  • C: Evidence of completion

Requirements:

  • For Grant Amounts up to 75k ada: minimum 3 milestones (2 + final)
  • For Grant Amounts 75k-150k ada: minimum 4 milestones (3 + final)
  • The final milestone must include Project Close-out Report and Video
", + "properties": { + "milestone_list": { + "type": "array", + "title": "Milestones", + "description": "What are the key milestones you need to achieve in order to complete your project successfully?", + "x-guidance": "

Milestone Requirements:

  • For Grant Amounts of up to 75k ada: at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)
  • For Grant Amounts over 75k ada up to 150k ada: at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)
  • For Grant Amounts over 150k ada up to 300k ada: at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)
  • For Grant Amounts exceeding 300k ada: at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)
", + "minItems": 3, + "maxItems": 6, + "items": { + "type": "object", + "required": [ + "title", + "outputs", + "acceptance_criteria", + "evidence", + "delivery_month", + "cost" + ], + "properties": { + "title": { + "$ref": "#/definitions/singleLineTextEntry", + "title": "Milestone Title", + "description": "A clear, concise title for this milestone", + "maxLength": 100 + }, + "outputs": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Milestone Outputs", + "description": "What will be delivered in this milestone", + "maxLength": 1000 + }, + "acceptance_criteria": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Acceptance Criteria", + "description": "Specific conditions that must be met", + "maxLength": 1000 + }, + "evidence": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Evidence of Completion", + "description": "How you will demonstrate achievement", + "maxLength": 1000 + }, + "delivery_month": { + "$ref": "#/definitions/durationInMonths", + "title": "Delivery Month", + "description": "The month when this milestone will be delivered", + "minimum": 1, + "maximum": 12 + }, + "cost": { + "$ref": "#/definitions/tokenValueCardanoADA", + "title": "Cost in ADA", + "description": "The cost of this milestone in ADA" + }, + "progress": { + "$ref": "#/definitions/dropDownSingleSelect", + "title": "Progress Status", + "description": "Current status of the milestone", + "enum": [ + "Not Started", + "In Progress", + "Completed", + "Delayed" + ], + "default": "Not Started" + } + } + } + } + }, + "required": [ + "milestone_list" + ] + } + }, + "x-order": [ + "milestones" + ] + }, + "pitch": { + "$ref": "#/definitions/segment", + "title": "Final Pitch", + "properties": { + "team": { + "$ref": "#/definitions/section", + "title": "Team", + "properties": { + "who": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Who is in the project team and what are their roles?", + "description": "

List your team, their Linkedin profiles (or similar) and state what aspect of the proposal’s work each team member will undertake.


If you are planning to recruit additional team members, please state what specific skills you will be looking for in the people you recruit, so readers can see that you understand what skills will be needed to complete the project.


You are expected to have already engaged the relevant members of the organizations referenced so you understand if they are willing and/or have capacity to support the project. If you have not taken any steps to engage with your team yet, it is likely that the resources will not be available if you are approved for funding, which can jeopardize the project before it has even begun. The Catalyst team cannot help with this, meaning you are expected to have understood the requirements and engaged the necessary people before submitting a proposal.


Have you engaged anyone on any of the technical group channels (eg Discord or Telegram), or do you have a direct line of communications with the people and resources required?


Important: Catalyst funding is not anonymous, and some level of ‘proof of life’ verifications will take place before initial funding is released. Also remember that your proposal will be publicly available, so make sure to obtain any consent required before including confidential or third party information.


All Project Participants must disclose their role and scope of services across any submitted proposals, even if they are not in the lead or co-proposer role, such as an implementer, vendor, service provider, etc. Failure to disclose this information may lead to disqualification from the current grant round.

", + "minLength": 1, + "maxLength": 10240 + } + } + }, + "budget": { + "$ref": "#/definitions/section", + "title": "Budget & Costs", + "properties": { + "costs": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "Please provide a cost breakdown of the proposed work and resources", + "description": "

Make sure every element mentioned in your plan reflects its cost. It may be helpful to refer to your plan and timeline, list all the resources you will need at each stage, and what they cost.


Here, provide a clear description of any third party product or service you will be using. This could be hardware, software licenses, professional services (legal, accounting, code auditing, etc) but does not need to include the use of contracted programmers and developers.


The exact budget elements you include will depend on what type of work you are doing, and you might need to give less detail for a small, low-budget proposal. If the cost of the project will exceed the funding request, please provide information about alternative sources of funding.


Consider including budget elements for publicity / marketing / promotion / community engagement; project management; documentation; and reporting back to the community. Most proposals need these, but many proposers forget to include them.


It is the project team’s responsibility to properly manage the funds provided. Make sure to reference Fund Rules to understand eligibility around costs.

", + "minLength": 1, + "maxLength": 10240 + } + } + }, + "value": { + "$ref": "#/definitions/section", + "title": "Value for Money", + "properties": { + "note": { + "$ref": "#/definitions/multiLineTextEntryMarkdown", + "title": "How does the cost of the project represent value for money for the Cardano ecosystem?", + "description": "

Use the response to provide the context about the costs you listed previously, particularly if they are high.


It may be helpful to include some brief information on how you have decided on the costs of the project. 


For instance, can you justify with supporting evidence that costs are proportional to the average wage in your country, or typical freelance rates in your industry? Is there anything else that helps to support how the project represents value for money?

", + "minLength": 1, + "maxLength": 10240 + } + } + } + }, + "x-order": [ + "team", + "budget", + "value" + ] + }, + "agreements": { + "$ref": "#/definitions/segment", + "title": "Acknowledgements", + "properties": { + "mandatory": { + "$ref": "#/definitions/section", + "title": "Mandatory", + "properties": { + "fund_rules": { + "$ref": "#/definitions/agreementConfirmation", + "title": "Fund Rules:", + "description": "

By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the Fund Rules.

" + }, + "terms_and_conditions": { + "$ref": "#/definitions/agreementConfirmation", + "title": "Terms and Conditions:", + "description": "

By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions.

" + }, + "privacy_policy": { + "$ref": "#/definitions/agreementConfirmation", + "title": "Privacy Policy: ", + "description": "

I acknowledge and agree that any data I share in connection with my participation in Project Catalyst Fund14 will be collected, stored, used and processed in accordance with the Catalyst FC’s Privacy Policy.

" + } + }, + "required": [ + "fund_rules", + "terms_and_conditions", + "privacy_policy" + ], + "x-order": [ + "fund_rules", + "terms_and_conditions", + "privacy_policy" + ] + } + }, + "x-order": [ + "mandatory" + ] + } + }, + "x-order": [ + "setup", + "summary", + "horizons", + "details", + "milestones", + "pitch", + "agreements" + ] +} \ No newline at end of file diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json new file mode 100644 index 00000000000..e0100f00cc8 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json @@ -0,0 +1,138 @@ +{ + "$schema": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", + "setup": { + "title": { + "title": "Example Catalyst Proposal" + }, + "proposer": { + "applicant": "John Smith", + "type": "Individual", + "coproposers": [ + "Jane Doe", + "Bob Wilson" + ] + } + }, + "summary": { + "budget": { + "requestedFunds": 150000 + }, + "time": { + "duration": 6 + }, + "translation": { + "isTranslated": true, + "originalLanguage": "German", + "originalDocumentLink": "https://example.com/original-doc" + }, + "problem": { + "statement": "Current challenge in the Cardano ecosystem...", + "impact": [ + "Technical Infrastructure", + "Developer Tooling", + "Adoption" + ] + }, + "solution": { + "summary": "Our solution provides a comprehensive toolkit...", + "approach": "We will implement this solution using...", + "innovationAspects": [ + "Novel testing framework", + "Automated integration tools" + ] + }, + "supportingLinks": { + "mainRepository": "https://github.com/example/project", + "documentation": "https://docs.example.com", + "other": [ + "https://example.com/whitepaper", + "https://example.com/roadmap" + ] + }, + "dependencies": { + "details": [ + { + "name": "External API Service", + "type": "Technical", + "description": "Integration with third-party API service", + "mitigationPlan": "Build fallback mechanisms and maintain alternative providers" + } + ] + }, + "open_source": { + "source_code": "MIT", + "documentation": "MIT", + "note": "All project outputs will be open source under MIT license" + } + }, + "horizons": { + "theme": { + "grouped_tag": { + "group": "DeFi", + "tag": "Staking" + } + } + }, + "details": { + "solution": { + "solution": "Our solution involves developing a comprehensive toolkit that will enhance the Cardano developer experience..." + }, + "impact": { + "impact": "The project will significantly impact developer productivity by reducing development time and improving code quality..." + }, + "feasibility": { + "feasibility": "Our team has extensive experience in blockchain development and has successfully delivered similar projects..." + } + }, + "milestones": { + "milestones": { + "milestone_list": [ + { + "title": "Initial Setup and Planning", + "outputs": "Project infrastructure setup and detailed planning documents", + "acceptance_criteria": "- Development environment configured\n- Detailed project plan approved", + "evidence": "- GitHub repository setup\n- Documentation of infrastructure\n- Project planning documents", + "delivery_month": 1, + "cost": 30000, + "progress": "Not Started" + }, + { + "title": "Core Development", + "outputs": "Implementation of main features", + "acceptance_criteria": "- Core features implemented\n- Unit tests passing", + "evidence": "- Code repository\n- Test results\n- Technical documentation", + "delivery_month": 3, + "cost": 60000, + "progress": "Not Started" + }, + { + "title": "Final Release and Documentation", + "outputs": "Project completion, documentation, and Project Close-out Report and Video", + "acceptance_criteria": "- All features implemented and tested\n- Documentation complete\n- Close-out report and video delivered", + "evidence": "- Final release\n- Complete documentation\n- Close-out report and video", + "delivery_month": 6, + "cost": 60000, + "progress": "Not Started" + } + ] + } + }, + "pitch": { + "team": { + "who": "Our team consists of experienced blockchain developers with proven track records..." + }, + "budget": { + "costs": "Budget breakdown:\n- Development (70%): 105,000 ADA\n- Testing (15%): 22,500 ADA\n- Documentation (15%): 22,500 ADA" + }, + "value": { + "note": "This project provides excellent value for money by delivering essential developer tools..." + } + }, + "agreements": { + "mandatory": { + "fund_rules": true, + "terms_and_conditions": true, + "privacy_policy": true + } + } +} \ No newline at end of file diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/helpers/read_json.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/helpers/read_json.dart new file mode 100644 index 00000000000..982db98a6ba --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/helpers/read_json.dart @@ -0,0 +1,10 @@ +import 'dart:io'; + +String readJson(String name) { + var dir = Directory.current.path; + + if (dir.endsWith('/test')) { + dir = dir.replaceAll('/test', ''); + } + return File('$dir/$name').readAsStringSync(); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_builder_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_builder_test.dart new file mode 100644 index 00000000000..d07c5b43bad --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_builder_test.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/document_builder_dto.dart'; +import 'package:catalyst_voices_repositories/src/dto/document_schema_dto.dart'; +import 'package:test/test.dart'; + +import '../../helpers/read_json.dart'; + +void main() { + group('DocumentBuilder', () { + const schemaPath = + 'test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json'; + + late Map schemaJson; + + setUpAll(() { + schemaJson = json.decode(readJson(schemaPath)) as Map; + }); + + test('Converts segments list into object for JSON', () { + final schemaDto = DocumentSchemaDto.fromJson(schemaJson); + final schema = schemaDto.toModel(); + + final proposalBuilder = DocumentBuilder.build(schema); + final proposalBuilderDto = DocumentBuilderDto.fromModel(proposalBuilder); + final proposalBuilderJson = proposalBuilderDto.toJson(); + + for (final segment in proposalBuilderDto.segments) { + expect(proposalBuilderJson[segment.id], isA>()); + } + }); + + test('Converts object from JSON into List of segments', () { + final schemaDto = DocumentSchemaDto.fromJson(schemaJson); + final schema = schemaDto.toModel(); + + final proposalBuilder = DocumentBuilder.build(schema); + final proposalBuilderDto = DocumentBuilderDto.fromModel(proposalBuilder); + + final proposalBuilderJson = proposalBuilderDto.toJson(); + final proposalBuilderDtoFromJson = + DocumentBuilderDto.fromJson(proposalBuilderJson); + + expect( + proposalBuilderDtoFromJson.segments.length, + proposalBuilderDto.segments.length, + ); + }); + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_definitions_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_definitions_test.dart new file mode 100644 index 00000000000..02f357faad5 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_definitions_test.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/document_schema_dto.dart'; +import 'package:test/test.dart'; + +import '../../helpers/read_json.dart'; + +void main() { + group('DocumentDefinitions', () { + const schemaPath = + 'test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json'; + + late Map schemaJson; + + setUpAll(() { + schemaJson = json.decode(readJson(schemaPath)) as Map; + }); + test( + // ignore: lines_longer_than_80_chars + 'Check if all definition are in definition list inside DefinitionDto model', + () async { + final schemaDto = DocumentSchemaDto.fromJson(schemaJson); + final definitions = schemaDto.definitions.definitionsModels; + + for (final value + in BaseDocumentDefinition.refPathToDefinitionType.values) { + final occurrences = definitions + .where((element) => element.runtimeType == value) + .length; + expect( + occurrences, + equals(1), + reason: 'Value $value appears $occurrences times in the list', + ); + } + }, + ); + + test('Check if document definition media type is parse correctly', () { + final schemaDto = DocumentSchemaDto.fromJson(schemaJson); + final definitions = schemaDto.definitions.definitionsModels; + + final singleLineTextEntry = + definitions.getDefinition('#/definitions/singleLineTextEntry') + as SingleLineTextEntryDefinition; + + expect( + singleLineTextEntry.contentMediaType, + DocumentDefinitionsContentMediaType.textPlain, + ); + }); + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_schema_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_schema_test.dart new file mode 100644 index 00000000000..a5c7969d46a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document_builder/document_schema_test.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/document_schema_dto.dart'; +import 'package:test/test.dart'; + +import '../../helpers/read_json.dart'; + +void main() { + group('DocumentSchema', () { + const schemaPath = + 'test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json'; + + late Map schemaJson; + + setUpAll(() { + schemaJson = json.decode(readJson(schemaPath)) as Map; + }); + + test('X-order of segments is kept in model class', () async { + final schemaDto = DocumentSchemaDto.fromJson(schemaJson); + + final schema = schemaDto.toModel(); + + if (schemaDto.order.length != schema.segments.length) { + return; + } + for (var i = 0; i < schema.segments.length; i++) { + expect(schema.segments[i].id, schemaDto.order[i]); + } + }); + + test('X-order of section is kept in model class', () { + final schemaDto = DocumentSchemaDto.fromJson(schemaJson); + final schema = schemaDto.toModel(); + + for (var i = 0; i < schema.segments.length; i++) { + if (schemaDto.segments[i].order.length != + schema.segments[i].sections.length) { + continue; + } + for (var j = 0; j < schema.segments[i].sections.length; j++) { + expect( + schema.segments[i].sections[j].id, + schemaDto.segments[i].order[j], + ); + } + } + }); + + test('Check if every segment has a SegmentDefinition as ref', () { + final schemaDto = DocumentSchemaDto.fromJson(schemaJson); + final schema = schemaDto.toModel(); + + for (final segment in schema.segments) { + expect(segment.ref, isA()); + } + }); + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/README.md b/catalyst_voices/packages/internal/catalyst_voices_services/README.md index 7b8e3d41d8e..7a752204a2f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/README.md +++ b/catalyst_voices/packages/internal/catalyst_voices_services/README.md @@ -1,37 +1 @@ # Catalyst Voices Services - -## Automated Code Generation - -This package is used for the code generation from the OpenAPI specifications. -It leverages `swagger_dart_code_generator` library and the artifacts generated -for the documentation of the `catalyst-gateway` backend. -The process consists in 3 simple steps: - -1. The OpenAPI specification is picked from the artifact generated in the -`Earthfile` of `catalyst-gateway`. -2. The code is generated and saved as an artifact in the `Earthfile` of -`catalyst_voices` -3. Generated code is placed in the proper location within the `catalyst_voices` -project (`packages/catalyst_voices_services/lib/generated/catalyst_gateway`) -and it's ready for local usage. - -This process can be achieved by executing from the `catalyst_voices` root -folder: - -```sh -earthly +code-generator --platform=linux/amd64 --save_locally=true -``` - -The `--platform=linux/amd64` flag is necessary only when running the command from -a different platform such as **Windows** or **macOS**. -It ensures that the code generation process is compatible with the target platform. -If you are running the command on a **Linux** platform, you can omit this flag. - -In this way it's possible to locally generate the code using the same version of -OpenAPI specs defined in the backend code and developers have full control of -what should be committed. - -To ensure the consistency of the generated code (especially when backend changes -occur) an earthly target is automatically executed on PR against main. -This `+test-flutter-code-generator` generates the code on the CI and compares -it with the code currently in repo, failing if there is an inconsistency. diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/build.yaml b/catalyst_voices/packages/internal/catalyst_voices_services/build.yaml deleted file mode 100644 index 87eb4af5de7..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/build.yaml +++ /dev/null @@ -1,15 +0,0 @@ -targets: - $default: - sources: - - lib/** - - openapi/** - - $package$ - builders: - chopper_generator: - options: - header: "// Generated code" - swagger_dart_code_generator: - options: - input_folder: "openapi/" - output_folder: "lib/generated/catalyst_gateway" - separate_models: true \ No newline at end of file diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway/client_index.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway/client_index.dart deleted file mode 100644 index f3649ba012f..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/generated/catalyst_gateway/client_index.dart +++ /dev/null @@ -1 +0,0 @@ -export 'cat_gateway_api.swagger.dart' show CatGatewayApi; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart new file mode 100644 index 00000000000..58e5e2388bb --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/campaign/campaign_service.dart @@ -0,0 +1,36 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; + +abstract interface class CampaignService { + factory CampaignService( + CampaignRepository campaignRepository, + ) { + return CampaignServiceImpl( + campaignRepository, + ); + } + + Future isAnyCampaignActive(); + + Future getActiveCampaign(); +} + +final class CampaignServiceImpl implements CampaignService { + final CampaignRepository _campaignRepository; + + const CampaignServiceImpl( + this._campaignRepository, + ); + + @override + Future isAnyCampaignActive() async { + return true; + } + + @override + Future getActiveCampaign() async { + final campaign = await _campaignRepository.getCampaign(id: 'F14'); + + return campaign; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart index 2032618e3b0..b4344585cd0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/catalyst_voices_services.dart @@ -1,17 +1,8 @@ -export 'crypto/key_derivation.dart'; +export 'campaign/campaign_service.dart' show CampaignService; +export 'config/config_service.dart' show ConfigService; export 'downloader/downloader.dart'; -export 'keychain/keychain.dart'; -export 'keychain/keychain_provider.dart'; -export 'keychain/keychain_transformers.dart'; -export 'keychain/vault_keychain.dart'; -export 'keychain/vault_keychain_provider.dart'; +export 'proposal/proposal_service.dart' show ProposalService; export 'registration/registration_progress_notifier.dart'; export 'registration/registration_service.dart' show RegistrationService; export 'registration/registration_transaction_builder.dart'; -export 'storage/dummy_auth_storage.dart'; -export 'storage/secure_storage.dart'; -export 'storage/storage.dart'; -export 'storage/vault/secure_storage_vault.dart'; -export 'storage/vault/vault.dart'; export 'user/user_service.dart' show UserService; -export 'user/user_storage.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/config/config_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/config/config_service.dart new file mode 100644 index 00000000000..c8a1c0376c2 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/config/config_service.dart @@ -0,0 +1,22 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; + +// ignore: one_member_abstracts +abstract interface class ConfigService { + factory ConfigService(ConfigRepository repository) { + return ConfigServiceImpl(repository); + } + + Future getAppConfig(); +} + +final class ConfigServiceImpl implements ConfigService { + final ConfigRepository _repository; + + ConfigServiceImpl( + this._repository, + ); + + @override + Future getAppConfig() => _repository.getAppConfig(); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain.dart deleted file mode 100644 index 238706dd85c..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/lockable.dart'; - -abstract interface class Keychain implements Lockable { - String get id; - - Future get isEmpty; - - Future get metadata; - - Future getMasterKey(); - - Future setMasterKey(Bip32Ed25519XPrivateKey key); - - Future clear(); -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_transformers.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_transformers.dart deleted file mode 100644 index 37e2101565d..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_transformers.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:async'; - -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; - -final class KeychainToUnlockTransformer - extends StreamTransformerBase { - KeychainToUnlockTransformer(); - - @override - Stream bind(Stream stream) { - return Stream.eventTransformed( - stream, - _KeychainUnlockStreamSink.new, - ); - } -} - -final class _KeychainUnlockStreamSink implements EventSink { - final EventSink _outputSink; - StreamSubscription? _streamSub; - - _KeychainUnlockStreamSink(this._outputSink); - - @override - void add(Keychain? event) { - final stream = event?.watchIsUnlocked ?? Stream.value(false); - - unawaited(_streamSub?.cancel()); - _streamSub = stream.listen(_outputSink.add); - } - - @override - void addError(Object error, [StackTrace? stackTrace]) { - _outputSink.addError(error, stackTrace); - } - - @override - void close() { - unawaited(_streamSub?.cancel()); - _streamSub = null; - _outputSink.close(); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain.dart deleted file mode 100644 index 171fb17495f..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; - -const _metadataKey = 'metadata'; -const _rootKey = 'rootKey'; - -const _allKeys = [ - _rootKey, -]; - -final class VaultKeychain extends SecureStorageVault implements Keychain { - /// See [SecureStorageVault.isStorageKey]. - static bool isKeychainKey(String value) { - return SecureStorageVault.isStorageKey(value); - } - - /// See [SecureStorageVault.getStorageId]. - static String getStorageId(String value) { - return SecureStorageVault.getStorageId(value); - } - - final _initializationCompleter = Completer(); - - VaultKeychain({ - required super.id, - super.secureStorage, - }) { - unawaited(_initialize()); - } - - @override - Future get isEmpty async { - await _initializationCompleter.future; - for (final key in _allKeys) { - if (await contains(key: key)) { - return false; - } - } - - return true; - } - - @override - Future get metadata async { - await _initializationCompleter.future; - final metadata = await _readMetadata(); - assert(metadata != null, 'Keychain was not initialized correctly'); - return metadata!; - } - - @override - Future writeString( - String? value, { - required String key, - }) async { - await _initializationCompleter.future; - return super.writeString(value, key: key).whenComplete(_updateUpdateAt); - } - - @override - Future readString({required String key}) async { - await _initializationCompleter.future; - return super.readString(key: key); - } - - @override - Future getMasterKey() async { - final encodedMasterKey = await readString(key: _rootKey); - return encodedMasterKey != null - ? Bip32Ed25519XPrivateKeyFactory.instance.fromHex(encodedMasterKey) - : null; - } - - @override - Future setMasterKey(Bip32Ed25519XPrivateKey data) async { - await writeString(data.toHex(), key: _rootKey); - } - - Future _initialize() async { - await _ensureHasMetadata(); - - _initializationCompleter.complete(); - } - - Future _ensureHasMetadata() async { - if (await _readMetadata() == null) { - await _writeMetadata(_newMetadata()); - } - } - - Future _updateUpdateAt() async { - final metadata = await this.metadata; - final updated = metadata.copyWith(updatedAt: DateTime.timestamp()); - - await _writeMetadata(updated); - } - - Future _readMetadata() async { - final key = buildKey(_metadataKey); - final encoded = await secureStorage.read(key: key); - if (encoded == null) { - return null; - } - - final decoded = json.decode(encoded) as Map; - return KeychainMetadata.fromJson(decoded); - } - - Future _writeMetadata(KeychainMetadata value) async { - final key = buildKey(_metadataKey); - final decoded = value.toJson(); - final encoded = json.encode(decoded); - - await secureStorage.write(key: key, value: encoded); - } - - KeychainMetadata _newMetadata() { - return KeychainMetadata( - createdAt: DateTime.timestamp(), - updatedAt: DateTime.timestamp(), - ); - } - - @override - String toString() => 'VaultKeychain[$id]'; -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart new file mode 100644 index 00000000000..3d2b87f3fa0 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/proposal/proposal_service.dart @@ -0,0 +1,32 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; + +// ignore: one_member_abstracts +abstract interface class ProposalService { + factory ProposalService( + ProposalRepository proposalRepository, + ) { + return ProposalServiceImpl( + proposalRepository, + ); + } + + /// Fetches proposals for the [campaignId]. + Future> getProposals({required String campaignId}); +} + +final class ProposalServiceImpl implements ProposalService { + final ProposalRepository _proposalRepository; + + const ProposalServiceImpl( + this._proposalRepository, + ); + + @override + Future> getProposals({required String campaignId}) async { + final proposals = + await _proposalRepository.getProposals(campaignId: campaignId); + + return proposals; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_service.dart index e7f162a345a..d67454ad140 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_service.dart @@ -6,7 +6,7 @@ import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; import 'package:catalyst_voices_services/catalyst_voices_services.dart'; -import 'package:logging/logging.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:uuid/uuid.dart'; // TODO(damian-molinski): remove once recover account is implemented @@ -53,13 +53,6 @@ abstract interface class RegistrationService { required SeedPhrase seedPhrase, }); - /// Creates [Keychain] for given [account] with [lockFactor]. - Future createKeychainFor({ - required Account account, - required SeedPhrase seedPhrase, - required LockFactor lockFactor, - }); - /// Builds an unsigned registration transaction from given parameters. /// /// Throws a subclass of [RegistrationException] in case of a failure. @@ -148,13 +141,16 @@ final class RegistrationServiceImpl implements RegistrationService { throw const RegistrationUnknownException(); } - // TODO(dtscalac): support more roles when backend is ready + // TODO(dtscalac): derive a key from the seed phrase and fetch + // from the backend info about the registration (roles, wallet, etc). final roles = {AccountRole.root}; + final keychainId = const Uuid().v4(); + final keychain = await _keychainProvider.create(keychainId); // Note. with rootKey query backend for account details. return Account( - keychainId: keychainId, + keychain: keychain, roles: roles, walletInfo: WalletInfo( metadata: const WalletMetadata(name: 'Dummy Wallet'), @@ -164,23 +160,6 @@ final class RegistrationServiceImpl implements RegistrationService { ); } - @override - Future createKeychainFor({ - required Account account, - required SeedPhrase seedPhrase, - required LockFactor lockFactor, - }) async { - final keychainId = account.keychainId; - final masterKey = await deriveMasterKey(seedPhrase: seedPhrase); - - final keychain = await _keychainProvider.create(keychainId); - await keychain.setLock(lockFactor); - await keychain.unlock(lockFactor); - await keychain.setMasterKey(masterKey); - - return keychain; - } - @override Future prepareRegistration({ required CardanoWallet wallet, @@ -200,15 +179,10 @@ final class RegistrationServiceImpl implements RegistrationService { ), ); - final keyPair = await _keyDerivation.deriveAccountRoleKeyPair( - masterKey: masterKey, - // TODO(dtscalac): support more roles when backend is ready - role: AccountRole.root, - ); - final registrationBuilder = RegistrationTransactionBuilder( transactionConfig: config, - keyPair: keyPair, + keyDerivation: _keyDerivation, + masterKey: masterKey, networkId: networkId, roles: roles, changeAddress: changeAddress, @@ -257,7 +231,7 @@ final class RegistrationServiceImpl implements RegistrationService { final address = await enabledWallet.getChangeAddress(); return Account( - keychainId: keychainId, + keychain: keychain, roles: roles, walletInfo: WalletInfo( metadata: WalletMetadata.fromCardanoWallet(wallet), @@ -278,7 +252,7 @@ final class RegistrationServiceImpl implements RegistrationService { required SeedPhrase seedPhrase, required LockFactor lockFactor, }) async { - final roles = {AccountRole.root}; + final roles = {AccountRole.voter, AccountRole.proposer}; final masterKey = await deriveMasterKey(seedPhrase: seedPhrase); final keychain = await _keychainProvider.create(keychainId); @@ -287,7 +261,7 @@ final class RegistrationServiceImpl implements RegistrationService { await keychain.setMasterKey(masterKey); return Account( - keychainId: keychainId, + keychain: keychain, roles: roles, walletInfo: WalletInfo( metadata: const WalletMetadata(name: 'Dummy Wallet'), diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_transaction_builder.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_transaction_builder.dart index 173dbeb38cf..b66dec8f62d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_transaction_builder.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/registration/registration_transaction_builder.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; /// The transaction metadata used for registration. typedef RegistrationMetadata = X509MetadataEnvelope; @@ -17,8 +18,11 @@ final class RegistrationTransactionBuilder { /// The transaction config with current network parameters. final TransactionBuilderConfig transactionConfig; - /// The key pair used to sign the user registration certificate. - final Bip32Ed25519XKeyPair keyPair; + /// The algorithm for deriving keys. + final KeyDerivation keyDerivation; + + /// The master key derived from the seed phrase. + final Bip32Ed25519XPrivateKey masterKey; /// The network ID where the transaction will be submitted. final NetworkId networkId; @@ -40,7 +44,8 @@ final class RegistrationTransactionBuilder { const RegistrationTransactionBuilder({ required this.transactionConfig, - required this.keyPair, + required this.keyDerivation, + required this.masterKey, required this.networkId, required this.roles, required this.changeAddress, @@ -67,36 +72,56 @@ final class RegistrationTransactionBuilder { } Future _buildMetadataEnvelope() async { - final cert = await _generateX509Certificate(keyPair: keyPair); - final derCert = cert.toDer(); + final rootKeyPair = await keyDerivation.deriveAccountRoleKeyPair( + masterKey: masterKey, + role: AccountRole.root, + ); + + final cert = await _generateX509Certificate(keyPair: rootKeyPair); + final derCerts = { + AccountRole.root: cert.toDer(), + }; + + final publicKeys = { + AccountRole.root: rootKeyPair.publicKey.toPublicKey(), + if (roles.contains(AccountRole.proposer)) + AccountRole.proposer: await _deriveProposerPublicKey(), + }; final x509Envelope = X509MetadataEnvelope.unsigned( purpose: UuidV4.fromString(_catalystUserRoleRegistrationPurpose), txInputsHash: TransactionInputsHash.fromTransactionInputs(utxos), chunkedData: RegistrationData( - derCerts: [derCert], - publicKeys: [keyPair.publicKey.toPublicKey()], + derCerts: derCerts.values.toList(), + publicKeys: publicKeys.values.toList(), roleDataSet: { - // TODO(dtscalac): currently we only support the voter account role, - // regardless of selected roles - // TODO(dtscalac): when RBAC specification will define other roles - // they should be registered here RoleData( roleNumber: AccountRole.root.number, - roleSigningKey: const LocalKeyReference( + roleSigningKey: LocalKeyReference( keyType: LocalKeyReferenceType.x509Certs, - offset: 0, + offset: derCerts.keys.toList().indexOf(AccountRole.root), ), // Refer to first key in transaction outputs, // in our case it's the change address (which the user controls). paymentKey: -1, ), + if (roles.contains(AccountRole.proposer)) + RoleData( + roleNumber: AccountRole.proposer.number, + roleSigningKey: LocalKeyReference( + keyType: LocalKeyReferenceType.pubKeys, + offset: publicKeys.keys.toList().indexOf(AccountRole.proposer), + ), + // Refer to first key in transaction outputs, + // in our case it's the change address (which the user controls). + paymentKey: -1, + ), }, ), ); return x509Envelope.sign( - privateKey: keyPair.privateKey, + privateKey: rootKeyPair.privateKey, serializer: (e) => e.toCbor(), ); } @@ -172,5 +197,14 @@ final class RegistrationTransactionBuilder { ); } + Future _deriveProposerPublicKey() async { + final keyPair = await keyDerivation.deriveAccountRoleKeyPair( + masterKey: masterKey, + role: AccountRole.proposer, + ); + + return keyPair.publicKey.toPublicKey(); + } + ShelleyAddress get _stakeAddress => rewardAddresses.first; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/dummy_auth_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/dummy_auth_storage.dart deleted file mode 100644 index 74a5674591f..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/dummy_auth_storage.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:async'; - -import 'package:catalyst_voices_services/src/storage/secure_storage.dart'; - -abstract interface class DummyAuthStorage { - FutureOr readEmail(); - - FutureOr writeEmail(String? value); - - FutureOr readPassword(); - - FutureOr writePassword(String? value); - - Future clear(); -} - -final class SecureDummyAuthStorage extends SecureStorage - implements DummyAuthStorage { - static const _emailKey = 'email'; - static const _passwordKey = 'password'; - - const SecureDummyAuthStorage({ - super.secureStorage, - }); - - @override - FutureOr readEmail() => readString(key: _emailKey); - - @override - FutureOr writeEmail(String? value) { - return writeString(value, key: _emailKey); - } - - @override - FutureOr readPassword() => readString(key: _passwordKey); - - @override - FutureOr writePassword(String? value) { - return writeString(value, key: _passwordKey); - } - - @override - Future clear() async { - await writeEmail(null); - await writePassword(null); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_service.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_service.dart index 5ad6755c893..5dd57b078e1 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_service.dart @@ -1,249 +1,131 @@ import 'dart:async'; -import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; -import 'package:logging/logging.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -abstract interface class UserService { +abstract interface class UserService implements ActiveAware { factory UserService({ - required KeychainProvider keychainProvider, - required UserStorage userStorage, + required UserRepository userRepository, }) { return UserServiceImpl( - keychainProvider, - userStorage, + userRepository, ); } Account? get account; - Stream get watchAccount; - - Keychain? get keychain; + List get accounts; - Future> get keychains; + Stream get watchAccount; - Stream get watchKeychain; + Future getUser(); Future useLastAccount(); Future useAccount(Account account); - Future useKeychain(String id); - - Future removeCurrentKeychain(); - - Future removeKeychain(String id); + Future removeAccount(Account account); Future dispose(); } final class UserServiceImpl implements UserService { - final KeychainProvider _keychainProvider; - final UserStorage _userStorage; + final UserRepository _userRepository; final _logger = Logger('UserService'); - User? _user; - final _userSC = StreamController.broadcast(); + User _user = const User(accounts: []); + final _userSC = StreamController.broadcast(); - Keychain? _keychain; - final _keychainSC = StreamController.broadcast(); - StreamSubscription? _keychainUnlockSub; + bool _isActive = true; UserServiceImpl( - this._keychainProvider, - this._userStorage, + this._userRepository, ); @override - Account? get account => _user?.activeAccount; + Account? get account => _user.activeAccount; + + @override + List get accounts => List.unmodifiable(_user.accounts); @override Stream get watchAccount async* { yield account; - yield* _userSC.stream.map((user) => user?.activeAccount).distinct(); + yield* _userSC.stream.map((user) => user.activeAccount).distinct(); } @override - Keychain? get keychain => _keychain; + bool get isActive => _isActive; @override - Future> get keychains => _keychainProvider.getAll(); + set isActive(bool value) { + if (_isActive != value) { + _isActive = value; + _user.activeAccount?.keychain.isActive = value; + } + } @override - Stream get watchKeychain async* { - yield _keychain; - yield* _keychainSC.stream; - } + Future getUser() => _userRepository.getUser(); @override Future useLastAccount() async { - final keychainId = await _userStorage.getLastKeychainId(); - if (keychainId == null) { - await _clearUser(); - await _useKeychain(null); - return; - } - - final keychain = await _findKeychain(keychainId); - if (keychain == null) { - _logger.severe('Active keychain[$keychainId] was not found!'); - } + final user = await _userRepository.getUser(); - await _clearUser(); - await _useKeychain(keychain); + await _updateUser(user); } @override Future useAccount(Account account) async { - await useKeychain(account.keychainId); - _updateUser(User(accounts: [account])); - } - - @override - Future useKeychain(String id) async { - final keychain = await _findKeychain(id); - if (keychain == null) { - _logger.severe('Account keychain[$id] was not found!'); - } - await _useKeychain(keychain); - } - - @override - Future removeCurrentKeychain() async { - final keychain = _keychain; - if (keychain == null) { - _logger.warning('Called remove keychain but no active found'); - return; - } - - await removeKeychain(keychain.id); - } + var user = await getUser(); - @override - Future removeKeychain(String id) async { - if (!await _keychainProvider.exists(id)) { - _logger.warning( - 'Called remove keychain[$id] but no such keychain was found', - ); - return; + if (!user.hasAccount(id: account.id)) { + user = user.addAccount(account); } - final isCurrentKeychain = id == _keychain?.id; - - final keychain = await _keychainProvider.get(id); - await keychain.clear(); + user = user.useAccount(id: account.id); - if (isCurrentKeychain) { - await _clearUser(); - await _useKeychain(null); - } + await _updateUser(user); } @override - Future dispose() async { - await _keychainUnlockSub?.cancel(); - _keychainUnlockSub = null; - - _keychain = null; - await _keychainSC.close(); - } - - Future _useKeychain(Keychain? keychain) async { - await _userStorage.setUsedKeychainId(keychain?.id); + Future removeAccount(Account account) async { + var user = await getUser(); - await _keychainUnlockSub?.cancel(); - _keychainUnlockSub = null; - - _updateActiveKeychain(keychain); - - if (keychain != null) { - _keychainUnlockSub = - keychain.watchIsUnlocked.listen(_onKeychainUnlockChanged); + if (user.hasAccount(id: account.id)) { + user = user.removeAccount(id: account.id); } - } - Future _onKeychainUnlockChanged(bool isUnlocked) async { - final keychain = _keychain; - - _logger.finest('$keychain unlock changed[$isUnlocked]'); - - assert( - keychain != null, - 'Keychain unlock stage changed but keychain is null', - ); - - if (!isUnlocked) { - await _clearUser(); - return; + if (user.activeAccount == null) { + final firstAccount = user.accounts.firstOrNull; + if (firstAccount != null) { + user = user.useAccount(id: firstAccount.id); + } } - await _fetchUserDetails(keychain!); - } - - // TODO(damian-molinski): fetch user details from backend with root key. - Future _fetchUserDetails(Keychain keychain) async { - await Future.delayed(const Duration(milliseconds: 100)); + await _updateUser(user); - final user = _user?.account.keychainId == keychain.id - ? _user - : _dummyUser(keychainId: keychain.id); - - _updateUser(user); - } - - Future _clearUser() async { - _updateUser(null); + await account.keychain.erase(); } - Future _findKeychain(String id) async { - final exists = await _keychainProvider.exists(id); + Future _updateUser(User user) async { + if (_user != user) { + _logger.info('Changing user to [$user]'); - return exists ? await _keychainProvider.get(id) : null; - } + if (_user.activeAccount?.keychain.id != user.activeAccount?.keychain.id) { + _user.activeAccount?.keychain.isActive = false; + user.activeAccount?.keychain.isActive = _isActive; + } - void _updateActiveKeychain(Keychain? keychain) { - if (_keychain?.id != keychain?.id) { - _logger.finest('Keychain changed to $keychain'); - _keychain = keychain; - _keychainSC.add(keychain); - } - } + await _userRepository.saveUser(user); - void _updateUser(User? user) { - if (_user != user) { - _logger.finest('User changed to $user'); _user = user; _userSC.add(user); } } -} -/// Temporary implementation for testing purposes. -User _dummyUser({ - required String keychainId, -}) { - /* cSpell:disable */ - final account = Account( - keychainId: keychainId, - roles: { - AccountRole.root, - }, - walletInfo: WalletInfo( - metadata: const WalletMetadata( - name: 'Dummy Wallet', - icon: null, - ), - balance: Coin.fromAda(10), - address: ShelleyAddress.fromBech32( - 'addr_test1vzpwq95z3xyum8vqndgdd' - '9mdnmafh3djcxnc6jemlgdmswcve6tkw', - ), - ), - ); - - return User(accounts: [account]); - /* cSpell:enable */ + @override + Future dispose() async {} } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_storage.dart deleted file mode 100644 index 55bdae93afe..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/user/user_storage.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; - -const _activeKeychainIdKey = 'activeKeychainId'; - -abstract interface class UserStorage { - Future getLastKeychainId(); - - Future setUsedKeychainId(String? id); -} - -final class SecureUserStorage extends SecureStorage implements UserStorage { - SecureUserStorage({ - super.secureStorage, - }); - - @override - Future getLastKeychainId() { - return readString(key: _activeKeychainIdKey); - } - - @override - Future setUsedKeychainId(String? id) { - return writeString(id, key: _activeKeychainIdKey); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml index cd52897cee3..8744e2f0762 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_services/pubspec.yaml @@ -16,29 +16,24 @@ dependencies: path: ../catalyst_voices_models catalyst_voices_repositories: path: ../catalyst_voices_repositories - chopper: ^7.2.0 - convert: ^3.1.1 - cryptography: ^2.7.0 - ed25519_hd_key: ^2.3.0 - equatable: ^2.0.7 + catalyst_voices_shared: + path: ../catalyst_voices_shared + collection: ^1.18.0 flutter: sdk: flutter flutter_driver: sdk: flutter flutter_secure_storage: ^9.2.2 - json_annotation: ^4.8.1 logging: ^1.2.0 path: ^1.9.0 rxdart: ^0.27.7 + shared_preferences: ^2.3.3 uuid: ^4.5.1 web: ^1.1.0 webdriver: ^3.0.3 dev_dependencies: - build_runner: ^2.4.12 catalyst_analysis: ^2.0.0 - chopper_generator: ^7.2.0 - json_serializable: ^6.7.1 mocktail: ^1.0.1 - swagger_dart_code_generator: ^2.15.2 + shared_preferences_platform_interface: ^2.4.1 test: ^1.24.9 diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/user_service_test.dart b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/user_service_test.dart index c4aaecb3b41..1f65ef61abd 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/user_service_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_services/test/src/user/user_service_test.dart @@ -1,98 +1,137 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; import 'package:catalyst_voices_services/src/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; import 'package:uuid/uuid.dart'; void main() { - final KeychainProvider provider = VaultKeychainProvider(); - final UserStorage storage = SecureUserStorage(); + late final KeychainProvider keychainProvider; + late final UserRepository userRepository; late UserService service; - setUp(() { + setUpAll(() { + final store = InMemorySharedPreferencesAsync.empty(); + SharedPreferencesAsyncPlatform.instance = store; FlutterSecureStorage.setMockInitialValues({}); - service = UserService(keychainProvider: provider, userStorage: storage); + keychainProvider = VaultKeychainProvider( + secureStorage: const FlutterSecureStorage(), + sharedPreferences: SharedPreferencesAsync(), + cacheConfig: const CacheConfig(), + ); + userRepository = UserRepository(SecureUserStorage(), keychainProvider); + }); + + setUp(() { + service = UserService( + userRepository: userRepository, + ); }); - group('Keychain', () { - test('when using keychain getter returns that keychain', () async { + tearDown(() async { + await const FlutterSecureStorage().deleteAll(); + await SharedPreferencesAsync().clear(); + }); + + group(UserService, () { + test('when using account getter returns that account', () async { // Given final keychainId = const Uuid().v4(); // When - final keychain = await provider.create(keychainId); + final keychain = await keychainProvider.create(keychainId); + final account = Account.dummy(keychain: keychain); - await service.useKeychain(keychain.id); + await service.useAccount(account); // Then - final currentKeychain = service.keychain; + final currentAccount = service.account; - expect(currentKeychain?.id, keychain.id); + expect(currentAccount?.id, account.id); + expect(currentAccount?.isActive, isTrue); }); - test('using different keychain emits update in stream', () async { + test('using different account emits update in stream', () async { // Given final keychainIdOne = const Uuid().v4(); final keychainIdTwo = const Uuid().v4(); // When - final keychainOne = await provider.create(keychainIdOne); - final keychainTwo = await provider.create(keychainIdTwo); + final keychainOne = await keychainProvider.create(keychainIdOne); + final keychainTwo = await keychainProvider.create(keychainIdTwo); + final accountOne = Account.dummy(keychain: keychainOne); + final accountTwo = Account.dummy(keychain: keychainTwo); - final keychainStream = service.watchKeychain; + final accountStream = service.watchAccount; // Then expect( - keychainStream, + accountStream, emitsInOrder([ isNull, - predicate((e) => e.id == keychainOne.id), - predicate((e) => e.id == keychainTwo.id), + predicate((e) => e?.id == accountOne.id), + predicate((e) => e?.id == accountTwo.id), + predicate((e) => e?.id == accountOne.id), isNull, ]), ); - await service.useKeychain(keychainOne.id); - await service.useKeychain(keychainTwo.id); - await service.removeCurrentKeychain(); + await service.useAccount(accountOne); + await service.useAccount(accountTwo); + + await service.removeAccount(accountTwo); + await service.removeAccount(accountOne); await service.dispose(); }); - test('keychains getter returns all initialized local instances', () async { + test('accounts getter returns all keychains initialized local instances', + () async { // Given final ids = List.generate(5, (_) => const Uuid().v4()); // When - final keychains = []; + final accounts = []; for (final id in ids) { - final keychain = await provider.create(id); - keychains.add(keychain); + final keychain = await keychainProvider.create(id); + final account = Account.dummy(keychain: keychain); + + accounts.add(account); } + await userRepository.saveUser(User(accounts: accounts)); + // Then - final serviceKeychains = await service.keychains; + final user = await service.getUser(); - expect(serviceKeychains.map((e) => e.id), keychains.map((e) => e.id)); + expect(user.accounts.map((e) => e.id), accounts.map((e) => e.id)); }); - }); - group('Account', () { - test('use last account restores previously stored keychain', () async { + test('use last account restores previously stored', () async { // Given final keychainId = const Uuid().v4(); // When - final expectedKeychain = await provider.create(keychainId); + final keychain = await keychainProvider.create(keychainId); + final lastAccount = Account.dummy( + keychain: keychain, + isActive: true, + ); - await storage.setUsedKeychainId(expectedKeychain.id); + final user = User(accounts: [lastAccount]); + await userRepository.saveUser(user); await service.useLastAccount(); // Then - expect(service.keychain?.id, expectedKeychain.id); + expect(service.account, lastAccount); }); test('use last account does nothing on clear instance', () async { @@ -102,7 +141,6 @@ void main() { await service.useLastAccount(); // Then - expect(service.keychain, isNull); expect(service.account, isNull); }); @@ -111,20 +149,25 @@ void main() { final keychainId = const Uuid().v4(); // When - final currentKeychain = await provider.create(keychainId); + final keychain = await keychainProvider.create(keychainId); + final account = Account.dummy( + keychain: keychain, + isActive: true, + ); - await storage.setUsedKeychainId(currentKeychain.id); + final user = User(accounts: [account]); + await userRepository.saveUser(user); await service.useLastAccount(); // Then - expect(service.keychain, isNotNull); + expect(service.account, isNotNull); - await service.removeCurrentKeychain(); + await service.removeAccount(account); - expect(service.keychain, isNull); - expect(await currentKeychain.isEmpty, isTrue); - expect(await provider.exists(keychainId), isFalse); + expect(service.account, isNull); + expect(await keychain.isEmpty, isTrue); + expect(await keychainProvider.exists(keychainId), isFalse); }); }); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/cache.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/cache.dart new file mode 100644 index 00000000000..ad097268e45 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/cache.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +abstract interface class Cache { + FutureOr contains({required K key}); + + FutureOr get({required K key}); + + FutureOr set( + V value, { + required K key, + }); + + FutureOr delete({required K key}); + + FutureOr clear(); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/local_tll_cache.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/local_tll_cache.dart new file mode 100644 index 00000000000..1b904f6f9ca --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/local_tll_cache.dart @@ -0,0 +1,92 @@ +import 'dart:async'; + +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +base class LocalTllCache extends LocalStorage + implements TtlCache { + final Duration _defaultTtl; + + LocalTllCache({ + super.key = 'LocalTllCache', + Set? allowList, + required super.sharedPreferences, + Duration defaultTtl = const Duration(minutes: 1), + }) : _defaultTtl = defaultTtl, + super( + allowList: allowList != null ? _extendAllowList(allowList) : null, + ); + + @override + Future get({ + required String key, + }) async { + final isExpired = await _isExpired(key: key); + if (isExpired) { + await delete(key: key); + await delete(key: _buildExpireKey(key)); + return null; + } + + return readString(key: key); + } + + @override + Future set( + String value, { + required String key, + Duration? ttl, + }) async { + await writeString(value, key: key); + await extendExpiration(key: key, ttl: ttl); + } + + @override + Future isExpired({required String key}) => _isExpired(key: key); + + @override + Future extendExpiration({ + required String key, + Duration? ttl, + }) async { + if (!(await contains(key: key))) { + throw ArgumentError( + 'Can not extend expiration for key[$key] because value is not set', + ); + } + + final effectiveTtl = ttl ?? _defaultTtl; + final now = DateTimeExt.now(); + + final expireDate = now.add(effectiveTtl); + final expireDateTimestamp = expireDate.toIso8601String(); + final expireDateKey = _buildExpireKey(key); + + await writeString(expireDateTimestamp, key: expireDateKey); + } + + Future _isExpired({required String key}) async { + final expireDate = await _readExpireDate(key: key); + final now = DateTimeExt.now(); + + final other = expireDate ?? now; + + final after = now.isAfter(other); + final atSameMoment = now.isAtSameMomentAs(other); + + return after || atSameMoment; + } + + Future _readExpireDate({required String key}) async { + final expireKey = _buildExpireKey(key); + final expireTimestamp = await readString(key: expireKey); + return DateTime.tryParse(expireTimestamp ?? ''); + } +} + +String _buildExpireKey(String key) => '$key.expireDate'; + +Set _extendAllowList(Set allowList) { + return allowList + .expand((element) => [element, _buildExpireKey(element)]) + .toSet(); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/ttl_cache.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/ttl_cache.dart new file mode 100644 index 00000000000..f83689e6a02 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/cache/ttl_cache.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'package:catalyst_voices_shared/src/cache/cache.dart'; + +abstract interface class TtlCache implements Cache { + @override + FutureOr set( + V value, { + required K key, + Duration? ttl, + }); + + Future isExpired({required K key}); + + FutureOr extendExpiration({ + required K key, + Duration? ttl, + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart index a69ca464d61..0e369f46b23 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart @@ -1,15 +1,39 @@ +export 'cache/cache.dart'; +export 'cache/local_tll_cache.dart'; +export 'cache/ttl_cache.dart'; export 'common/build_config.dart'; export 'common/build_environment.dart'; +export 'crypto/crypto_service.dart'; +export 'crypto/key_derivation.dart'; +export 'crypto/local_crypto_service.dart'; export 'dependency/dependency_provider.dart'; +export 'document/document_manager.dart'; +export 'document/extension/document_list_sort_ext.dart'; +export 'document/extension/document_map_to_list_ext.dart'; +export 'document/identifiable.dart'; export 'formatter/cryptocurrency_formatter.dart'; export 'formatter/wallet_address_formatter.dart'; +export 'keychain/keychain.dart'; +export 'keychain/keychain_provider.dart'; +export 'keychain/keychain_transformers.dart'; +export 'keychain/vault_keychain.dart'; +export 'keychain/vault_keychain_provider.dart'; export 'logging/logging_service.dart'; export 'platform/catalyst_platform.dart'; export 'platform_aware_builder/platform_aware_builder.dart'; +export 'range/range.dart'; export 'responsive/responsive_builder.dart'; export 'responsive/responsive_child.dart'; export 'responsive/responsive_padding.dart'; +export 'storage/local_storage.dart'; +export 'storage/memory_storage.dart'; +export 'storage/secure_storage.dart'; +export 'storage/storage.dart'; +export 'storage/vault/secure_storage_vault.dart'; +export 'storage/vault/vault.dart'; +export 'utils/active_aware.dart'; export 'utils/date_time_ext.dart'; export 'utils/future_ext.dart'; export 'utils/iterable_ext.dart'; +export 'utils/lockable.dart'; export 'utils/typedefs.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/crypto/crypto_service.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/crypto_service.dart similarity index 100% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/crypto/crypto_service.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/crypto_service.dart diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/crypto/key_derivation.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/key_derivation.dart similarity index 100% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/crypto/key_derivation.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/key_derivation.dart diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/local_crypto_service.dart similarity index 92% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/local_crypto_service.dart index 0bc0e6505bb..c783526f667 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/crypto/vault_crypto_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/local_crypto_service.dart @@ -2,25 +2,23 @@ import 'dart:convert'; import 'dart:math'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; -import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; -final _logger = Logger('VaultCryptoService'); +final _logger = Logger('LocalCryptoService'); /// [CryptoService] implementation used by default in [Vault]. /// /// It uses Pbkdf2 for key derivation as well as /// AesGcm for encryption/decryption. /// -/// Only keys build by [VaultCryptoService.deriveKey] should be used -/// for crypto operations are we're adding [VaultCryptoService] specific +/// Only keys build by [LocalCryptoService.deriveKey] should be used +/// for crypto operations are we're adding [LocalCryptoService] specific /// metadata to them. /// /// Supports version for future changes. -final class VaultCryptoService implements CryptoService { +final class LocalCryptoService implements CryptoService { /// Salt length for key derivation. static const int _saltLength = 16; @@ -36,7 +34,7 @@ final class VaultCryptoService implements CryptoService { final Random _random; - VaultCryptoService({ + LocalCryptoService({ Random? random, }) : _random = random ?? Random.secure(); diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/dependency/dependency_provider.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/dependency/dependency_provider.dart index e18fbb2c52d..df364d30c0c 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/dependency/dependency_provider.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/dependency/dependency_provider.dart @@ -39,33 +39,49 @@ abstract class DependencyProvider { } @protected - void registerFactory(ValueGetter factoryFunc) { - _getIt.registerFactory(factoryFunc); + void registerFactory( + ValueGetter factoryFunc, { + String? instanceName, + }) { + _getIt.registerFactory( + factoryFunc, + instanceName: instanceName, + ); } @protected void registerLazySingleton( ValueGetter factoryFunc, { + String? instanceName, DisposingFunc? dispose, }) { _getIt.registerLazySingleton( factoryFunc, + instanceName: instanceName, dispose: dispose, ); } @protected - void registerSingleton(T instance) { - _getIt.registerSingleton(instance); + void registerSingleton( + T instance, { + String? instanceName, + }) { + _getIt.registerSingleton( + instance, + instanceName: instanceName, + ); } @protected void registerSingletonAsync( ValueGetter> factoryFunc, { + String? instanceName, Iterable? dependsOn, }) { _getIt.registerSingletonAsync( factoryFunc, + instanceName: instanceName, dependsOn: dependsOn, ); } @@ -73,10 +89,12 @@ abstract class DependencyProvider { @protected void registerSingletonWithDependencies( FactoryFunc factoryFunc, { + String? instanceName, required List dependsOn, }) { _getIt.registerSingletonWithDependencies( factoryFunc, + instanceName: instanceName, dependsOn: dependsOn, ); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart new file mode 100644 index 00000000000..b5582446d4a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager.dart @@ -0,0 +1,83 @@ +import 'dart:typed_data'; + +import 'package:catalyst_compression/catalyst_compression.dart'; +import 'package:catalyst_cose/catalyst_cose.dart'; +import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; +import 'package:cbor/cbor.dart'; +import 'package:equatable/equatable.dart'; + +part 'document_manager_impl.dart'; + +/// Parses the document from the bytes obtained from [BinaryDocument.toBytes]. +/// +/// Usually this would convert the [bytes] into a [String], +/// decode a [String] into a json and then parse the data class +/// from the json representation. +typedef DocumentParser = T Function(Uint8List bytes); + +/// Manages the [SignedDocument]s. +abstract interface class DocumentManager { + /// The default constructor for the [DocumentManager], + /// provides the default implementation of the interface. + const factory DocumentManager() = _DocumentManagerImpl; + + /// Parses the document from the [bytes] representation. + /// + /// The [parser] must be able to parse the document + /// from the bytes produced by [BinaryDocument.toBytes]. + /// + /// The implementation of this method must be able to understand the [bytes] + /// that are obtained from the [SignedDocument.toBytes] method. + Future> parseDocument( + Uint8List bytes, { + required DocumentParser parser, + }); + + /// Signs the [document] with a single [privateKey]. + /// + /// The [publicKey] will be added as metadata in the signed document + /// so that it's easier to identify who signed it. + Future> signDocument( + T document, { + required Uint8List publicKey, + required Uint8List privateKey, + }); +} + +/// Represents an abstract document that is protected +/// with cryptographic signature. +/// +/// The [document] payload can be UTF-8 encoded bytes, a binary data +/// or anything else that can be represented in binary format. +abstract interface class SignedDocument { + /// The default constructor for the [SignedDocument]. + const SignedDocument(); + + /// A getter that returns a parsed document. + T get document; + + /// Verifies if the [document] has been signed by a private key + /// that belongs to the given [publicKey]. + Future verifySignature(Uint8List publicKey); + + /// Converts the document into binary representation. + Uint8List toBytes(); +} + +/// Represents an abstract document that can be represented in binary format. +// ignore: one_member_abstracts +abstract interface class BinaryDocument { + /// Converts the document into a binary representation. + /// + /// See [DocumentParser]. + Uint8List toBytes(); + + /// Returns the document content type. + DocumentContentType get contentType; +} + +/// Defines the content type of the [BinaryDocument]. +enum DocumentContentType { + /// The document's content type is JSON. + json, +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart new file mode 100644 index 00000000000..38682442e32 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/document_manager_impl.dart @@ -0,0 +1,129 @@ +part of 'document_manager.dart'; + +const _brotliEncoding = StringValue(CoseValues.brotliContentEncoding); + +final class _DocumentManagerImpl implements DocumentManager { + const _DocumentManagerImpl(); + + @override + Future> parseDocument( + Uint8List bytes, { + required DocumentParser parser, + }) async { + final coseSign = CoseSign.fromCbor(cbor.decode(bytes)); + final payload = await _brotliDecompressPayload(coseSign); + final document = parser(payload); + return _CoseSignedDocument(coseSign, document); + } + + @override + Future> signDocument( + T document, { + required Uint8List publicKey, + required Uint8List privateKey, + }) async { + final compressedPayload = await _brotliCompressPayload(document.toBytes()); + + final coseSign = await CoseSign.sign( + protectedHeaders: CoseHeaders.protected( + contentEncoding: _brotliEncoding, + contentType: document.contentType.asCose, + ), + unprotectedHeaders: const CoseHeaders.unprotected(), + payload: compressedPayload, + signers: [_Bip32Ed25519XSigner(publicKey, privateKey)], + ); + + return _CoseSignedDocument(coseSign, document); + } + + Future _brotliCompressPayload(Uint8List payload) async { + final compressor = CatalystCompression.instance.brotli; + final compressed = await compressor.compress(payload); + return Uint8List.fromList(compressed); + } + + Future _brotliDecompressPayload(CoseSign coseSign) async { + if (coseSign.protectedHeaders.contentEncoding == _brotliEncoding) { + final compressor = CatalystCompression.instance.brotli; + final decompressed = await compressor.decompress(coseSign.payload); + return Uint8List.fromList(decompressed); + } else { + return coseSign.payload; + } + } +} + +final class _CoseSignedDocument + extends SignedDocument with EquatableMixin { + final CoseSign _coseSign; + + @override + final T document; + + const _CoseSignedDocument(this._coseSign, this.document); + + @override + Future verifySignature(Uint8List publicKey) async { + return _coseSign.verify( + verifier: _Bip32Ed25519XVerifier(publicKey), + ); + } + + @override + Uint8List toBytes() { + final bytes = cbor.encode(_coseSign.toCbor()); + return Uint8List.fromList(bytes); + } + + @override + List get props => [_coseSign, document]; +} + +extension _CoseDocumentContentType on DocumentContentType { + /// Maps the [DocumentContentType] into COSE representation. + StringOrInt get asCose { + switch (this) { + case DocumentContentType.json: + return const IntValue(CoseValues.jsonContentType); + } + } +} + +final class _Bip32Ed25519XSigner implements CatalystCoseSigner { + final Uint8List publicKey; + final Uint8List privateKey; + + const _Bip32Ed25519XSigner(this.publicKey, this.privateKey); + + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async => publicKey; + + @override + Future sign(Uint8List data) async { + final pk = Bip32Ed25519XPrivateKeyFactory.instance.fromBytes(privateKey); + final signature = await pk.sign(data); + return Uint8List.fromList(signature.bytes); + } +} + +final class _Bip32Ed25519XVerifier implements CatalystCoseVerifier { + final Uint8List publicKey; + + const _Bip32Ed25519XVerifier(this.publicKey); + + @override + Future get kid async => publicKey; + + @override + Future verify(Uint8List data, Uint8List signature) async { + final pk = Bip32Ed25519XPublicKeyFactory.instance.fromBytes(publicKey); + return pk.verify( + data, + signature: Bip32Ed25519XSignatureFactory.instance.fromBytes(signature), + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/extension/document_list_sort_ext.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/extension/document_list_sort_ext.dart new file mode 100644 index 00000000000..7ff95db6d34 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/extension/document_list_sort_ext.dart @@ -0,0 +1,13 @@ +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +extension DocumentListSortExt on List { + void sortByOrder(List order) { + final orderMap = {for (var i = 0; i < order.length; i++) order[i]: i}; + + sort((a, b) { + final aIndex = orderMap[a.id] ?? double.maxFinite.toInt(); + final bIndex = orderMap[b.id] ?? double.maxFinite.toInt(); + return aIndex.compareTo(bIndex); + }); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/extension/document_map_to_list_ext.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/extension/document_map_to_list_ext.dart new file mode 100644 index 00000000000..cab157eed80 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/extension/document_map_to_list_ext.dart @@ -0,0 +1,28 @@ +extension DocumentMapToListExt on Map { + List> convertMapToListWithIds() { + final list = >[]; + + for (final entry in entries) { + if (entry.key == r'$schema') continue; + final value = entry.value as Map; + value['id'] = entry.key; + list.add(value); + } + + return list; + } + + List> convertMapToListWithIdsAndValues() { + final list = >[]; + + for (final entry in entries) { + if (entry.key == r'$schema') continue; + final value = {}; + value['id'] = entry.key; + value['value'] = entry.value; + list.add(value); + } + + return list; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/identifiable.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/identifiable.dart new file mode 100644 index 00000000000..5d0eb779875 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/document/identifiable.dart @@ -0,0 +1,3 @@ +abstract class Identifiable { + String get id; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain.dart new file mode 100644 index 00000000000..4453d999ded --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain.dart @@ -0,0 +1,14 @@ +import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +abstract interface class Keychain implements Lockable, ActiveAware { + String get id; + + Future get isEmpty; + + Future getMasterKey(); + + Future setMasterKey(Bip32Ed25519XPrivateKey key); + + Future erase(); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_provider.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_provider.dart similarity index 72% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_provider.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_provider.dart index ac523a80260..2289eee2a5d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/keychain_provider.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_provider.dart @@ -1,4 +1,4 @@ -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; abstract interface class KeychainProvider { Future create(String id); diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_transformers.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_transformers.dart new file mode 100644 index 00000000000..5bbafcd9df2 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/keychain_transformers.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +final class KeychainToUnlockTransformer + extends StreamTransformerBase { + KeychainToUnlockTransformer(); + + @override + Stream bind(Stream stream) { + return Stream.eventTransformed( + stream, + _KeychainUnlockStreamSink.new, + ); + } +} + +final class AccountToKeychainUnlockTransformer + extends StreamTransformerBase { + AccountToKeychainUnlockTransformer(); + + @override + Stream bind(Stream stream) { + return Stream.eventTransformed( + stream, + _AccountToKeychainUnlockStreamSink.new, + ); + } +} + +final class _KeychainUnlockStreamSink implements EventSink { + final EventSink _outputSink; + StreamSubscription? _streamSub; + + _KeychainUnlockStreamSink(this._outputSink); + + @override + void add(Keychain? event) { + final stream = event?.watchIsUnlocked ?? Stream.value(false); + + unawaited(_streamSub?.cancel()); + _streamSub = stream.listen(_outputSink.add); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _outputSink.addError(error, stackTrace); + } + + @override + void close() { + unawaited(_streamSub?.cancel()); + _streamSub = null; + _outputSink.close(); + } +} + +final class _AccountToKeychainUnlockStreamSink implements EventSink { + final EventSink _outputSink; + StreamSubscription? _streamSub; + + _AccountToKeychainUnlockStreamSink(this._outputSink); + + @override + void add(Account? event) { + final stream = event?.keychain.watchIsUnlocked ?? Stream.value(false); + + unawaited(_streamSub?.cancel()); + _streamSub = stream.listen(_outputSink.add); + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + _outputSink.addError(error, stackTrace); + } + + @override + void close() { + unawaited(_streamSub?.cancel()); + _streamSub = null; + _outputSink.close(); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain.dart new file mode 100644 index 00000000000..8bab2a67207 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +const _rootKey = 'rootKey'; + +const _allKeys = [ + _rootKey, +]; + +final class VaultKeychain extends SecureStorageVault implements Keychain { + /// See [SecureStorageVault.isStorageKey]. + static bool isKeychainKey( + String value, { + String key = SecureStorageVault.defaultKey, + }) { + return SecureStorageVault.isStorageKey(value, key: key); + } + + /// See [SecureStorageVault.getStorageId]. + static String getStorageId( + String value, { + String key = SecureStorageVault.defaultKey, + }) { + return SecureStorageVault.getStorageId(value, key: key); + } + + VaultKeychain({ + required super.id, + super.key, + required super.secureStorage, + required super.sharedPreferences, + super.unlockTtl, + super.cryptoService, + }); + + @override + Future get isEmpty async { + for (final key in _allKeys) { + if (await contains(key: key)) { + return false; + } + } + + return true; + } + + @override + Future getMasterKey() async { + final encodedMasterKey = await readString(key: _rootKey); + return encodedMasterKey != null + ? Bip32Ed25519XPrivateKeyFactory.instance.fromHex(encodedMasterKey) + : null; + } + + @override + Future setMasterKey(Bip32Ed25519XPrivateKey data) async { + await writeString(data.toHex(), key: _rootKey); + } + + @override + Future erase() => clear(); + + @override + String toString() => 'VaultKeychain[$id]'; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain_provider.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain_provider.dart similarity index 67% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain_provider.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain_provider.dart index a26bd9f9aa8..46e0b2249d5 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/keychain/vault_keychain_provider.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/keychain/vault_keychain_provider.dart @@ -1,15 +1,22 @@ -import 'package:catalyst_voices_services/src/catalyst_voices_services.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:logging/logging.dart'; +import 'package:shared_preferences/shared_preferences.dart'; final _logger = Logger('VaultKeychainProvider'); final class VaultKeychainProvider implements KeychainProvider { final FlutterSecureStorage _secureStorage; + final SharedPreferencesAsync _sharedPreferences; + final CacheConfig _cacheConfig; VaultKeychainProvider({ - FlutterSecureStorage secureStorage = const FlutterSecureStorage(), - }) : _secureStorage = secureStorage; + required FlutterSecureStorage secureStorage, + required SharedPreferencesAsync sharedPreferences, + required CacheConfig cacheConfig, + }) : _secureStorage = secureStorage, + _sharedPreferences = sharedPreferences, + _cacheConfig = cacheConfig; @override Future create(String id) async { @@ -32,12 +39,7 @@ final class VaultKeychainProvider implements KeychainProvider { } @override - Future get(String id) async { - return VaultKeychain( - id: id, - secureStorage: _secureStorage, - ); - } + Future get(String id) async => _get(id); @override Future> getAll() async { @@ -47,23 +49,17 @@ final class VaultKeychainProvider implements KeychainProvider { .then((keys) => keys.where(VaultKeychain.isKeychainKey)) .then((keys) => keys.map(VaultKeychain.getStorageId).toSet()); - final keychains = keychainsIds - .map((id) => VaultKeychain(id: id, secureStorage: _secureStorage)) - .cast() - .toList(); + final keychains = keychainsIds.map(_get).cast().toList(); return keychains; } Future _buildKeychain(String id) async { - final Keychain keychain = VaultKeychain( - id: id, - secureStorage: _secureStorage, - ); + final keychain = _get(id); if (!await keychain.isEmpty) { _logger.warning('Overriding existing keychain[$id]'); - await keychain.clear(); + await keychain.erase(); return _buildKeychain(id); } @@ -85,4 +81,13 @@ final class VaultKeychainProvider implements KeychainProvider { await _secureStorage.delete(key: key); } } + + Keychain _get(String id) { + return VaultKeychain( + id: id, + secureStorage: _secureStorage, + sharedPreferences: _sharedPreferences, + unlockTtl: _cacheConfig.expiryDuration.keychainUnlock, + ); + } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/range/range.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/range/range.dart new file mode 100644 index 00000000000..ad17adab392 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/range/range.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +class Range extends Equatable { + final T min; + final T max; + + const Range({required this.min, required this.max}); + + static Range? optionalRangeOf({T? min, T? max}) { + if (min == null || max == null) { + return null; + } + return Range(min: min, max: max); + } + + @override + List get props => [min, max]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/local_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/local_storage.dart new file mode 100644 index 00000000000..3ebbc549462 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/local_storage.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_shared/src/storage/storage_string_mixin.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +base class LocalStorage with StorageAsStringMixin implements Storage { + final String key; + + /// See [SharedPreferencesAsync.clear]. + final Set? allowList; + final SharedPreferencesAsync _sharedPreferences; + + LocalStorage({ + this.key = 'LocalStorage', + this.allowList, + required SharedPreferencesAsync sharedPreferences, + }) : _sharedPreferences = sharedPreferences; + + @override + Future contains({required String key}) { + final effectiveKey = _effectiveKey(key); + + return _sharedPreferences.containsKey(effectiveKey); + } + + @override + Future readString({required String key}) { + final effectiveKey = _effectiveKey(key); + + return _sharedPreferences.getString(effectiveKey); + } + + @override + Future writeString( + String? value, { + required String key, + }) async { + final effectiveKey = _effectiveKey(key); + + if (value != null) { + await _sharedPreferences.setString(effectiveKey, value); + } else { + await _sharedPreferences.remove(effectiveKey); + } + } + + @override + Future clear() async { + final keysToRemove = allowList?.map(_effectiveKey).toSet() ?? + await () async { + final keys = await _sharedPreferences.getKeys(); + return keys.where((element) => element.startsWith(key)).toSet(); + }(); + + await _sharedPreferences.clear(allowList: keysToRemove); + } + + String _effectiveKey(String value) { + return '$key.$value'; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/memory_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/memory_storage.dart new file mode 100644 index 00000000000..44197357aa4 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/memory_storage.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_shared/src/storage/storage_string_mixin.dart'; + +base class MemoryStorage with StorageAsStringMixin implements Storage { + final _data = {}; + + MemoryStorage(); + + @override + Future contains({required String key}) async { + return _data.containsKey(key); + } + + @override + FutureOr readString({required String key}) { + return _data[key]; + } + + @override + FutureOr writeString( + String? value, { + required String key, + }) { + if (value != null) { + _data[key] = value; + } else { + _data.remove(key); + } + } + + @override + FutureOr clear() { + _data.clear(); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/secure_storage.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/secure_storage.dart similarity index 75% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/secure_storage.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/secure_storage.dart index 7f34cec02aa..3e78a4255da 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/secure_storage.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/secure_storage.dart @@ -1,15 +1,15 @@ import 'dart:async'; -import 'package:catalyst_voices_services/src/storage/storage.dart'; -import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; +import 'package:catalyst_voices_shared/src/storage/storage.dart'; +import 'package:catalyst_voices_shared/src/storage/storage_string_mixin.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -const _keyPrefix = 'SecureStorage'; - base class SecureStorage with StorageAsStringMixin implements Storage { + final String key; final FlutterSecureStorage _secureStorage; const SecureStorage({ + this.key = 'SecureStorage', FlutterSecureStorage secureStorage = const FlutterSecureStorage(), }) : _secureStorage = secureStorage; @@ -20,7 +20,7 @@ base class SecureStorage with StorageAsStringMixin implements Storage { @override Future readString({required String key}) { - final effectiveKey = _buildVaultKey(key); + final effectiveKey = _effectiveKey(key); return _secureStorage.read(key: effectiveKey); } @@ -30,7 +30,7 @@ base class SecureStorage with StorageAsStringMixin implements Storage { String? value, { required String key, }) async { - final effectiveKey = _buildVaultKey(key); + final effectiveKey = _effectiveKey(key); if (value != null) { await _secureStorage.write(key: effectiveKey, value: value); @@ -42,14 +42,14 @@ base class SecureStorage with StorageAsStringMixin implements Storage { @override FutureOr clear() async { final all = await _secureStorage.readAll(); - final vaultKeys = List.of(all.keys).where((e) => e.startsWith(_keyPrefix)); + final vaultKeys = List.of(all.keys).where((e) => e.startsWith(key)); for (final key in vaultKeys) { await _secureStorage.delete(key: key); } } - String _buildVaultKey(String key) { - return '$_keyPrefix.$key'; + String _effectiveKey(String value) { + return '$key.$value'; } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/storage.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/storage.dart similarity index 100% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/storage.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/storage.dart diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/storage_string_mixin.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/storage_string_mixin.dart similarity index 89% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/storage_string_mixin.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/storage_string_mixin.dart index ccb19f245d8..8db0b2f946b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/storage_string_mixin.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/storage_string_mixin.dart @@ -3,14 +3,11 @@ import 'dart:async'; import 'dart:convert'; -import 'package:catalyst_voices_services/src/storage/storage.dart'; -import 'package:catalyst_voices_services/src/storage/vault/secure_storage_vault.dart'; +import 'package:catalyst_voices_shared/src/storage/storage.dart'; import 'package:flutter/foundation.dart'; /// Utility mixin which implements all but String read/write of [Storage] /// interface. Every method is has its mapping to [readString]/[writeString]. -/// -/// See [SecureStorageVault] as example. mixin StorageAsStringMixin implements Storage { @override FutureOr readInt({required String key}) async { diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault.dart similarity index 52% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault.dart index 7bef14095d4..64f0622e903 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/vault/secure_storage_vault.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault.dart @@ -2,31 +2,39 @@ import 'dart:async'; import 'dart:convert'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/crypto/crypto_service.dart'; -import 'package:catalyst_voices_services/src/crypto/vault_crypto_service.dart'; -import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; -import 'package:catalyst_voices_services/src/storage/vault/vault.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_shared/src/storage/storage_string_mixin.dart'; +import 'package:catalyst_voices_shared/src/storage/vault/secure_storage_vault_cache.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; const _lockKey = 'LockKey'; +const _createDateKey = 'CreateDate'; /// Implementation of [Vault] that uses [FlutterSecureStorage] as /// facade for read/write operations. base class SecureStorageVault with StorageAsStringMixin implements Vault { @override final String id; - @protected - final FlutterSecureStorage secureStorage; + final String _key; + final FlutterSecureStorage _secureStorage; + final SecureStorageVaultCache _cache; final CryptoService _cryptoService; final _isUnlockedSC = StreamController.broadcast(); - bool __isUnlocked = false; + final _initializationCompleter = Completer(); + + bool _isUnlocked = false; + bool _isActive = false; /// Check if given [value] belongs to any [SecureStorageVault]. - static bool isStorageKey(String value) { + static bool isStorageKey( + String value, { + String key = defaultKey, + }) { try { - getStorageId(value); + getStorageId(value, key: key); return true; } catch (e) { return false; @@ -39,7 +47,10 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { /// /// See [isStorageKey] to make sure key is valid before /// calling [getStorageId]. - static String getStorageId(String value) { + static String getStorageId( + String value, { + String key = defaultKey, + }) { final parts = value.split('.'); if (parts.length != 3) { throw ArgumentError('Key sections count is invalid'); @@ -48,35 +59,38 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { final prefix = parts[0]; final id = parts[1]; - if (prefix != _keyPrefix) { + if (prefix != key) { throw ArgumentError('Key prefix does not match'); } return id; } - static const _keyPrefix = 'SecureStorageVault'; + static const defaultKey = 'SecureStorageVault'; SecureStorageVault({ required this.id, - this.secureStorage = const FlutterSecureStorage(), + String key = defaultKey, + required FlutterSecureStorage secureStorage, + required SharedPreferencesAsync sharedPreferences, + Duration unlockTtl = const Duration(hours: 1), CryptoService? cryptoService, - }) : _cryptoService = cryptoService ?? VaultCryptoService(); - - String get _instanceKeyPrefix => '$_keyPrefix.$id'; - - bool get _isUnlocked => __isUnlocked; - - set _isUnlocked(bool value) { - if (__isUnlocked != value) { - __isUnlocked = value; - _isUnlockedSC.add(value); - } + }) : _key = key, + _secureStorage = secureStorage, + _cache = SecureStorageVaultTtlCache( + key: '$key.$id.Cache', + sharedPreferences: sharedPreferences, + defaultTtl: unlockTtl, + ), + _cryptoService = cryptoService ?? LocalCryptoService() { + unawaited(_initialize()); } + String get _instanceKey => '$_key.$id'; + Future get _lock async { - final effectiveKey = buildKey(_lockKey); - final encodedLock = await secureStorage.read(key: effectiveKey); + final effectiveKey = _buildKey(_lockKey); + final encodedLock = await _secureStorage.read(key: effectiveKey); return encodedLock != null ? base64.decode(encodedLock) : null; } @@ -89,26 +103,44 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { } Future get _hasLock { - final effectiveKey = buildKey(_lockKey); - return secureStorage.containsKey(key: effectiveKey); + final effectiveKey = _buildKey(_lockKey); + return _secureStorage.containsKey(key: effectiveKey); } @override - Future get isUnlocked => Future(() => _isUnlocked); + bool get lastIsUnlocked => _isUnlocked; + + @override + Future get isUnlocked => _getIsUnlockedAndSync(); @override Stream get watchIsUnlocked async* { - yield _isUnlocked; + yield await _getIsUnlockedAndSync(); yield* _isUnlockedSC.stream; } @override - Future lock() async { - _isUnlocked = false; + bool get isActive => _isActive; + + @override + set isActive(bool value) { + if (_isActive != value) { + _isActive = value; + + // When vault becomes inactive we should extend ttl for last unlock state. + if (!value) { + unawaited(_extendIsUnlockedExpirationDate()); + } + } } + @override + Future lock() => _updateUnlocked(false); + @override Future unlock(LockFactor unlock) async { + await _initializationCompleter.future; + if (!await _hasLock) { throw const LockNotFoundException('Set lock before unlocking Vault'); } @@ -116,7 +148,8 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { final seed = unlock.seed; final lock = await _requireLock; - _isUnlocked = await _cryptoService.verifyKey(seed, key: lock); + final isVerified = await _cryptoService.verifyKey(seed, key: lock); + await _updateUnlocked(isVerified); _erase(lock); @@ -125,6 +158,8 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { @override Future setLock(LockFactor lock) async { + await _initializationCompleter.future; + if (await _hasLock && !await isUnlocked) { throw const VaultLockedException(); } @@ -133,18 +168,17 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { final key = await _cryptoService.deriveKey(seed); final encodedKey = base64.encode(key); - final effectiveLockKey = buildKey(_lockKey); + final effectiveLockKey = _buildKey(_lockKey); - await secureStorage.write(key: effectiveLockKey, value: encodedKey); + await _secureStorage.write(key: effectiveLockKey, value: encodedKey); } - @protected - String buildKey(String key) => '$_instanceKeyPrefix.$key'; - @override Future contains({required String key}) async { - final effectiveKey = buildKey(key); - return secureStorage.containsKey(key: effectiveKey); + await _initializationCompleter.future; + + final effectiveKey = _buildKey(key); + return _secureStorage.containsKey(key: effectiveKey); } @override @@ -160,13 +194,17 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { @override Future clear() async { - final all = await secureStorage.readAll(); + await _initializationCompleter.future; + + final all = await _secureStorage.readAll(); final vaultKeys = - List.of(all.keys).where((key) => key.startsWith(_instanceKeyPrefix)); + List.of(all.keys).where((key) => key.startsWith(_instanceKey)); for (final key in vaultKeys) { - await secureStorage.delete(key: key); + await _secureStorage.delete(key: key); } + + await _cache.clear(); } @override @@ -180,10 +218,11 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { Future _guardedRead({ required String key, }) async { + await _initializationCompleter.future; await _ensureUnlocked(); - final effectiveKey = buildKey(key); - final encryptedData = await secureStorage.read(key: effectiveKey); + final effectiveKey = _buildKey(key); + final encryptedData = await _secureStorage.read(key: effectiveKey); if (encryptedData == null) { return null; } @@ -204,12 +243,13 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { String? value, { required String key, }) async { + await _initializationCompleter.future; await _ensureUnlocked(); - final effectiveKey = buildKey(key); + final effectiveKey = _buildKey(key); if (value == null) { - await secureStorage.delete(key: effectiveKey); + await _secureStorage.delete(key: effectiveKey); return; } @@ -218,7 +258,7 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { _erase(lock); - await secureStorage.write(key: effectiveKey, value: encryptedData); + await _secureStorage.write(key: effectiveKey, value: encryptedData); } Future _ensureUnlocked() async { @@ -228,6 +268,50 @@ base class SecureStorageVault with StorageAsStringMixin implements Vault { } } + Future _getIsUnlockedAndSync() async { + final isUnlocked = await _getIsUnlocked(); + await _updateUnlocked(isUnlocked); + return isUnlocked; + } + + Future _getIsUnlocked() async { + if (isActive) { + await _extendIsUnlockedExpirationDate(); + } + + return _cache.getIsUnlocked(); + } + + Future _extendIsUnlockedExpirationDate() async { + if (await _cache.containsIsUnlocked()) { + await _cache.extendIsUnlocked(); + } + } + + Future _updateUnlocked(bool value) async { + if (_isUnlocked != value) { + _isUnlocked = value; + + await _cache.setIsUnlocked(value: value); + + _isUnlockedSC.add(value); + } + } + + Future _initialize() async { + final effectiveKey = _buildKey(_createDateKey); + final contains = await _secureStorage.containsKey(key: effectiveKey); + if (!contains) { + final now = DateTimeExt.now(); + final timestamp = now.toIso8601String(); + await _secureStorage.write(key: effectiveKey, value: timestamp); + } + + _initializationCompleter.complete(); + } + + String _buildKey(String key) => '$_instanceKey.$key'; + Future _encrypt( String data, { required Uint8List key, diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault_cache.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault_cache.dart new file mode 100644 index 00000000000..d938361e513 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/secure_storage_vault_cache.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; + +const _isUnlockedKey = 'IsUnlocked'; + +abstract interface class SecureStorageVaultCache { + Future getIsUnlocked(); + + Future setIsUnlocked({required bool value}); + + Future containsIsUnlocked(); + + Future isUnlockedExpired(); + + Future extendIsUnlocked(); + + Future clear(); +} + +final class SecureStorageVaultTtlCache extends LocalTllCache + implements SecureStorageVaultCache { + SecureStorageVaultTtlCache({ + super.key, + required super.sharedPreferences, + super.defaultTtl = const Duration(hours: 1), + }) : super( + allowList: {_isUnlockedKey}, + ); + + @override + Future getIsUnlocked() { + return get(key: _isUnlockedKey).then((value) => value == 'true'); + } + + @override + Future setIsUnlocked({required bool value}) { + return set('$value', key: _isUnlockedKey); + } + + @override + Future containsIsUnlocked() => contains(key: _isUnlockedKey); + + @override + Future isUnlockedExpired() => isExpired(key: _isUnlockedKey); + + @override + Future extendIsUnlocked() => extendExpiration(key: _isUnlockedKey); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/vault/vault.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/vault.dart similarity index 64% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/vault/vault.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/vault.dart index 67bbbb620d1..7e798dfb817 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/storage/vault/vault.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/storage/vault/vault.dart @@ -1,12 +1,12 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/lockable.dart'; -import 'package:catalyst_voices_services/src/storage/storage.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; /// Secure version of [Storage] where any read/write methods can take /// effect only when [isUnlocked] returns true. /// /// In order to unlock [Vault] sufficient [LockFactor] have to be /// set via [unlock] that can unlock [LockFactor] from [setLock]. -abstract interface class Vault implements Storage, Lockable { +abstract interface class Vault implements Storage, Lockable, ActiveAware { + /// Identifier of instance. String get id; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/active_aware.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/active_aware.dart new file mode 100644 index 00000000000..135bc1a696f --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/active_aware.dart @@ -0,0 +1,11 @@ +/// An interface for objects that can be active or inactive. +/// +/// Active objects are typically performing some kind of work or are ready to +/// do so. Inactive objects are not currently performing any work. +abstract interface class ActiveAware { + /// Whether this vault is currently being used. + bool get isActive; + + /// Updates usage status. + set isActive(bool value); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/lockable.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/lockable.dart similarity index 83% rename from catalyst_voices/packages/internal/catalyst_voices_services/lib/src/lockable.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/lockable.dart index 4f2b0fa4b08..28c12eb2e7f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/lib/src/lockable.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/lockable.dart @@ -1,6 +1,10 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; abstract interface class Lockable { + /// Returns last known state of unlock. Effectively synchronous getter for + /// [watchIsUnlocked]. + bool get lastIsUnlocked; + /// Returns true when have sufficient [LockFactor] from [unlock]. Future get isUnlocked; diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml index d3e6434f921..14da899f7d6 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml @@ -9,16 +9,31 @@ environment: dependencies: catalyst_cardano_serialization: ^0.4.0 + catalyst_compression: ^0.3.0 + catalyst_cose: ^0.3.0 + catalyst_key_derivation: ^0.1.0 + catalyst_voices_models: + path: ../catalyst_voices_models + cbor: ^6.2.0 collection: ^1.18.0 + convert: ^3.1.1 + cryptography: ^2.7.0 + equatable: ^2.0.7 flutter: sdk: flutter + flutter_secure_storage: ^9.2.2 get_it: ^7.6.7 intl: ^0.19.0 + json_annotation: ^4.9.0 logging: ^1.2.0 + shared_preferences: ^2.3.3 + uuid: ^4.5.1 web: ^1.1.0 dev_dependencies: catalyst_analysis: ^2.0.0 flutter_test: sdk: flutter + mocktail: ^1.0.1 + shared_preferences_platform_interface: ^2.4.1 test: ^1.24.9 diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/cache/local_tll_cache_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/cache/local_tll_cache_test.dart new file mode 100644 index 00000000000..8748c060d93 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/cache/local_tll_cache_test.dart @@ -0,0 +1,79 @@ +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; + +void main() { + late final SharedPreferencesAsync sharedPreferences; + + final now = DateTime(2024, 12, 10, 12, 34); + + setUpAll(() { + final store = InMemorySharedPreferencesAsync.empty(); + SharedPreferencesAsyncPlatform.instance = store; + sharedPreferences = SharedPreferencesAsync(); + }); + + setUp(() { + DateTimeExt.mockedDateTime = now; + }); + + tearDown(() async { + await sharedPreferences.clear(); + }); + + group(LocalTllCache, () { + test('when key is not expired value is returned', () async { + // Given + final cache = LocalTllCache(sharedPreferences: sharedPreferences); + const key = 'isUnlocked'; + const value = true; + + // When + await cache.set('$value', key: key); + + // Then + final cachedValue = await cache.get(key: key); + + expect(cachedValue, '$value'); + }); + + test('when key is expired null is returned', () async { + // Given + final cache = LocalTllCache(sharedPreferences: sharedPreferences); + const key = 'isUnlocked'; + const value = true; + const ttl = Duration(hours: 1); + + // When + await cache.set('$value', key: key, ttl: ttl); + + DateTimeExt.mockedDateTime = now.add(ttl); + + // Then + final cachedValue = await cache.get(key: key); + + expect(cachedValue, isNull); + }); + + test('extend expiration makes key valid for longer', () async { + // Given + final cache = LocalTllCache(sharedPreferences: sharedPreferences); + const key = 'isUnlocked'; + const value = true; + const ttl = Duration(hours: 1); + + // When + await cache.set('$value', key: key); + await cache.extendExpiration(key: key, ttl: ttl); + + DateTimeExt.mockedDateTime = now.add(ttl - const Duration(seconds: 1)); + + // Then + final cachedValue = await cache.get(key: key); + + expect(cachedValue, '$value'); + }); + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/crypto/key_derivation_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/crypto/key_derivation_test.dart similarity index 97% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/crypto/key_derivation_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/crypto/key_derivation_test.dart index 71e6d6a30e5..178d245e43a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/crypto/key_derivation_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/crypto/key_derivation_test.dart @@ -1,6 +1,6 @@ import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/crypto/key_derivation.dart'; +import 'package:catalyst_voices_shared/src/crypto/key_derivation.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/crypto/local_crypto_service_test.dart similarity index 96% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/crypto/local_crypto_service_test.dart index ff938a365ac..2313c68af40 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/crypto/vault_crypto_service_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/crypto/local_crypto_service_test.dart @@ -1,12 +1,12 @@ import 'dart:convert'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/crypto/vault_crypto_service.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter/foundation.dart'; import 'package:test/test.dart'; void main() { - final cryptoService = VaultCryptoService(); + final cryptoService = LocalCryptoService(); group('key derivation', () { test( diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart new file mode 100644 index 00000000000..40b1e7ff4c6 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/document/document_manager_test.dart @@ -0,0 +1,172 @@ +import 'dart:convert'; + +import 'package:catalyst_compression/catalyst_compression.dart'; +import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; +import 'package:catalyst_voices_shared/src/document/document_manager.dart'; +import 'package:convert/convert.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +final _privateKey = Uint8List.fromList(List.filled(32, 0)); +final _publicKey = Uint8List.fromList(List.filled(32, 1)); +final _signature = Uint8List.fromList(List.filled(32, 2)); + +void main() { + group(DocumentManager, () { + const documentManager = DocumentManager(); + + setUpAll(() { + CatalystCompressionPlatform.instance = _FakeCatalystCompressionPlatform(); + + Bip32Ed25519XPublicKeyFactory.instance = + _FakeBip32Ed25519XPublicKeyFactory(); + + Bip32Ed25519XPrivateKeyFactory.instance = + _FakeBip32Ed25519XPrivateKeyFactory(); + + Bip32Ed25519XSignatureFactory.instance = + _FakeBip32Ed25519XSignatureFactory(); + }); + + test( + 'signDocument creates a signed document ' + 'that can be converted from/to bytes', () async { + const document = _JsonDocument('title'); + + final signedDocument = await documentManager.signDocument( + document, + publicKey: _publicKey, + privateKey: _privateKey, + ); + + expect(signedDocument.document, equals(document)); + + final isVerified = await signedDocument.verifySignature(_publicKey); + expect(isVerified, isTrue); + + final signedDocumentBytes = signedDocument.toBytes(); + final parsedDocument = await documentManager.parseDocument( + signedDocumentBytes, + parser: _JsonDocument.fromBytes, + ); + + expect(parsedDocument, equals(signedDocument)); + }); + }); +} + +final class _JsonDocument extends Equatable implements BinaryDocument { + final String title; + + const _JsonDocument(this.title); + + factory _JsonDocument.fromBytes(Uint8List bytes) { + final string = utf8.decode(bytes); + final map = json.decode(string); + return _JsonDocument.fromJson(map as Map); + } + + factory _JsonDocument.fromJson(Map map) { + return _JsonDocument(map['title'] as String); + } + + Map toJson() { + return {'title': title}; + } + + @override + Uint8List toBytes() { + final jsonString = json.encode(toJson()); + return utf8.encode(jsonString); + } + + @override + DocumentContentType get contentType => DocumentContentType.json; + + @override + List get props => [title]; +} + +class _FakeCatalystCompressionPlatform extends CatalystCompressionPlatform { + @override + CatalystCompressor get brotli => const _FakeCompressor(); +} + +final class _FakeCompressor implements CatalystCompressor { + const _FakeCompressor(); + + @override + Future> compress(List bytes) async => bytes; + + @override + Future> decompress(List bytes) async => bytes; +} + +class _FakeBip32Ed25519XPublicKeyFactory extends Bip32Ed25519XPublicKeyFactory { + @override + Bip32Ed25519XPublicKey fromBytes(List bytes) { + return _FakeBip32Ed22519XPublicKey(bytes: bytes); + } +} + +class _FakeBip32Ed25519XPrivateKeyFactory + extends Bip32Ed25519XPrivateKeyFactory { + @override + Bip32Ed25519XPrivateKey fromBytes(List bytes) { + return _FakeBip32Ed22519XPrivateKey(bytes: bytes); + } +} + +class _FakeBip32Ed25519XSignatureFactory extends Bip32Ed25519XSignatureFactory { + @override + Bip32Ed25519XSignature fromBytes(List bytes) { + return _FakeBip32Ed22519XSignature(bytes: bytes); + } +} + +class _FakeBip32Ed22519XPublicKey extends Fake + implements Bip32Ed25519XPublicKey { + @override + final List bytes; + + _FakeBip32Ed22519XPublicKey({required this.bytes}); + + @override + Future verify( + List message, { + required Bip32Ed25519XSignature signature, + }) async { + return listEquals(signature.bytes, _signature); + } + + @override + String toHex() => hex.encode(bytes); +} + +class _FakeBip32Ed22519XPrivateKey extends Fake + implements Bip32Ed25519XPrivateKey { + @override + final List bytes; + + _FakeBip32Ed22519XPrivateKey({required this.bytes}); + + @override + Future sign(List message) async { + return _FakeBip32Ed22519XSignature(bytes: _signature); + } + + @override + String toHex() => hex.encode(bytes); +} + +class _FakeBip32Ed22519XSignature extends Fake + implements Bip32Ed25519XSignature { + @override + final List bytes; + + _FakeBip32Ed22519XSignature({required this.bytes}); + + @override + String toHex() => hex.encode(bytes); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/keychain_transformers_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/keychain_transformers_test.dart similarity index 66% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/keychain_transformers_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/keychain_transformers_test.dart index 25a8e76c361..fea4ad84d06 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/keychain_transformers_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/keychain_transformers_test.dart @@ -1,20 +1,34 @@ import 'dart:async'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:test/expect.dart'; import 'package:test/scaffolding.dart'; void main() { setUp(() { FlutterSecureStorage.setMockInitialValues({}); + + final store = InMemorySharedPreferencesAsync.empty(); + SharedPreferencesAsyncPlatform.instance = store; }); group(KeychainToUnlockTransformer, () { + VaultKeychain buildKeychain(String id) { + return VaultKeychain( + id: id, + secureStorage: const FlutterSecureStorage(), + sharedPreferences: SharedPreferencesAsync(), + ); + } + test('emits keychain unlock stage changes when keychain is set', () async { // Given - final keychain = VaultKeychain(id: 'id'); + final keychain = buildKeychain('id'); const lockFactor = PasswordLockFactor('Test1234'); final keychainSC = StreamController.broadcast(); diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_provider_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_provider_test.dart similarity index 79% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_provider_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_provider_test.dart index f8418f71e06..9ad74709018 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_provider_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_provider_test.dart @@ -1,17 +1,34 @@ import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/keychain/vault_keychain_provider.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:convert/convert.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:test/test.dart'; import 'package:uuid/uuid.dart'; void main() { - final provider = VaultKeychainProvider(); + late final VaultKeychainProvider provider; - setUp(() { + setUpAll(() { FlutterSecureStorage.setMockInitialValues({}); + + final store = InMemorySharedPreferencesAsync.empty(); + SharedPreferencesAsyncPlatform.instance = store; + + provider = VaultKeychainProvider( + secureStorage: const FlutterSecureStorage(), + sharedPreferences: SharedPreferencesAsync(), + cacheConfig: const CacheConfig(), + ); + }); + + tearDown(() async { + await const FlutterSecureStorage().deleteAll(); + await SharedPreferencesAsync().clear(); }); group(VaultKeychainProvider, () { diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_test.dart similarity index 60% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_test.dart index 6d4d4d542a9..3a46aa5d362 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/keychain/vault_keychain_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/keychain/vault_keychain_test.dart @@ -1,26 +1,52 @@ import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/catalyst_voices_services.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:convert/convert.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:test/test.dart'; import 'package:uuid/uuid.dart'; void main() { group(VaultKeychain, () { + late final FlutterSecureStorage secureStorage; + late final SharedPreferencesAsync sharedPreferences; + setUpAll(() { FlutterSecureStorage.setMockInitialValues({}); + + final store = InMemorySharedPreferencesAsync.empty(); + SharedPreferencesAsyncPlatform.instance = store; + + secureStorage = const FlutterSecureStorage(); + sharedPreferences = SharedPreferencesAsync(); + Bip32Ed25519XPrivateKeyFactory.instance = _FakeBip32Ed25519XPrivateKeyFactory(); }); + tearDown(() async { + await secureStorage.deleteAll(); + await sharedPreferences.clear(); + }); + + VaultKeychain buildKeychain(String id) { + return VaultKeychain( + id: id, + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + ); + } + test('is considered empty even with metadata in it', () async { // Given final id = const Uuid().v4(); // When - final vault = VaultKeychain(id: id); + final vault = buildKeychain(id); // Then expect(await vault.isEmpty, isTrue); @@ -35,7 +61,7 @@ void main() { ); // When - final vault = VaultKeychain(id: id); + final vault = buildKeychain(id); await vault.setLock(lock); await vault.unlock(lock); @@ -44,56 +70,17 @@ void main() { expect(await vault.isEmpty, isFalse); }); - test('metadata is updated after writing master key', () async { - // Given - final id = const Uuid().v4(); - const lock = PasswordLockFactor('Test1234'); - final key = Bip32Ed25519XPrivateKeyFactory.instance.fromHex( - '8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c', - ); - - // When - final vault = VaultKeychain(id: id); - await vault.setLock(lock); - await vault.unlock(lock); - - // Then - final metadataBefore = await vault.metadata; - await vault.setMasterKey(key); - final metadataAfter = await vault.metadata; - - expect( - metadataBefore.updatedAt.isBefore(metadataAfter.updatedAt), - isTrue, - ); - }); - test('are not equal when id is matching', () async { // Given final id = const Uuid().v4(); // When - final vaultOne = VaultKeychain(id: id); - final vaultTwo = VaultKeychain(id: id); + final vaultOne = buildKeychain(id); + final vaultTwo = buildKeychain(id); // Then expect(vaultOne, isNot(equals(vaultTwo))); }); - - test('metadata dates are in UTC', () async { - // Given - final id = const Uuid().v4(); - - // When - final vault = VaultKeychain(id: id); - - // Then - - final metadata = await vault.metadata; - - expect(metadata.createdAt.isUtc, isTrue); - expect(metadata.updatedAt.isUtc, isTrue); - }); }); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/secure_storage_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/secure_storage_test.dart similarity index 96% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/secure_storage_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/secure_storage_test.dart index 72c8921b2de..3ac5ef86d77 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/secure_storage_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/secure_storage_test.dart @@ -1,4 +1,4 @@ -import 'package:catalyst_voices_services/src/storage/secure_storage.dart'; +import 'package:catalyst_voices_shared/src/storage/secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:test/test.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/storage_string_mixin_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/storage_string_mixin_test.dart similarity index 95% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/storage_string_mixin_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/storage_string_mixin_test.dart index cc494db9727..6330526d9e0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/storage_string_mixin_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/storage_string_mixin_test.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:catalyst_voices_services/src/storage/storage.dart'; -import 'package:catalyst_voices_services/src/storage/storage_string_mixin.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_shared/src/storage/storage_string_mixin.dart'; import 'package:test/test.dart'; void main() { diff --git a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/vault/secure_storage_vault_test.dart similarity index 87% rename from catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart rename to catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/vault/secure_storage_vault_test.dart index ef640bfc070..bb93ad1a9ca 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_services/test/src/storage/vault/secure_storage_vault_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/test/src/storage/vault/secure_storage_vault_test.dart @@ -1,26 +1,36 @@ import 'dart:convert'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_services/src/storage/vault/secure_storage_vault.dart'; +import 'package:catalyst_voices_shared/src/storage/vault/secure_storage_vault.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:test/test.dart'; void main() { late final FlutterSecureStorage flutterSecureStorage; + late final SharedPreferencesAsync sharedPreferences; late final SecureStorageVault vault; setUpAll(() { FlutterSecureStorage.setMockInitialValues({}); + final store = InMemorySharedPreferencesAsync.empty(); + SharedPreferencesAsyncPlatform.instance = store; + flutterSecureStorage = const FlutterSecureStorage(); + sharedPreferences = SharedPreferencesAsync(); vault = SecureStorageVault( id: 'id', secureStorage: flutterSecureStorage, + sharedPreferences: sharedPreferences, ); }); tearDown(() async { await flutterSecureStorage.deleteAll(); + await sharedPreferences.clear(); }); test('lock and unlock factor fallbacks to lock state', () async { @@ -120,14 +130,11 @@ void main() { await vault.clear(); - final futures = - vaultKeyValues.keys.map((e) => vault.readString(key: e)).toList(); - - final values = await Future.wait(futures); + final isUnlocked = await vault.isUnlocked; final fValues = await flutterSecureStorage.readAll(); // Then - expect(values, everyElement(isNull)); + expect(isUnlocked, isFalse); expect(fValues, nonVaultKeyValues); }); diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/access_control.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/access_control.dart new file mode 100644 index 00000000000..eed09cc903d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/access_control.dart @@ -0,0 +1,93 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +final class AccessControl { + const AccessControl(); + static const defaultSpacesAccess = [Space.discovery]; + + static const List _votingAccess = [ + Space.discovery, + Space.voting, + Space.fundedProjects, + ]; + + static const List _proposalAccess = [ + Space.discovery, + Space.workspace, + Space.voting, + Space.fundedProjects, + ]; + + static const List _adminAccess = [ + Space.discovery, + Space.workspace, + Space.treasury, + Space.voting, + Space.fundedProjects, + ]; + + static final Map allSpacesShortcutsActivators = { + Space.discovery: + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit1), + Space.workspace: + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit2), + Space.voting: + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit3), + Space.fundedProjects: + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.digit4), + Space.treasury: + LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyT), + }; + + List spacesAccess(Account? account) { + if (account == null) return defaultSpacesAccess; + if (account.isAdmin) return Space.values; + if (_hasProposerOrDrepRole(account)) { + // TODO(ryszard-schossler): After F14 use _proposalAccess + return [Space.discovery, Space.workspace]; + } + + // TODO(ryszard-schossler): After F14 use _votingAccess + return defaultSpacesAccess; + } + + List overallSpaces(Account? account) { + if (account == null) return _votingAccess; + if (account.isAdmin) return _adminAccess; + if (_hasProposerOrDrepRole(account)) return _proposalAccess; + return _votingAccess; + } + + Map spacesShortcutsActivators( + Account? account, + ) { + if (account == null) { + return allSpacesShortcutsActivators.useKeys([Space.discovery]); + } + if (account.isAdmin) return allSpacesShortcutsActivators; + if (_hasProposerOrDrepRole(account)) { + return allSpacesShortcutsActivators.useKeys([ + Space.discovery, + Space.workspace, + // TODO(ryszard-schossler): After F14 add + // Space.voting and Space.fundedProjects + // OR use values from _proposalAccess + ]); + } + return allSpacesShortcutsActivators.useKeys([Space.discovery]); + } + + static bool _hasProposerOrDrepRole(Account account) { + return account.roles + .any((role) => [AccountRole.proposer, AccountRole.drep].contains(role)); + } +} + +extension MapFilterExtension on Map { + Map useKeys(List keys) { + return Map.fromEntries( + entries.where((entry) => keys.contains(entry.key)), + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/authentication.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/authentication.dart index 6ac95a1ea33..d47c1bbfad9 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/authentication.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/authentication.dart @@ -1,3 +1,4 @@ +export 'access_control.dart'; export 'email.dart'; export 'exception/localized_unlock_password_exception.dart'; export 'password.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_category_section.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_category_section.dart new file mode 100644 index 00000000000..7baf375a4e6 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_category_section.dart @@ -0,0 +1,41 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/src/menu/menu_item.dart'; +import 'package:equatable/equatable.dart'; + +final class CampaignCategorySection extends Equatable implements MenuItem { + @override + final String id; + final CampaignCategory category; + final String title; + final String body; + @override + final bool isEnabled; + + const CampaignCategorySection({ + required this.id, + required this.category, + required this.title, + required this.body, + this.isEnabled = true, + }); + + CampaignCategorySection.fromCategory(CampaignSection section) + : this( + id: section.id, + category: section.category, + title: section.title, + body: section.body, + ); + + @override + String get label => category.name; + + @override + List get props => [ + id, + category, + title, + body, + isEnabled, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_info.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_info.dart new file mode 100644 index 00000000000..8179170f37d --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_info.dart @@ -0,0 +1,67 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/src/campaign/campaign_stage.dart'; +import 'package:equatable/equatable.dart'; + +final class CampaignInfo extends Equatable { + final String id; + final CampaignStage stage; + final DateTime startDate; + final DateTime endDate; + final String description; + + const CampaignInfo({ + required this.id, + required this.stage, + required this.startDate, + required this.endDate, + required this.description, + }); + + /// Calculates the [campaign] info state at given [date]. + factory CampaignInfo.fromCampaign(Campaign campaign, DateTime date) { + final stage = CampaignStage.fromCampaign(campaign, date); + return CampaignInfo( + id: campaign.id, + stage: stage, + startDate: campaign.startDate, + endDate: campaign.endDate, + description: campaign.description, + ); + } + + /// Creates a mocked campaign info from [campaign] at given [campaignStage]. + factory CampaignInfo.mockStageFromCampaign( + Campaign campaign, + CampaignStage campaignStage, + ) { + return CampaignInfo( + id: campaign.id, + stage: campaignStage, + startDate: _mockCampaignStartDate(campaignStage), + endDate: _mockCampaignEndDate(campaignStage), + description: campaign.description, + ); + } + + static DateTime _mockCampaignStartDate(CampaignStage stage) { + return switch (stage) { + CampaignStage.draft => DateTimeExt.now().plusDays(3), + CampaignStage.scheduled => DateTimeExt.now().plusDays(3), + CampaignStage.live => DateTimeExt.now().minusDays(4), + CampaignStage.completed => DateTimeExt.now().minusDays(7), + }; + } + + static DateTime _mockCampaignEndDate(CampaignStage stage) { + return switch (stage) { + CampaignStage.draft => DateTimeExt.now().plusDays(5), + CampaignStage.scheduled => DateTimeExt.now().plusDays(5), + CampaignStage.live => DateTimeExt.now().minusDays(2), + CampaignStage.completed => DateTimeExt.now().minusDays(5), + }; + } + + @override + List get props => [id, stage, startDate, endDate, description]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_list_item.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_list_item.dart new file mode 100644 index 00000000000..3954b6527b1 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_list_item.dart @@ -0,0 +1,42 @@ +import 'package:catalyst_voices_view_models/src/campaign/campaign_category_section.dart'; +import 'package:equatable/equatable.dart'; + +sealed class CampaignListItem extends Equatable {} + +final class CampaignDetailsListItem extends CampaignListItem { + final String description; + final DateTime startDate; + final DateTime endDate; + final int proposalsCount; + final int categoriesCount; + + CampaignDetailsListItem({ + required this.description, + required this.startDate, + required this.endDate, + required this.proposalsCount, + required this.categoriesCount, + }); + + @override + List get props => [ + description, + startDate, + endDate, + proposalsCount, + categoriesCount, + ]; +} + +final class CampaignCategoriesListItem extends CampaignListItem { + final List sections; + + CampaignCategoriesListItem({ + required this.sections, + }); + + @override + List get props => [ + sections, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_stage.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_stage.dart new file mode 100644 index 00000000000..5ea86493544 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_stage.dart @@ -0,0 +1,45 @@ +import 'package:catalyst_voices_localization/generated/catalyst_voices_localizations.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; + +/// Enum representing the stage of a campaign. +/// +/// Stages: +/// - [draft]: Campaign has not started. +/// Required when publish state is [CampaignPublish.draft] +/// - [scheduled]: Campaign has start/end dates set but hasn't started yet +/// - [live]: Campaign is currently active (between start and end dates) +/// - [completed]: Campaign has ended (current date is after end date) +/// +/// Note: All stages except [draft] require [CampaignPublish.published] state +enum CampaignStage { + draft, + scheduled, + live, + completed; + + /// Calculates the [campaign] stage at given [date]. + factory CampaignStage.fromCampaign(Campaign campaign, DateTime date) { + switch (campaign.publish) { + case CampaignPublish.draft: + return CampaignStage.draft; + case CampaignPublish.published: + if (date.isBefore(campaign.startDate)) { + return CampaignStage.scheduled; + } else if (date.isAfter(campaign.endDate)) { + return CampaignStage.completed; + } else { + return CampaignStage.live; + } + } + } + + bool get isCompleted => this == CampaignStage.completed; + bool get isDraft => this == CampaignStage.draft; + + String localizedName(VoicesLocalizations l10n) => switch (this) { + CampaignStage.draft => l10n.campaignStartingSoon, + CampaignStage.scheduled => l10n.campaignStartingSoon, + CampaignStage.live => l10n.campaignIsLive, + CampaignStage.completed => l10n.campaignConcluded, + }; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart index f745b4c9141..59bb9c47a30 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/catalyst_voices_view_models.dart @@ -1,12 +1,20 @@ export 'authentication/authentication.dart'; +export 'campaign/campaign_category_section.dart'; +export 'campaign/campaign_info.dart'; +export 'campaign/campaign_list_item.dart'; +export 'campaign/campaign_stage.dart'; export 'exception/localized_exception.dart'; export 'exception/localized_unknown_exception.dart'; +export 'menu/menu_item.dart'; +export 'menu/popup_menu_item.dart'; export 'navigation/sections_list_view_item.dart'; export 'navigation/sections_navigation.dart'; export 'proposal/comment.dart'; -export 'proposal/guidance/guidance.dart'; -export 'proposal/guidance/guidance_type.dart'; +export 'proposal/proposal_view_model.dart'; export 'registration/exception/localized_registration_exception.dart'; export 'registration/registration.dart'; +export 'session/session_status.dart'; export 'treasury/treasury_sections.dart'; +export 'workspace/workspace_proposal_list_item.dart'; export 'workspace/workspace_sections.dart'; +export 'workspace/workspace_tab_type.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/menu/menu_item.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/menu/menu_item.dart new file mode 100644 index 00000000000..1481dba245e --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/menu/menu_item.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; + +abstract interface class MenuItem { + String get id; + + String get label; + + bool get isEnabled; +} + +base class BasicMenuItem extends Equatable implements MenuItem { + @override + final String id; + @override + final String label; + @override + final bool isEnabled; + + const BasicMenuItem({ + required this.id, + required this.label, + this.isEnabled = true, + }); + + @override + List get props => [ + id, + label, + isEnabled, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/menu/popup_menu_item.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/menu/popup_menu_item.dart new file mode 100644 index 00000000000..6624c1bc1f1 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/menu/popup_menu_item.dart @@ -0,0 +1,32 @@ +import 'package:catalyst_voices_view_models/src/menu/menu_item.dart'; +import 'package:flutter/widgets.dart'; + +abstract interface class PopupMenuItem implements MenuItem { + Widget? get icon; + + bool get showDivider; +} + +base class BasicPopupMenuItem extends BasicMenuItem implements PopupMenuItem { + @override + final Widget? icon; + + @override + final bool showDivider; + + const BasicPopupMenuItem({ + required super.id, + required super.label, + super.isEnabled, + this.icon, + this.showDivider = false, + }); + + @override + List get props => + super.props + + [ + icon, + showDivider, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/navigation/sections_navigation.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/navigation/sections_navigation.dart index 45a82fb464f..0eba0c4bc75 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/navigation/sections_navigation.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/navigation/sections_navigation.dart @@ -3,10 +3,10 @@ import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; -typedef SectionStepId = ({int sectionId, int stepId}); +typedef SectionStepId = ({String sectionId, String stepId}); abstract interface class Section implements SectionsListViewItem { - int get id; + String get id; SvgGenImage get icon; @@ -16,9 +16,9 @@ abstract interface class Section implements SectionsListViewItem { } abstract interface class SectionStep implements SectionsListViewItem { - int get id; + String get id; - int get sectionId; + String get sectionId; SectionStepId get sectionStepId; @@ -26,15 +26,13 @@ abstract interface class SectionStep implements SectionsListViewItem { bool get isEditable; - List get guidances; - String localizedName(BuildContext context); } abstract base class BaseSection extends Equatable implements Section { @override - final int id; + final String id; @override final List steps; @@ -58,22 +56,19 @@ abstract base class BaseSection extends Equatable abstract base class BaseSectionStep extends Equatable implements SectionStep { @override - final int id; + final String id; @override - final int sectionId; + final String sectionId; @override final bool isEnabled; @override final bool isEditable; - @override - final List guidances; const BaseSectionStep({ required this.id, required this.sectionId, this.isEnabled = true, this.isEditable = true, - this.guidances = const [], }); @override diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance_type.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance_type.dart deleted file mode 100644 index 7b6ccc6990e..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/guidance/guidance_type.dart +++ /dev/null @@ -1,11 +0,0 @@ -enum GuidanceType { mandatory, education, tips } - -extension GuidanceTypeExt on GuidanceType { - int get priority { - return switch (this) { - GuidanceType.mandatory => 0, // Highest priority - GuidanceType.education => 1, - GuidanceType.tips => 2, // Lowest priority - }; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/proposal_view_model.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/proposal_view_model.dart new file mode 100644 index 00000000000..c60e353158c --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/proposal/proposal_view_model.dart @@ -0,0 +1,151 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/src/campaign/campaign_stage.dart'; +import 'package:equatable/equatable.dart'; + +/// A proposal view model spanning proposals in different stages. +abstract base class ProposalViewModel extends Equatable { + final String id; + + const ProposalViewModel({required this.id}); + + factory ProposalViewModel.fromProposalAtStage({ + required Proposal proposal, + required String campaignName, + required CampaignStage campaignStage, + }) { + switch (campaignStage) { + case CampaignStage.draft: + case CampaignStage.scheduled: + case CampaignStage.live: + return PendingProposal.fromProposal( + proposal, + campaignName: campaignName, + ); + case CampaignStage.completed: + // TODO(dtscalac): whether proposal is funded or not should depend + // not on campaign stage but on the proposal properties. + // In the future when proposals are refined update this. + return FundedProposal.fromProposal( + proposal, + campaignName: campaignName, + ); + } + } + + @override + List get props => [id]; +} + +/// Defines the pending proposal that is not funded yet. +final class PendingProposal extends ProposalViewModel { + final String campaignName; + final String category; + final String title; + final DateTime lastUpdateDate; + final Coin _fundsRequested; + final int commentsCount; + final String description; + final int completedSegments; + final int totalSegments; + + const PendingProposal({ + required super.id, + required this.campaignName, + required this.category, + required this.title, + required this.lastUpdateDate, + required Coin fundsRequested, + required this.commentsCount, + required this.description, + required this.completedSegments, + required this.totalSegments, + }) : _fundsRequested = fundsRequested; + + PendingProposal.fromProposal( + Proposal proposal, { + required String campaignName, + }) : this( + id: proposal.id, + campaignName: campaignName, + category: proposal.category, + title: proposal.title, + lastUpdateDate: proposal.updateDate, + fundsRequested: proposal.fundsRequested, + commentsCount: proposal.commentsCount, + description: proposal.description, + completedSegments: proposal.completedSegments, + totalSegments: proposal.totalSegments, + ); + + String get fundsRequested { + return CryptocurrencyFormatter.formatAmount(_fundsRequested); + } + + @override + List get props => [ + ...super.props, + campaignName, + category, + title, + lastUpdateDate, + _fundsRequested.value, + commentsCount, + description, + completedSegments, + totalSegments, + ]; +} + +/// Defines the already funded proposal. +final class FundedProposal extends ProposalViewModel { + final String campaignName; + final String category; + final String title; + final DateTime fundedDate; + final Coin _fundsRequested; + final int commentsCount; + final String description; + + const FundedProposal({ + required super.id, + required this.campaignName, + required this.category, + required this.title, + required this.fundedDate, + required Coin fundsRequested, + required this.commentsCount, + required this.description, + }) : _fundsRequested = fundsRequested; + + FundedProposal.fromProposal( + Proposal proposal, { + required String campaignName, + }) : this( + id: proposal.id, + campaignName: campaignName, + category: proposal.category, + title: proposal.title, + fundedDate: proposal.fundedDate!, + fundsRequested: proposal.fundsRequested, + commentsCount: proposal.commentsCount, + description: proposal.description, + ); + + String get fundsRequested { + return CryptocurrencyFormatter.formatAmount(_fundsRequested); + } + + @override + List get props => [ + ...super.props, + campaignName, + category, + title, + fundedDate, + _fundsRequested, + commentsCount, + description, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_status.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_status.dart new file mode 100644 index 00000000000..041ba43c953 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/session/session_status.dart @@ -0,0 +1,20 @@ +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; + +enum SessionStatus { + /// A user has a keychain and it is unlocked. + actor, + + /// A user has a keychain but it is locked. + guest, + + /// A user doesn't have a keychain. + visitor; + + String name(VoicesLocalizations l10n) { + return switch (this) { + SessionStatus.actor => l10n.actor, + SessionStatus.guest => l10n.guest, + SessionStatus.visitor => l10n.visitor, + }; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/treasury/campaign_setup.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/treasury/campaign_setup.dart deleted file mode 100644 index c7f5eb56331..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/treasury/campaign_setup.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'treasury_sections.dart'; - -final class CampaignSetup extends TreasurySection { - const CampaignSetup({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Setup Campaign'; - } -} - -final class DummyTopicStep extends TreasurySectionStep { - const DummyTopicStep({ - required super.id, - required super.sectionId, - super.isEditable, - }); - - @override - String localizedName(BuildContext context) { - return 'Topic $id'; - } - - String localizedDesc(BuildContext context) { - return localizedName(context); - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/treasury/treasury_sections.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/treasury/treasury_sections.dart index 441471b193a..1eade439520 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/treasury/treasury_sections.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/treasury/treasury_sections.dart @@ -1,14 +1,17 @@ +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/widgets.dart'; -part 'campaign_setup.dart'; - -sealed class TreasurySection - extends BaseSection { +final class TreasurySection extends BaseSection { const TreasurySection({ required super.id, required super.steps, }); + + @override + String localizedName(BuildContext context) { + return context.l10n.treasuryCreateCampaign; + } } sealed class TreasurySectionStep extends BaseSectionStep { @@ -18,4 +21,59 @@ sealed class TreasurySectionStep extends BaseSectionStep { super.isEnabled, super.isEditable, }); + + String localizedDesc(BuildContext context) => localizedName(context); +} + +final class SetupCampaignDetailsStep extends TreasurySectionStep { + const SetupCampaignDetailsStep({ + required super.id, + required super.sectionId, + }); + + @override + String localizedName(BuildContext context) { + return context.l10n.setupCampaignDetails; + } +} + +final class SetupCampaignStagesStep extends TreasurySectionStep { + const SetupCampaignStagesStep({ + required super.id, + required super.sectionId, + }); + + @override + String localizedName(BuildContext context) { + return context.l10n.setupCampaignStages; + } +} + +final class SetupProposalTemplateStep extends TreasurySectionStep { + const SetupProposalTemplateStep({ + required super.id, + required super.sectionId, + }); + + @override + String localizedName(BuildContext context) { + return context.l10n.setupBaseProposalTemplate; + } + + @override + String localizedDesc(BuildContext context) { + return context.l10n.setupBaseQuestions; + } +} + +final class SetupCampaignCategoriesStep extends TreasurySectionStep { + const SetupCampaignCategoriesStep({ + required super.id, + required super.sectionId, + }); + + @override + String localizedName(BuildContext context) { + return context.l10n.setupCategories; + } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/capability_and_feasibility.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/capability_and_feasibility.dart deleted file mode 100644 index c24b42a864c..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/capability_and_feasibility.dart +++ /dev/null @@ -1,51 +0,0 @@ -part of 'workspace_sections.dart'; - -final class CompatibilityAndFeasibility extends WorkspaceSection { - const CompatibilityAndFeasibility({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Compatibility & Feasibility'; - } -} - -final class DeliveryAndAccountabilityStep extends RichTextStep { - const DeliveryAndAccountabilityStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - }); - - @override - String localizedName(BuildContext context) { - return 'Delivery & Accountability'; - } - - @override - String localizedDesc(BuildContext context) { - return 'How do you proof trust and accountability for your project?'; - } -} - -final class FeasibilityChecksStep extends RichTextStep { - const FeasibilityChecksStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - }); - - @override - String localizedName(BuildContext context) { - return 'Feasibility checks'; - } - - @override - String localizedDesc(BuildContext context) { - return 'How will you check if your approach will work?'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_impact.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_impact.dart deleted file mode 100644 index 92c55ac5280..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_impact.dart +++ /dev/null @@ -1,41 +0,0 @@ -part of 'workspace_sections.dart'; - -final class ProposalImpact extends WorkspaceSection { - const ProposalImpact({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Proposal impact'; - } -} - -final class BonusMarkUpStep extends RichTextStep { - const BonusMarkUpStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - }); - - @override - String localizedName(BuildContext context) { - return 'Bonus mark-up'; - } -} - -final class ValueForMoneyStep extends RichTextStep { - const ValueForMoneyStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - }); - - @override - String localizedName(BuildContext context) { - return 'Value for Money'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_setup.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_setup.dart deleted file mode 100644 index 179ea21663a..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_setup.dart +++ /dev/null @@ -1,27 +0,0 @@ -part of 'workspace_sections.dart'; - -final class ProposalSetup extends WorkspaceSection { - const ProposalSetup({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Proposal setup'; - } -} - -final class TitleStep extends RichTextStep { - const TitleStep({ - required super.id, - required super.sectionId, - required super.data, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Title'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_solution.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_solution.dart deleted file mode 100644 index 013e1ed37b2..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_solution.dart +++ /dev/null @@ -1,73 +0,0 @@ -part of 'workspace_sections.dart'; - -final class ProposalSolution extends WorkspaceSection { - const ProposalSolution({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Proposal solution'; - } -} - -final class ProblemPerspectiveStep extends RichTextStep { - const ProblemPerspectiveStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Problem perspective'; - } - - @override - String localizedDesc(BuildContext context) { - return "What is your perspective on the problem you're solving?"; - } -} - -final class PerspectiveRationaleStep extends RichTextStep { - const PerspectiveRationaleStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Perspective rationale'; - } - - @override - String localizedDesc(BuildContext context) { - return 'Why did you choose this perspective?'; - } -} - -final class ProjectEngagementStep extends RichTextStep { - const ProjectEngagementStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Project engagement'; - } - - @override - String localizedDesc(BuildContext context) { - return 'Who will your project engage?'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_summary.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_summary.dart deleted file mode 100644 index bed4f23ffcd..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/proposal_summary.dart +++ /dev/null @@ -1,58 +0,0 @@ -part of 'workspace_sections.dart'; - -final class ProposalSummary extends WorkspaceSection { - const ProposalSummary({ - required super.id, - required super.steps, - }); - - @override - String localizedName(BuildContext context) { - return 'Proposal summary'; - } -} - -final class ProblemStep extends RichTextStep { - const ProblemStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Problem segment'; - } -} - -final class SolutionStep extends RichTextStep { - const SolutionStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Solution segment'; - } -} - -final class PublicDescriptionStep extends RichTextStep { - const PublicDescriptionStep({ - required super.id, - required super.sectionId, - required super.data, - super.charsLimit, - super.guidances, - }); - - @override - String localizedName(BuildContext context) { - return 'Public description'; - } -} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_proposal_list_item.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_proposal_list_item.dart new file mode 100644 index 00000000000..65f41502dae --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_proposal_list_item.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +final class WorkspaceProposalListItem extends Equatable { + final String id; + final String name; + + const WorkspaceProposalListItem({ + required this.id, + required this.name, + }); + + @override + List get props => [id, name]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_sections.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_sections.dart index 658e273775f..4ad9e817cd7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_sections.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_sections.dart @@ -2,17 +2,23 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/widgets.dart'; -part 'capability_and_feasibility.dart'; -part 'proposal_impact.dart'; -part 'proposal_setup.dart'; -part 'proposal_solution.dart'; -part 'proposal_summary.dart'; +final class WorkspaceSection extends BaseSection { + final String name; -sealed class WorkspaceSection extends BaseSection { const WorkspaceSection({ required super.id, + required this.name, required super.steps, }); + + @override + String localizedName(BuildContext context) => name; + + @override + List get props => [ + ...super.props, + name, + ]; } sealed class WorkspaceSectionStep extends BaseSectionStep { @@ -21,22 +27,34 @@ sealed class WorkspaceSectionStep extends BaseSectionStep { required super.sectionId, super.isEnabled, super.isEditable, - super.guidances, }); } -abstract base class RichTextStep extends WorkspaceSectionStep { - final DocumentJson data; +final class RichTextStep extends WorkspaceSectionStep { + final String name; + final String? description; + final MarkdownData? initialData; final int? charsLimit; const RichTextStep({ required super.id, required super.sectionId, - required this.data, + required this.name, + this.description, + this.initialData, this.charsLimit, super.isEditable, - super.guidances, }); - String localizedDesc(BuildContext context) => localizedName(context); + @override + String localizedName(BuildContext context) => name; + + @override + List get props => [ + ...super.props, + name, + description, + initialData, + charsLimit, + ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_tab_type.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_tab_type.dart new file mode 100644 index 00000000000..486e48789e9 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/workspace/workspace_tab_type.dart @@ -0,0 +1,17 @@ +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:flutter/widgets.dart'; + +enum WorkspaceTabType { + draftProposal, + finalProposal; + + String localizedName( + BuildContext context, { + required int count, + }) { + return switch (this) { + WorkspaceTabType.draftProposal => context.l10n.draftProposalsX(count), + WorkspaceTabType.finalProposal => context.l10n.finalProposalsX(count), + }; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_view_models/pubspec.yaml index b87ce58469f..6a475cdae52 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/pubspec.yaml @@ -17,10 +17,13 @@ dependencies: path: ../catalyst_voices_localization catalyst_voices_models: path: ../catalyst_voices_models + catalyst_voices_shared: + path: ../catalyst_voices_shared equatable: ^2.0.7 flutter: sdk: flutter formz: ^0.7.0 + intl: ^0.19.0 dev_dependencies: catalyst_analysis: ^2.0.0 diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/test/campaign/campaign_stage_test.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/test/campaign/campaign_stage_test.dart new file mode 100644 index 00000000000..6d10f5d3da6 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/test/campaign/campaign_stage_test.dart @@ -0,0 +1,69 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/src/campaign/campaign_stage.dart'; +import 'package:test/test.dart'; + +void main() { + group(CampaignStage, () { + final date = DateTime(2024, 12, 3, 12, 0); + final campaign = Campaign( + id: 'id', + name: 'name', + description: 'description', + startDate: date, + endDate: date, + proposalsCount: 0, + sections: const [], + publish: CampaignPublish.draft, + proposalTemplate: const ProposalTemplate(sections: []), + ); + + test('draft campaign resolves to draft stage', () { + final draftCampaign = campaign.copyWith(publish: CampaignPublish.draft); + + expect( + CampaignStage.fromCampaign(draftCampaign, date), + equals(CampaignStage.draft), + ); + }); + + test('scheduled campaign resolves to scheduled stage', () { + final scheduledCampaign = campaign.copyWith( + publish: CampaignPublish.published, + startDate: date.plusDays(1), + endDate: date.plusDays(2), + ); + + expect( + CampaignStage.fromCampaign(scheduledCampaign, date), + equals(CampaignStage.scheduled), + ); + }); + + test('live campaign resolves to live stage', () { + final liveCampaign = campaign.copyWith( + publish: CampaignPublish.published, + startDate: date.minusDays(1), + endDate: date.plusDays(2), + ); + + expect( + CampaignStage.fromCampaign(liveCampaign, date), + equals(CampaignStage.live), + ); + }); + + test('completed campaign resolves to completed stage', () { + final liveCampaign = campaign.copyWith( + publish: CampaignPublish.published, + startDate: date.minusDays(2), + endDate: date.minusDays(1), + ); + + expect( + CampaignStage.fromCampaign(liveCampaign, date), + equals(CampaignStage.completed), + ); + }); + }); +} diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/homePage.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/homePage.ts index 6c77f456e56..8ad84835d00 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/homePage.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/homePage.ts @@ -266,6 +266,7 @@ export class HomePage { } async getPublicDRepKey(): Promise { + await this.page.waitForTimeout(2000); const isVisible = await this.publicDRepKeyLabel.isVisible(); if (!isVisible) { throw new Error("Public DRep Key label is not visible"); @@ -341,7 +342,7 @@ export class HomePage { expect(actualWalletCipData.networkId).not.toBeNaN(); expect(actualWalletCipData.changeAddress).not.toBeNaN(); expect(actualWalletCipData.rewardAddresses.length).toBeGreaterThan(0); - expect(actualWalletCipData.unusedAddresses.length).toBeGreaterThan(0); + //expect(actualWalletCipData.unusedAddresses.length).toBeGreaterThan(0); expect(actualWalletCipData.usedAddresses.length).toBeGreaterThan(0); expect(actualWalletCipData.utxos.length).toBeGreaterThan(0); expect(actualWalletCipData.publicDRepKey).not.toBeNaN(); diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/modal.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/modal.ts index 09545502963..f31d447ebb6 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/modal.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/modal.ts @@ -1,12 +1,12 @@ import { expect, Locator, Page } from "@playwright/test"; export enum ModalName { - SignData = 'SignData', - SignAndSubmitTx = 'SignAndSubmitTx', - SignAndSubmitRBACTx = 'SignAndSubmitRBACTx', - SignDataUserDeclined = 'UserDeclined', - SignTxUserDeclined = 'SignTxUserDeclined', - SignRBACTxUserDeclined = 'SignRBACTxUserDeclined', + SignData = "SignData", + SignAndSubmitTx = "SignAndSubmitTx", + SignAndSubmitRBACTx = "SignAndSubmitRBACTx", + SignDataUserDeclined = "UserDeclined", + SignTxUserDeclined = "SignTxUserDeclined", + SignRBACTxUserDeclined = "SignRBACTxUserDeclined", } export interface ModalContent { @@ -16,28 +16,28 @@ export interface ModalContent { export const modalContents: { [key in ModalName]: ModalContent } = { [ModalName.SignData]: { - header: 'Sign data', - unchangingText: 'Signature:', + header: "Sign data", + unchangingText: "Signature:", }, [ModalName.SignAndSubmitTx]: { - header: 'Sign & submit tx', - unchangingText: 'Tx hash:', + header: "Sign & submit tx", + unchangingText: "Tx hash:", }, [ModalName.SignAndSubmitRBACTx]: { - header: 'Sign & submit RBAC tx', - unchangingText: 'Tx hash:', + header: "Sign & submit RBAC tx", + unchangingText: "Tx hash:", }, [ModalName.SignDataUserDeclined]: { - header: 'Sign data', - unchangingText: 'user declined sign data', + header: "Sign data", + unchangingText: "WalletApiException", }, [ModalName.SignTxUserDeclined]: { - header: 'Sign & submit tx', - unchangingText: 'user declined sign tx', + header: "Sign & submit tx", + unchangingText: "WalletApiException", }, [ModalName.SignRBACTxUserDeclined]: { - header: 'Sign & submit RBAC tx', - unchangingText: 'user declined sign tx', + header: "Sign & submit RBAC tx", + unchangingText: "WalletApiException", }, }; @@ -50,12 +50,15 @@ export class Modal { constructor(page: Page, modalName: ModalName) { this.page = page; this.content = modalContents[modalName]; - this.modalHeader = this.page.getByText(this.content.header, { exact: true }); - this.modalBody = this.page.getByText(this.content.unchangingText) + this.modalHeader = this.page.getByText(this.content.header, { + exact: true, + }); + this.modalBody = this.page.getByText(this.content.unchangingText); } async assertModalIsVisible() { + await this.page.waitForTimeout(2000); await expect(this.modalHeader).toBeVisible(); await expect(this.modalBody).toBeVisible(); } -} \ No newline at end of file +} diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/walletListPage.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/walletListPage.ts index cb3df5087e5..3f2d59c9967 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/walletListPage.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/pages/walletListPage.ts @@ -1,6 +1,5 @@ -import { Locator, Page } from '@playwright/test'; -import { BrowserExtensionName } from '../utils/extensions'; - +import { Page } from "@playwright/test"; +import { BrowserExtensionName } from "../utils/extensions"; export class WalletListPage { readonly page: Page; @@ -9,10 +8,15 @@ export class WalletListPage { this.page = page; } async clickEnableWallet(walletName: BrowserExtensionName): Promise { - const enableButton = (walletName: BrowserExtensionName) => this.page.locator( - `flt-semantics:has(flt-semantics-img[aria-label*="Name: ${walletName.toLowerCase()}"]) ` + - `flt-semantics[role="button"]:has-text("Enable wallet")` - ); - await enableButton(walletName).click(); + if (walletName === BrowserExtensionName.Nufi) { + const [walletPopup] = await Promise.all([ + this.page.context().waitForEvent("page"), + await this.page.locator('//*[text()="Enable wallet"]').first().click(), + ]); + await walletPopup.locator("button:has-text('Connect')").click(); + } else { + await this.page.locator('//*[text()="Enable wallet"]').first().click(); + } + await this.page.waitForTimeout(2000); } -} \ No newline at end of file +} diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/setup.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/setup.ts index 841cc05d0f8..4630d313d71 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/setup.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/setup.ts @@ -42,12 +42,11 @@ export const enableWallet = async ( browser: BrowserContext ) => { const page = browser.pages()[0]; - await page.reload(); await page.goto("/"); await page.waitForTimeout(4000); const [walletPopup] = await Promise.all([ browser.waitForEvent("page"), - page.locator('//*[text()="Enable wallet"]').click(), + page.locator('//*[text()="Enable wallet"]').first().click(), ]); await walletPopup.waitForTimeout(2000); await allowExtension(walletPopup, walletConfig.extension.Name); diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallets.spec.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallets.spec.ts index 66a3b998045..7867550c2b7 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallets.spec.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/tests/wallets.spec.ts @@ -13,10 +13,18 @@ walletConfigs.forEach((walletConfig) => { walletConfig.extension.Name === "Typhon", "https://github.com/input-output-hk/catalyst-voices/issues/753" ); + test.skip( + walletConfig.extension.Name === "Yoroi", + "https://github.com/input-output-hk/catalyst-voices/issues/753" + ); test.skip( walletConfig.extension.Name === "Lace", "https://github.com/input-output-hk/catalyst-voices/issues/1190" ); + test.skip( + walletConfig.extension.Name === "Nufi", + "https://github.com/input-output-hk/catalyst-voices/issues/1190" + ); test.beforeAll(async () => { browser = await restoreWallet(walletConfig); await enableWallet(walletConfig, browser); @@ -40,11 +48,7 @@ walletConfigs.forEach((walletConfig) => { walletConfig.extension.Name ); const homePage = new HomePage(page); - const [walletPopup] = await Promise.all([ - browser.waitForEvent("page"), - homePage.signDataButton.click(), - ]); - await signWalletPopup(walletPopup, walletConfig); + await signWalletPopup(browser, walletConfig, homePage.signDataButton); await homePage.assertModal(ModalName.SignData); }); @@ -56,11 +60,11 @@ walletConfigs.forEach((walletConfig) => { walletConfig.extension.Name ); const homePage = new HomePage(page); - const [walletPopup] = await Promise.all([ - browser.waitForEvent("page"), - homePage.signAndSubmitTxButton.click(), - ]); - await signWalletPopup(walletPopup, walletConfig); + await signWalletPopup( + browser, + walletConfig, + homePage.signAndSubmitTxButton + ); await homePage.assertModal(ModalName.SignAndSubmitTx); }); @@ -74,11 +78,11 @@ walletConfigs.forEach((walletConfig) => { walletConfig.extension.Name ); const homePage = new HomePage(page); - const [walletPopup] = await Promise.all([ - browser.waitForEvent("page"), - homePage.signAndSubmitRBACTxButton.click(), - ]); - await signWalletPopup(walletPopup, walletConfig); + await signWalletPopup( + browser, + walletConfig, + homePage.signAndSubmitRBACTxButton + ); await homePage.assertModal(ModalName.SignAndSubmitRBACTx); } ); @@ -94,13 +98,14 @@ walletConfigs.forEach((walletConfig) => { walletConfig.extension.Name ); const homePage = new HomePage(page); - const [walletPopup] = await Promise.all([ - browser.waitForEvent("page"), - homePage.signDataButton.click(), - ]); const walletConfigClone = structuredClone(walletConfig); walletConfigClone.password = "BadPassword"; - await signWalletPopup(walletPopup, walletConfigClone, false); + await signWalletPopup( + browser, + walletConfigClone, + homePage.signDataButton, + false + ); await homePage.assertModal(ModalName.SignDataUserDeclined); } ); @@ -116,13 +121,14 @@ walletConfigs.forEach((walletConfig) => { walletConfig.extension.Name ); const homePage = new HomePage(page); - const [walletPopup] = await Promise.all([ - browser.waitForEvent("page"), - homePage.signAndSubmitTxButton.click(), - ]); const walletConfigClone = structuredClone(walletConfig); walletConfigClone.password = "BadPassword"; - await signWalletPopup(walletPopup, walletConfigClone, false); + await signWalletPopup( + browser, + walletConfigClone, + homePage.signAndSubmitTxButton, + false + ); await homePage.assertModal(ModalName.SignTxUserDeclined); } ); @@ -138,13 +144,14 @@ walletConfigs.forEach((walletConfig) => { walletConfig.extension.Name ); const homePage = new HomePage(page); - const [walletPopup] = await Promise.all([ - browser.waitForEvent("page"), - homePage.signAndSubmitRBACTxButton.click(), - ]); const walletConfigClone = structuredClone(walletConfig); walletConfigClone.password = "BadPassword"; - await signWalletPopup(walletPopup, walletConfigClone, false); + await signWalletPopup( + browser, + walletConfigClone, + homePage.signAndSubmitRBACTxButton, + false + ); await homePage.assertModal(ModalName.SignRBACTxUserDeclined); } ); diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensionDownloader.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensionDownloader.ts index de09abeca3a..d448035c537 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensionDownloader.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensionDownloader.ts @@ -44,14 +44,43 @@ export class ExtensionDownloader { } // Download the extension - const crxPath = await this.downloadExtension(extensionName); - + if (extensionName === BrowserExtensionName.Nufi) { + const zipPath = await this.downloadNufiExtension(); + await this.extractExtension(zipPath, extensionPath); + } else { + const crxPath = await this.downloadExtension(extensionName); + await this.extractExtension(crxPath, extensionPath); + } // Extract the extension - await this.extractExtension(crxPath, extensionPath); return extensionPath; } + private async downloadNufiExtension(): Promise { + const url = + "https://assets.nu.fi/extension/testnet/nufi-cwe-testnet-latest.zip"; + const filePath = path.join( + this.extensionsDir, + "nufi-cwe-testnet-latest.zip" + ); + + // Ensure the download directory exists + await fsPromises.mkdir(this.extensionsDir, { recursive: true }); + + // Fetch the extension + const res = await nodeFetch(url); + if (!res.ok) { + throw new Error(`Failed to download extension: ${res.statusText}`); + } + + // Stream the response directly to a file + const fileStream = fs.createWriteStream(filePath); + await pipeline(res.body, fileStream); + + console.log(`Extension has been downloaded to: ${filePath}`); + return filePath; + } + private async extractExtension( extensionPath: string, extractPath: string diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensions.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensions.ts index 78bee1df967..7c9b6a35337 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensions.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/extensions.ts @@ -8,6 +8,8 @@ export enum BrowserExtensionName { Lace = "Lace", Typhon = "Typhon", Eternl = "Eternl", + Yoroi = "Yoroi", + Nufi = "Nufi", } /* cspell: disable */ export const browserExtensions: BrowserExtension[] = [ @@ -26,6 +28,16 @@ export const browserExtensions: BrowserExtension[] = [ Id: "kmhcihpebfmpgmihbkipmjlmmioameka", HomeUrl: "index.html#/app/preprod/welcome", }, + { + Name: BrowserExtensionName.Yoroi, + Id: "poonlenmfdfbjfeeballhiibknlknepo", + HomeUrl: "main_window.html#", + }, + { + Name: BrowserExtensionName.Nufi, + Id: "hbklpdnlgiadjhdadfnfmemmklbopbcm", + HomeUrl: "/index.html#", + }, ]; /* cspell: enable */ diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/walletConfigs.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/walletConfigs.ts index ee099a04de1..ce1e11a8f04 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/walletConfigs.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/walletConfigs.ts @@ -3,86 +3,135 @@ import { WalletConfig } from "./wallets/walletUtils"; export const walletConfigs: WalletConfig[] = [ { - id: '1', + id: "1", extension: getBrowserExtension(BrowserExtensionName.Lace), seed: [ - 'stomach', - 'horn', - 'rail', - 'afraid', - 'flip', - 'also', - 'abandon', - 'speed', - 'chaos', - 'daring', - 'soon', - 'soft', - 'okay', - 'online', - 'benefit', + "stomach", + "horn", + "rail", + "afraid", + "flip", + "also", + "abandon", + "speed", + "chaos", + "daring", + "soon", + "soft", + "okay", + "online", + "benefit", ], - username: 'test123', - password: 'test12345678@', - cipBridge: ['cip-95'] + username: "test123", + password: "test12345678@", + cipBridge: ["cip-95"], }, { - id: '2', + id: "2", extension: getBrowserExtension(BrowserExtensionName.Typhon), seed: [ - 'stomach', - 'horn', - 'rail', - 'afraid', - 'flip', - 'also', - 'abandon', - 'speed', - 'chaos', - 'daring', - 'soon', - 'soft', - 'okay', - 'online', - 'benefit', + "stomach", + "horn", + "rail", + "afraid", + "flip", + "also", + "abandon", + "speed", + "chaos", + "daring", + "soon", + "soft", + "okay", + "online", + "benefit", ], - username: 'test123', - password: 'test12345678@', - cipBridge: ['cip-30'] + username: "test123", + password: "test12345678@", + cipBridge: ["cip-30"], }, { - id: '3', + id: "3", extension: getBrowserExtension(BrowserExtensionName.Eternl), seed: [ - 'stomach', - 'horn', - 'rail', - 'afraid', - 'flip', - 'also', - 'abandon', - 'speed', - 'chaos', - 'daring', - 'soon', - 'soft', - 'okay', - 'online', - 'benefit', + "stomach", + "horn", + "rail", + "afraid", + "flip", + "also", + "abandon", + "speed", + "chaos", + "daring", + "soon", + "soft", + "okay", + "online", + "benefit", ], - username: 'test123', - password: 'test12345678@!!', - cipBridge: ['cip-30', 'cip-95'] - } + username: "test123", + password: "test12345678@!!", + cipBridge: ["cip-30", "cip-95"], + }, + { + id: "4", + extension: getBrowserExtension(BrowserExtensionName.Yoroi), + seed: [ + "stomach", + "horn", + "rail", + "afraid", + "flip", + "also", + "abandon", + "speed", + "chaos", + "daring", + "soon", + "soft", + "okay", + "online", + "benefit", + ], + username: "test123", + password: "test12345678@!!", + cipBridge: ["cip-95"], + }, + { + id: "5", + extension: getBrowserExtension(BrowserExtensionName.Nufi), + seed: [ + "stomach", + "horn", + "rail", + "afraid", + "flip", + "also", + "abandon", + "speed", + "chaos", + "daring", + "soon", + "soft", + "okay", + "online", + "benefit", + ], + username: "test123", + password: "test12345678@!!", + cipBridge: ["cip-95"], + }, ]; export const getWalletConfig = (id: string): WalletConfig => { - const walletConfig = walletConfigs.find(walletConfig => walletConfig.id === id); + const walletConfig = walletConfigs.find( + (walletConfig) => walletConfig.id === id + ); if (!walletConfig) { throw new Error(`Wallet config with id ${id} not found`); } return walletConfig; -} +}; export const getWalletConfigs = (): WalletConfig[] => walletConfigs; - \ No newline at end of file diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/nufiUtils.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/nufiUtils.ts new file mode 100644 index 00000000000..fd1c058b179 --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/nufiUtils.ts @@ -0,0 +1,43 @@ +import { Page } from "playwright"; +import { WalletConfig } from "./walletUtils"; + +export const onboardNufiWallet = async ( + page: Page, + walletConfig: WalletConfig +): Promise => { + await page.locator("//*[@data-testid='RestorePageIcon']").click(); + const seedPhrase = walletConfig.seed; + for (let i = 0; i < 15; i++) { + await page + .locator(`//div[@rtl-data-test-id='mnemonic-field-input-${i}']//input`) + .fill(seedPhrase[i]); + } + await page + .locator("//span[@data-test-id='terms-and-conditions-checkbox']/input") + .check(); + await page.locator("button:has-text('Continue')").click(); + await page + .locator("//input[@rtl-data-test-id='wallet-name-field']") + .fill(walletConfig.username); + await page.locator("//input[@id=':rg:']").fill(walletConfig.password); + await page.locator("//input[@id=':rh:']").fill(walletConfig.password); + await page.locator("button:has-text('Continue')").click(); + await page.locator("button:has-text('Recover')").click(); + await page.locator("button:has-text('Go to Wallet')").click(); +}; + +export const connectWalletPopup = async (page: Page): Promise => { + await page.locator("button:has-text('Connect')").click(); +}; + +export const signNufiData = async ( + page: Page, + password: string, + isCorrectPassword: boolean +): Promise => { + if (!isCorrectPassword) { + await page.locator("button:has-text('Reject')").click(); + return; + } + await page.locator("button:has-text('Sign')").click(); +}; diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/walletUtils.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/walletUtils.ts index fc698a1a689..3b5638a9c45 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/walletUtils.ts +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/walletUtils.ts @@ -1,8 +1,10 @@ -import { Page } from "@playwright/test"; +import { BrowserContext, Locator, Page } from "@playwright/test"; import { BrowserExtension, BrowserExtensionName } from "../extensions"; import { onboardEternlWallet, signEternlData } from "./eternlUtils"; import { onboardLaceWallet, signLaceData } from "./laceUtils"; +import { onboardNufiWallet, signNufiData } from "./nufiUtils"; import { onboardTyphonWallet, signTyphonData } from "./typhonUtils"; +import { onboardYoroiWallet, signYoroiData } from "./yoroiUtils"; export interface WalletConfig { id: string; @@ -27,6 +29,12 @@ export const onboardWallet = async ( case BrowserExtensionName.Eternl: await onboardEternlWallet(page, walletConfig); break; + case BrowserExtensionName.Yoroi: + await onboardYoroiWallet(page, walletConfig); + break; + case BrowserExtensionName.Nufi: + await onboardNufiWallet(page, walletConfig); + break; default: throw new Error("Wallet not in use"); } @@ -49,16 +57,45 @@ export const allowExtension = async ( case "Eternl": await tab.locator('button:has-text("Grant Access")').click(); break; + case "Yoroi": + await tab.locator("button:has(#connectedWalletName)").click(); + break; + case "Nufi": + await tab.locator("//input[@type='password']").fill("test12345678@!!"); + await tab.locator("button:has-text('Connect')").click(); + await tab.locator("button:has-text('Connect')").click(); + break; default: throw new Error("Wallet not in use"); } }; +const getWalletPopup = async ( + browser: BrowserContext, + triggerLocatorCLick: Locator +): Promise => { + if (browser.pages().length > 1) { + await triggerLocatorCLick.click(); + await browser + .pages() + [browser.pages().length - 1].waitForLoadState("domcontentloaded"); + return browser.pages()[browser.pages().length - 1]; + } else { + const [page] = await Promise.all([ + browser.waitForEvent("page"), + triggerLocatorCLick.click(), + ]); + return page; + } +}; + export const signWalletPopup = async ( - page: Page, + browser: BrowserContext, walletConfig: WalletConfig, + locatorTrigger: Locator, isCorrectPassword = true ): Promise => { + const page = await getWalletPopup(browser, locatorTrigger); switch (walletConfig.extension.Name) { case BrowserExtensionName.Typhon: await signTyphonData(page, walletConfig.password, isCorrectPassword); @@ -69,6 +106,12 @@ export const signWalletPopup = async ( case BrowserExtensionName.Eternl: await signEternlData(page, walletConfig.password, isCorrectPassword); break; + case BrowserExtensionName.Yoroi: + await signYoroiData(page, walletConfig.password, isCorrectPassword); + break; + case BrowserExtensionName.Nufi: + await signNufiData(page, walletConfig.password, isCorrectPassword); + break; default: throw new Error("Wallet not in use"); } diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/yoroiUtils.ts b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/yoroiUtils.ts new file mode 100644 index 00000000000..b21b5785619 --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/utils/wallets/yoroiUtils.ts @@ -0,0 +1,49 @@ +import { Page } from "playwright"; +import { WalletConfig } from "./walletUtils"; + +export const onboardYoroiWallet = async ( + page: Page, + walletConfig: WalletConfig +): Promise => { + /* cspell: disable */ + await page.locator("#initialPage-tosAgreement-checkbox").check(); + await page.locator("#initialPage-continue-button").click(); + await page.locator("#mui-2").click(); + await page.locator("#somewhere-checkbox").check(); + await page.locator('button:has-text("Continue")').click(); + + await page + .locator(".UriPromptForm_buttonsWrapper button.MuiButton-secondary") + .click(); + + await page.locator("#restoreWalletButton").click(); + await page.locator('button:has-text("Cardano Preprod Testnet")').click(); + await page.locator("#fifteenWordsButton").click(); + const seedPhrase = walletConfig.seed; + for (let i = 0; i < seedPhrase.length; i++) { + const ftSeedPhraseSelector = `#downshift-${i}-input`; + await page.locator(ftSeedPhraseSelector).fill(seedPhrase[i]); + } + await page.locator(`#downshift-${seedPhrase.length - 1}-input`).blur(); + await page.locator("#primaryButton").click(); + await page.locator("#infoDialogContinueButton").click(); + await page.locator("#walletNameInput-label").fill(walletConfig.username); + await page.locator("#walletPasswordInput-label").fill(walletConfig.password); + await page.locator("#repeatPasswordInput-label").fill(walletConfig.password); + await page.locator("#primaryButton").click(); + await page.locator("#dialog-gotothewallet-button").click(); +}; +/* cspell: enable */ + +export const signYoroiData = async ( + signTab: Page, + password: string, + isCorrectPassword: boolean +): Promise => { + await signTab.locator("#walletPassword").fill(password); + await signTab.locator("#confirmButton").click(); + if (!isCorrectPassword) { + await signTab.locator("#cancelButton").click(); + return; + } +}; diff --git a/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart b/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart index 74d552552da..21461bcdd30 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart +++ b/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/rbac/auth_token.dart @@ -1,9 +1,11 @@ import 'dart:convert'; import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_cardano_serialization/src/utils/cbor.dart'; import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:cbor/cbor.dart'; -import 'package:ulid/ulid.dart'; +import 'package:uuid/data.dart'; +import 'package:uuid/uuid.dart'; /// The Authentication Token is based loosely on JWT. /// It consists of an Authentication Header attached to every authenticated @@ -23,9 +25,9 @@ import 'package:ulid/ulid.dart'; /// The Encoded Binary Token is a [CBOR sequence] that consists of 3 fields. /// /// * `kid` : The key identifier. -/// * `ulid` : A ULID which defines when the token was issued, +/// * `uuid` : A UUID v7 which defines when the token was issued, /// and a random nonce. -/// * `signature` : The signature over the `kid` and `ulid` fields. +/// * `signature` : The signature over the `kid` and `uuid` fields. final class AuthToken { /// The token prefix which distinguishes this auth token from other /// auth tokens and allows version via the v{} part. @@ -45,20 +47,21 @@ final class AuthToken { required Ed25519PrivateKey privateKey, required DateTime timestamp, }) async { - final ulid = CborBytes( - Ulid(millis: timestamp.millisecondsSinceEpoch).toBytes(), - ); + final uuidConfig = V7Options(timestamp.millisecondsSinceEpoch, null); + final uuidString = const Uuid().v7(config: uuidConfig); + final uuidBytes = Uuid.parse(uuidString); + final uuid = CborBytes(uuidBytes, tags: [CborCustomTags.uuid]); final toBeSigned = [ ...cbor.encode(kid.toCbor()), - ...cbor.encode(ulid), + ...cbor.encode(uuid), ]; final signature = await privateKey.sign(toBeSigned); final cborToken = [ ...cbor.encode(kid.toCbor()), - ...cbor.encode(ulid), + ...cbor.encode(uuid), ...cbor.encode(signature.toCbor()), ]; diff --git a/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/utils/cbor.dart b/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/utils/cbor.dart index 9b1d8390cf0..dda2c0e341e 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/utils/cbor.dart +++ b/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/utils/cbor.dart @@ -2,6 +2,9 @@ final class CborCustomTags { const CborCustomTags._(); + /// A cbor tag describing a uuid. + static const int uuid = 37; + /// A cbor tag describing a key-value pairs data. static const int map = 259; diff --git a/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/utils/uuid.dart b/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/utils/uuid.dart index 1650e12b8a1..a947552fa33 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/utils/uuid.dart +++ b/catalyst_voices/packages/libs/catalyst_cardano_serialization/lib/src/utils/uuid.dart @@ -60,3 +60,17 @@ extension type UuidV4._(List bytes) { return Uint8List.fromList(byteList); } } + +/// Utils around UUID v7. +abstract class UuidV7 { + /// Extracts the timestamp from the Uuid V7 string. + static DateTime parseTimestamp(String uuid) { + final uuidParts = uuid.split('-'); + // First 48 bits of UUIDv7 + final uuidTimestampHex = uuidParts[0] + uuidParts[1].substring(0, 4); + + // Convert timestampHex to milliseconds since Unix epoch + final timestamp = int.parse(uuidTimestampHex, radix: 16); + return DateTime.fromMillisecondsSinceEpoch(timestamp, isUtc: true); + } +} diff --git a/catalyst_voices/packages/libs/catalyst_cardano_serialization/pubspec.yaml b/catalyst_voices/packages/libs/catalyst_cardano_serialization/pubspec.yaml index 5073d1d1d1d..316b4690058 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano_serialization/pubspec.yaml +++ b/catalyst_voices/packages/libs/catalyst_cardano_serialization/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: cryptography: ^2.7.0 equatable: ^2.0.7 pinenacl: ^0.6.0 - ulid: ^2.0.0 + uuid: ^4.5.1 dev_dependencies: build_runner: ^2.4.12 diff --git a/catalyst_voices/packages/libs/catalyst_cardano_serialization/test/rbac/auth_token_test.dart b/catalyst_voices/packages/libs/catalyst_cardano_serialization/test/rbac/auth_token_test.dart index 15d49b0d21a..6de680b71d9 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano_serialization/test/rbac/auth_token_test.dart +++ b/catalyst_voices/packages/libs/catalyst_cardano_serialization/test/rbac/auth_token_test.dart @@ -5,7 +5,7 @@ import 'package:catalyst_key_derivation/catalyst_key_derivation.dart'; import 'package:cbor/cbor.dart'; import 'package:cryptography/cryptography.dart'; import 'package:test/test.dart'; -import 'package:ulid/ulid.dart'; +import 'package:uuid/parsing.dart'; // The certificate provided in the request final _c509Cert = C509Certificate.fromHex( @@ -55,19 +55,22 @@ void main() { expect(decodedCbor.length, 3); final decodedKid = decodedCbor[0] as CborBytes; - final decodedUlid = decodedCbor[1] as CborBytes; + final decodedUuid = decodedCbor[1] as CborBytes; final decodedSignature = decodedCbor[2] as CborBytes; expect(decodedKid.bytes, (kid.toCbor() as CborBytes).bytes); + + final decodedUuidBytes = decodedUuid.bytes; + expect(decodedUuidBytes, hasLength(16)); expect( - Ulid.fromBytes(decodedUlid.bytes).toMillis(), - timestamp.millisecondsSinceEpoch, + UuidV7.parseTimestamp(UuidParsing.unparse(decodedUuidBytes)), + timestamp, ); // Verify the signature final toBeSigned = [ ...cbor.encode(kid.toCbor()), - ...cbor.encode(decodedUlid), + ...cbor.encode(decodedUuid), ]; final publicKey = await privateKey.derivePublicKey(); diff --git a/catalyst_voices/packages/libs/catalyst_cose/README.md b/catalyst_voices/packages/libs/catalyst_cose/README.md index e8658b94f3e..90915e7da43 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/README.md +++ b/catalyst_voices/packages/libs/catalyst_cose/README.md @@ -32,6 +32,7 @@ dependencies: // ignore_for_file: avoid_print import 'dart:convert'; +import 'dart:typed_data'; import 'package:catalyst_cose/catalyst_cose.dart'; import 'package:cbor/cbor.dart'; @@ -39,26 +40,28 @@ import 'package:convert/convert.dart'; import 'package:cryptography/cryptography.dart'; Future main() async { + await _coseSign1(); + await _coseSign(); +} + +Future _coseSign1() async { final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - final privateKey = await keyPair.extractPrivateKeyBytes(); - final publicKey = await keyPair.extractPublicKey().then((e) => e.bytes); - - final payload = utf8.encode('This is the content.'); + final signerVerifier = _SignerVerifier(algorithm, keyPair); - final coseSign1 = await CatalystCose.sign1( - privateKey: privateKey, - payload: payload, - kid: CborBytes(publicKey), + final coseSign1 = await CoseSign1.sign( + protectedHeaders: const CoseHeaders.protected(), + unprotectedHeaders: const CoseHeaders.unprotected(), + signer: signerVerifier, + payload: utf8.encode('This is the content.'), ); - final verified = await CatalystCose.verifyCoseSign1( - coseSign1: coseSign1, - publicKey: publicKey, + final verified = await coseSign1.verify( + verifier: signerVerifier, ); print('COSE_SIGN1:'); - print(hex.encode(cbor.encode(coseSign1))); + print(hex.encode(cbor.encode(coseSign1.toCbor()))); print('verified: $verified'); assert( @@ -67,6 +70,68 @@ Future main() async { 'signed by the owner of the given public key', ); } + +Future _coseSign() async { + final algorithm = Ed25519(); + final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); + final signerVerifier = _SignerVerifier(algorithm, keyPair); + + final coseSign = await CoseSign.sign( + protectedHeaders: const CoseHeaders.protected(), + unprotectedHeaders: const CoseHeaders.unprotected(), + signers: [signerVerifier], + payload: utf8.encode('This is the content.'), + ); + + final verified = await coseSign.verifyAll( + verifiers: [signerVerifier], + ); + + print('COSE_SIGN:'); + print(hex.encode(cbor.encode(coseSign.toCbor()))); + print('verified: $verified'); + + assert( + verified, + 'The signature proves that given COSE_SIGN structure has been ' + 'signed by the owner of the given public key', + ); +} + +final class _SignerVerifier + implements CatalystCoseSigner, CatalystCoseVerifier { + final SignatureAlgorithm _algorithm; + final SimpleKeyPair _keyPair; + + const _SignerVerifier(this._algorithm, this._keyPair); + + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async { + final pk = await _keyPair.extractPublicKey(); + return Uint8List.fromList(pk.bytes); + } + + @override + Future sign(Uint8List data) async { + final signature = await _algorithm.sign(data, keyPair: _keyPair); + return Uint8List.fromList(signature.bytes); + } + + @override + Future verify(Uint8List data, Uint8List signature) async { + final publicKey = await _keyPair.extractPublicKey(); + return _algorithm.verify( + data, + signature: Signature( + signature, + publicKey: SimplePublicKey(publicKey.bytes, type: KeyPairType.ed25519), + ), + ); + } +} ``` ## Limitations diff --git a/catalyst_voices/packages/libs/catalyst_cose/example/main.dart b/catalyst_voices/packages/libs/catalyst_cose/example/main.dart index 5afbf2d0b1a..3bed84f2724 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/example/main.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/example/main.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_print import 'dart:convert'; +import 'dart:typed_data'; import 'package:catalyst_cose/catalyst_cose.dart'; import 'package:cbor/cbor.dart'; @@ -8,26 +9,28 @@ import 'package:convert/convert.dart'; import 'package:cryptography/cryptography.dart'; Future main() async { + await _coseSign1(); + await _coseSign(); +} + +Future _coseSign1() async { final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - final privateKey = await keyPair.extractPrivateKeyBytes(); - final publicKey = await keyPair.extractPublicKey().then((e) => e.bytes); + final signerVerifier = _SignerVerifier(algorithm, keyPair); - final payload = utf8.encode('This is the content.'); - - final coseSign1 = await CatalystCose.sign1( - privateKey: privateKey, - payload: payload, - kid: CborBytes(publicKey), + final coseSign1 = await CoseSign1.sign( + protectedHeaders: const CoseHeaders.protected(), + unprotectedHeaders: const CoseHeaders.unprotected(), + signer: signerVerifier, + payload: utf8.encode('This is the content.'), ); - final verified = await CatalystCose.verifyCoseSign1( - coseSign1: coseSign1, - publicKey: publicKey, + final verified = await coseSign1.verify( + verifier: signerVerifier, ); print('COSE_SIGN1:'); - print(hex.encode(cbor.encode(coseSign1))); + print(hex.encode(cbor.encode(coseSign1.toCbor()))); print('verified: $verified'); assert( @@ -36,3 +39,65 @@ Future main() async { 'signed by the owner of the given public key', ); } + +Future _coseSign() async { + final algorithm = Ed25519(); + final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); + final signerVerifier = _SignerVerifier(algorithm, keyPair); + + final coseSign = await CoseSign.sign( + protectedHeaders: const CoseHeaders.protected(), + unprotectedHeaders: const CoseHeaders.unprotected(), + signers: [signerVerifier], + payload: utf8.encode('This is the content.'), + ); + + final verified = await coseSign.verifyAll( + verifiers: [signerVerifier], + ); + + print('COSE_SIGN:'); + print(hex.encode(cbor.encode(coseSign.toCbor()))); + print('verified: $verified'); + + assert( + verified, + 'The signature proves that given COSE_SIGN structure has been ' + 'signed by the owner of the given public key', + ); +} + +final class _SignerVerifier + implements CatalystCoseSigner, CatalystCoseVerifier { + final SignatureAlgorithm _algorithm; + final SimpleKeyPair _keyPair; + + const _SignerVerifier(this._algorithm, this._keyPair); + + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async { + final pk = await _keyPair.extractPublicKey(); + return Uint8List.fromList(pk.bytes); + } + + @override + Future sign(Uint8List data) async { + final signature = await _algorithm.sign(data, keyPair: _keyPair); + return Uint8List.fromList(signature.bytes); + } + + @override + Future verify(Uint8List data, Uint8List signature) async { + final publicKey = await _keyPair.extractPublicKey(); + return _algorithm.verify( + data, + signature: Signature( + signature, + publicKey: SimplePublicKey(publicKey.bytes, type: KeyPairType.ed25519), + ), + ); + } +} 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 195f3996889..5b124e4669e 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/catalyst_cose.dart +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/catalyst_cose.dart @@ -1 +1,6 @@ -export 'src/catalyst_cose.dart'; +export 'src/cose_constants.dart'; +export 'src/cose_sign.dart'; +export 'src/cose_sign1.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/catalyst_cose.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/catalyst_cose.dart deleted file mode 100644 index d82a3a679f1..00000000000 --- a/catalyst_voices/packages/libs/catalyst_cose/lib/src/catalyst_cose.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:cbor/cbor.dart'; -import 'package:cryptography/cryptography.dart'; - -/// A dart plugin implementing CBOR Object Signing and Encryption -/// [RFC-9052](https://datatracker.ietf.org/doc/rfc9052/), -/// [RFC 9053](https://datatracker.ietf.org/doc/rfc9053/). -final class CatalystCose { - static const int _coseSign1Tag = 18; - static const int _algKey = 1; - static const int _kidKey = 4; - static const int _eddsaAlg = 3; - - CatalystCose._(); - - /// Signs the [payload] and returns a [CborValue] representing - /// a COSE_SIGN1 structure. - /// - /// This [kid] parameter identifies one piece of data that can be - /// used as input to find the needed cryptographic key. - /// - /// Limited to EdDSA algorithm with Ed25519 curve. - static Future sign1({ - required List privateKey, - required List payload, - CborValue? kid, - }) async { - final algorithm = Ed25519(); - final keyPair = await algorithm.newKeyPairFromSeed(privateKey); - - final protectedHeader = CborBytes( - cbor.encode( - CborMap({ - const CborSmallInt(_algKey): const CborSmallInt(_eddsaAlg), - if (kid != null) const CborSmallInt(_kidKey): kid, - }), - ), - ); - - final unprotectedHeader = CborMap({}); - - final sigStructure = _createCoseSign1SigStructure( - protectedHeader: protectedHeader, - payload: CborBytes(payload), - ); - - final toBeSigned = cbor.encode( - CborBytes( - cbor.encode(sigStructure), - ), - ); - - final signature = await algorithm.sign( - toBeSigned, - keyPair: keyPair, - ); - - final coseSign1Structure = CborList( - [ - protectedHeader, - unprotectedHeader, - CborBytes(payload), - CborBytes(signature.bytes), - ], - tags: [_coseSign1Tag], - ); - - return coseSign1Structure; - } - - /// Verifies whether the given COSE_SIGN1 structure's signature - /// was created using the provided public key. - /// - /// Limited to EdDSA algorithm with Ed25519 curve. - /// - /// Returns `true` if the signature is valid, `false` otherwise. - static Future verifyCoseSign1({ - required CborValue coseSign1, - required List publicKey, - }) async { - final algorithm = Ed25519(); - - if (coseSign1 is! CborList || - coseSign1.tags.contains(_coseSign1Tag) != true) { - return false; - } - - final cborList = coseSign1; - if (cborList.length != 4) { - return false; - } - - final protectedHeader = cborList[0]; - final unprotectedHeader = cborList[1]; - final payload = cborList[2]; - final signature = cborList[3]; - - if (protectedHeader is! CborBytes || - unprotectedHeader is! CborMap || - payload is! CborBytes || - signature is! CborBytes) { - return false; - } - - final signatureBytes = signature.bytes; - - final sigStructure = _createCoseSign1SigStructure( - protectedHeader: protectedHeader, - payload: payload, - ); - - final toBeSigned = cbor.encode( - CborBytes( - cbor.encode(sigStructure), - ), - ); - - try { - final verified = await algorithm.verify( - toBeSigned, - signature: Signature( - signatureBytes, - publicKey: SimplePublicKey(publicKey, type: KeyPairType.ed25519), - ), - ); - return verified; - } catch (e) { - return false; - } - } - - static CborValue _createCoseSign1SigStructure({ - required CborValue protectedHeader, - required CborValue payload, - }) { - return CborList([ - // Context text identifying the context of the signature - CborString('Signature1'), - - // The protected attributes from the body structure - protectedHeader, - - // External supplied data, empty since not supplied - CborBytes([]), - - // Payload to be signed - payload, - ]); - } -} diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart new file mode 100644 index 00000000000..a914fd49529 --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_constants.dart @@ -0,0 +1,98 @@ +import 'dart:typed_data'; + +import 'package:catalyst_cose/src/types/string_or_int.dart'; +import 'package:cbor/cbor.dart'; + +/// Holds commonly used tags in COSE. +/// +/// Defined in [RFC-9052](https://datatracker.ietf.org/doc/rfc9052). +abstract final class CoseTags { + /// The tag that describes a COSE_SIGN1 structure. + static const int coseSign1 = 18; + + /// The tags that describes a COSE_SIGN structure. + static const int coseSign = 98; +} + +/// Holds commonly used keys for protected/unprotected headers in COSE. +final class CoseHeaderKeys { + const CoseHeaderKeys._(); + + /// The header key describing the signature algorithm. + static const alg = CborSmallInt(1); + + /// The header key describing the content-type of the payload. + static const contentType = CborSmallInt(3); + + /// The header key describing the key identifier. + static const kid = CborSmallInt(4); + + /// The header key for "content encoding". + static final contentEncoding = CborString('content encoding'); + + /// The header key for "type". + static final type = CborString('type'); + + /// The header key for "id". + static final id = CborString('id'); + + /// The header key for "ver". + static final ver = CborString('ver'); + + /// The header key for "ref". + static final ref = CborString('ref'); + + /// The header key for "template". + static final template = CborString('template'); + + /// The header key for "reply". + static final reply = CborString('reply'); + + /// The header key for "section". + static final section = CborString('section'); + + /// The header key for "collabs". + static final collabs = CborString('collabs'); +} + +/// Holds commonly used keys for protected/unprotected headers in COSE. +final class CoseValues { + const CoseValues._(); + + /// The Edwards-Curve Digital Signature Algorithm (EdDSA). + static const eddsaAlg = -8; + + /// The json content type value. + static const jsonContentType = 50; + + /// The brotli compression content encoding. + static const brotliContentEncoding = 'br'; +} + +/// The interface for the data signer callback. +// ignore: one_member_abstracts +abstract interface class CatalystCoseSigner { + /// Returns the alg identifier that should refer + /// to the cryptographic algorithm used to [sign] the data. + StringOrInt? get alg; + + /// Returns a key identifier that typically should refer to the public key + /// of the private key used to sign the data. + Future get kid; + + /// The [data] should be signed with a private key + /// and the resulting signature returned as [Uint8List]. + Future sign(Uint8List data); +} + +/// The interface for the signature verifier callback. +// ignore: one_member_abstracts +abstract interface class CatalystCoseVerifier { + /// Returns a key identifier that typically should refer to the public key + /// of the private key used to sign the data. + Future get kid; + + /// The [signature] should be verified against + /// a known public/private key over the [data]. + Future verify(Uint8List data, Uint8List signature); +} 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 new file mode 100644 index 00000000000..7d73a4eacec --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign.dart @@ -0,0 +1,261 @@ +import 'dart:typed_data'; + +import 'package:catalyst_cose/src/cose_constants.dart'; +import 'package:catalyst_cose/src/types/cose_headers.dart'; +import 'package:cbor/cbor.dart'; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; + +/// The COSE_SIGN structure implementation, supporting multiple signatures. +/// +/// [RFC-9052](https://datatracker.ietf.org/doc/rfc9052/), +/// [RFC 9053](https://datatracker.ietf.org/doc/rfc9053/). +final class CoseSign extends Equatable { + /// The protected headers that are protected + /// by the cryptographic [signatures]. + final CoseHeaders protectedHeaders; + + /// The unprotected headers that are not protected by the [signatures]. + final CoseHeaders unprotectedHeaders; + + /// The data that is signed by the [signatures]. + final Uint8List payload; + + /// The cryptographic signatures over + /// the [protectedHeaders] and the [payload]. + final List signatures; + + /// The default constructor for the [CoseSign]. + const CoseSign({ + required this.protectedHeaders, + required this.unprotectedHeaders, + required this.payload, + required this.signatures, + }); + + /// Deserializes the type from cbor. + factory CoseSign.fromCbor(CborValue value) { + if (value is! CborList || value.length != 4) { + throw FormatException('$value is not a valid COSE_SIGN structure'); + } + + final protectedHeaders = value[0]; + final unprotectedHeaders = value[1]; + final payload = value[2]; + final signatures = value[3]; + + return CoseSign( + protectedHeaders: CoseHeaders.fromCbor( + protectedHeaders, + encodeAsBytes: true, + ), + unprotectedHeaders: CoseHeaders.fromCbor( + unprotectedHeaders, + encodeAsBytes: false, + ), + payload: Uint8List.fromList((payload as CborBytes).bytes), + signatures: (signatures as CborList).map(CoseSignature.fromCbor).toList(), + ); + } + + /// Creates a signed COSE_SIGN structure. + /// + /// The [CoseHeaders.alg] parameter in headers must match + /// the signature algorithm used by the [signers]. + static Future sign({ + required CoseHeaders protectedHeaders, + required CoseHeaders unprotectedHeaders, + required Uint8List payload, + required List signers, + }) async { + 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, + ); + } + + /// Verifies whether the COSE_SIGN signature is valid. + /// + /// The signature is selected from the list of [signatures] based on the kid]. + /// The [verifier] is responsible for providing the verification algorithm. + Future verify({ + required CatalystCoseVerifier verifier, + }) async { + for (final signature in signatures) { + if (const DeepCollectionEquality() + .equals(signature.protectedHeaders.kid, await verifier.kid)) { + final toBeSigned = _createCoseSignSigStructureBytes( + bodyProtectedHeaders: protectedHeaders, + signatureProtectedHeaders: signature.protectedHeaders, + payload: payload, + ); + return verifier.verify(toBeSigned, signature.signature); + } + } + + // no eligible signature found that would match the kid + return false; + } + + /// Verifies whether the COSE_SIGN [signatures] are valid. + /// + /// The [verifiers] are responsible for providing the verification algorithm. + Future verifyAll({ + required List verifiers, + }) async { + for (final verifier in verifiers) { + final isVerified = await verify(verifier: verifier); + if (!isVerified) { + return false; + } + } + + return true; + } + + /// Serializes the type as cbor. + CborValue toCbor() { + return CborList( + [ + protectedHeaders.toCbor(), + unprotectedHeaders.toCbor(), + CborBytes(payload), + CborList([ + for (final signature in signatures) signature.toCbor(), + ]), + ], + tags: [CoseTags.coseSign], + ); + } + + @override + List get props => [ + protectedHeaders, + unprotectedHeaders, + payload, + signatures, + ]; + + static Uint8List _createCoseSignSigStructureBytes({ + required CoseHeaders bodyProtectedHeaders, + required CoseHeaders signatureProtectedHeaders, + required Uint8List payload, + }) { + final sigStructure = _createCoseSignSigStructure( + bodyProtectedHeaders: bodyProtectedHeaders.toCbor(), + signatureProtectedHeaders: signatureProtectedHeaders.toCbor(), + payload: CborBytes(payload), + ); + + return Uint8List.fromList( + cbor.encode( + CborBytes( + cbor.encode(sigStructure), + ), + ), + ); + } + + static CborList _createCoseSignSigStructure({ + required CborValue bodyProtectedHeaders, + required CborValue signatureProtectedHeaders, + required CborValue payload, + }) { + return CborList([ + // Context text identifying the context of the signature + CborString('Signature'), + + // The protected attributes from the body structure + bodyProtectedHeaders, + + // The protected attributes from the signature structure + signatureProtectedHeaders, + + // External supplied data, empty since not supplied + CborBytes([]), + + // Payload to be signed + payload, + ]); + } +} + +/// The signature of the COSE_SIGN structure. +final class CoseSignature extends Equatable { + /// The protected headers that are protected by the cryptographic [signature]. + final CoseHeaders protectedHeaders; + + /// The unprotected headers that are not protected by the [signature]. + final CoseHeaders unprotectedHeaders; + + /// The cryptographic signature over the [protectedHeaders] and the payload. + final Uint8List signature; + + /// The default constructor for the [CoseSignature]. + const CoseSignature({ + required this.protectedHeaders, + required this.unprotectedHeaders, + required this.signature, + }); + + /// Deserializes the type from cbor. + factory CoseSignature.fromCbor(CborValue value) { + final list = value as CborList; + + final protectedHeaders = list[0]; + final unprotectedHeaders = list[1]; + final signature = list[2]; + + return CoseSignature( + protectedHeaders: CoseHeaders.fromCbor( + protectedHeaders, + encodeAsBytes: true, + ), + unprotectedHeaders: CoseHeaders.fromCbor( + unprotectedHeaders, + encodeAsBytes: false, + ), + signature: Uint8List.fromList((signature as CborBytes).bytes), + ); + } + + /// Serializes the type as cbor. + CborValue toCbor() { + return CborList([ + protectedHeaders.toCbor(), + unprotectedHeaders.toCbor(), + CborBytes(signature), + ]); + } + + @override + List get props => [ + protectedHeaders, + unprotectedHeaders, + signature, + ]; +} 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 new file mode 100644 index 00000000000..842fa57ef5f --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/cose_sign1.dart @@ -0,0 +1,162 @@ +import 'dart:typed_data'; + +import 'package:catalyst_cose/src/cose_constants.dart'; +import 'package:catalyst_cose/src/types/cose_headers.dart'; +import 'package:cbor/cbor.dart'; +import 'package:equatable/equatable.dart'; + +/// The COSE_SIGN1 structure implementation, supporting a single signature. +/// +/// [RFC-9052](https://datatracker.ietf.org/doc/rfc9052/), +/// [RFC 9053](https://datatracker.ietf.org/doc/rfc9053/). +final class CoseSign1 extends Equatable { + /// The protected headers that are protected by the cryptographic [signature]. + final CoseHeaders protectedHeaders; + + /// The unprotected headers that are not protected by the [signature]. + final CoseHeaders unprotectedHeaders; + + /// The data that is signed by the [signature]. + final Uint8List payload; + + /// The cryptographic signature over the [protectedHeaders] and the [payload]. + final Uint8List signature; + + /// The default constructor for the [CoseSign1]. + const CoseSign1({ + required this.protectedHeaders, + required this.unprotectedHeaders, + required this.payload, + required this.signature, + }); + + /// Deserializes the type from cbor. + factory CoseSign1.fromCbor(CborValue value) { + if (value is! CborList || value.length != 4) { + throw FormatException('$value is not a valid COSE_SIGN1 structure'); + } + + final protectedHeaders = value[0]; + final unprotectedHeaders = value[1]; + final payload = value[2]; + final signature = value[3]; + + return CoseSign1( + protectedHeaders: CoseHeaders.fromCbor( + protectedHeaders, + encodeAsBytes: true, + ), + unprotectedHeaders: CoseHeaders.fromCbor( + unprotectedHeaders, + encodeAsBytes: false, + ), + payload: Uint8List.fromList((payload as CborBytes).bytes), + signature: Uint8List.fromList((signature as CborBytes).bytes), + ); + } + + /// Creates a signed COSE_SIGN1 structure. + /// + /// The [CoseHeaders.alg] parameter in headers must match + /// the signature algorithm used by the [signer]. + static Future sign({ + required CoseHeaders protectedHeaders, + required CoseHeaders unprotectedHeaders, + required Uint8List payload, + required CatalystCoseSigner signer, + }) async { + final kid = await signer.kid; + + protectedHeaders = protectedHeaders.copyWith( + alg: () => signer.alg, + kid: () => kid, + ); + + final sigStructure = _createCoseSign1SigStructure( + protectedHeader: protectedHeaders.toCbor(), + payload: CborBytes(payload), + ); + + final toBeSigned = Uint8List.fromList( + cbor.encode( + CborBytes( + cbor.encode(sigStructure), + ), + ), + ); + + return CoseSign1( + protectedHeaders: protectedHeaders, + unprotectedHeaders: unprotectedHeaders, + payload: payload, + signature: await signer.sign(toBeSigned), + ); + } + + /// Verifies whether the COSE_SIGN1 [signature] is valid. + /// + /// The [verifier] is responsible for providing the verification algorithm. + Future verify({ + required CatalystCoseVerifier verifier, + }) async { + final sigStructure = _createCoseSign1SigStructure( + protectedHeader: protectedHeaders.toCbor(), + payload: CborBytes(payload), + ); + + final toBeSigned = cbor.encode( + CborBytes( + cbor.encode(sigStructure), + ), + ); + + try { + return await verifier.verify( + Uint8List.fromList(toBeSigned), + Uint8List.fromList(signature), + ); + } catch (e) { + return false; + } + } + + /// Serializes the type as cbor. + CborValue toCbor() { + return CborList( + [ + protectedHeaders.toCbor(), + unprotectedHeaders.toCbor(), + CborBytes(payload), + CborBytes(signature), + ], + tags: [CoseTags.coseSign1], + ); + } + + @override + List get props => [ + protectedHeaders, + unprotectedHeaders, + payload, + signature, + ]; + + static CborValue _createCoseSign1SigStructure({ + required CborValue protectedHeader, + required CborValue payload, + }) { + return CborList([ + // Context text identifying the context of the signature + CborString('Signature1'), + + // The protected attributes from the body structure + protectedHeader, + + // External supplied data, empty since not supplied + CborBytes([]), + + // Payload to be signed + payload, + ]); + } +} diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart new file mode 100644 index 00000000000..ff1da58c113 --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/cose_headers.dart @@ -0,0 +1,229 @@ +import 'dart:typed_data'; + +import 'package:catalyst_cose/src/cose_constants.dart'; +import 'package:catalyst_cose/src/types/string_or_int.dart'; +import 'package:catalyst_cose/src/types/uuid.dart'; +import 'package:catalyst_cose/src/utils/cbor_utils.dart'; +import 'package:cbor/cbor.dart'; +import 'package:equatable/equatable.dart'; + +/// A callback to get an optional value. +/// Helps to distinguish whether a method argument +/// has been passed as null or not passed at all. +/// +/// See [CoseHeaders.copyWith]. +typedef OptionalValueGetter = T? Function(); + +/// A class that specifies headers that +/// can be used in protected/unprotected COSE headers. +final class CoseHeaders extends Equatable { + /// See [CoseHeaderKeys.alg]. + /// + /// Do not set the [alg] directly in the headers, + /// it will be auto-populated with [CatalystCoseSigner.alg] value. + final StringOrInt? alg; + + /// See [CoseHeaderKeys.kid]. + /// + /// Do not set the [kid] directly in the headers, + /// it will be auto-populated with [CatalystCoseSigner.kid] value. + final Uint8List? kid; + + /// See [CoseHeaderKeys.contentType]. + final StringOrInt? contentType; + + /// See [CoseHeaderKeys.contentEncoding]. + final StringOrInt? contentEncoding; + + /// See [CoseHeaderKeys.type]. + final Uuid? type; + + /// See [CoseHeaderKeys.id]. + final Uuid? id; + + /// See [CoseHeaderKeys.ver]. + final Uuid? ver; + + /// See [CoseHeaderKeys.ref]. + final ReferenceUuid? ref; + + /// See [CoseHeaderKeys.template]. + final ReferenceUuid? template; + + /// See [CoseHeaderKeys.reply]. + final ReferenceUuid? reply; + + /// See [CoseHeaderKeys.section]. + final String? section; + + /// See [CoseHeaderKeys.collabs]. + final List? collabs; + + /// Whether the type should be wrapped in extra [CborBytes] + /// or be a plain [CborMap], both formats are supported. + /// + /// For protected headers this should be `true` + /// while for unprotected headers it should be `false`. + final bool encodeAsBytes; + + /// The default constructor for the [CoseHeaders]. + const CoseHeaders({ + this.alg, + this.kid, + this.contentType, + this.contentEncoding, + this.type, + this.id, + this.ver, + this.ref, + this.template, + this.reply, + this.section, + this.collabs, + required this.encodeAsBytes, + }); + + /// The constructor for the protected [CoseHeaders]. + const CoseHeaders.protected({ + this.alg, + this.kid, + this.contentType, + this.contentEncoding, + this.type, + this.id, + this.ver, + this.ref, + this.template, + this.reply, + this.section, + this.collabs, + }) : encodeAsBytes = true; + + /// The constructor for the unprotected [CoseHeaders]. + const CoseHeaders.unprotected({ + this.alg, + this.kid, + this.contentType, + this.contentEncoding, + this.type, + this.id, + this.ver, + this.ref, + this.template, + this.reply, + this.section, + this.collabs, + }) : encodeAsBytes = false; + + /// Deserializes the type from cbor. + factory CoseHeaders.fromCbor(CborValue value, {bool encodeAsBytes = true}) { + final CborMap map; + + if (value is CborMap) { + // cose headers per specification might be wrapped in extra CborBytes, + // both formats are valid + map = value; + } else { + final cborBytes = value as CborBytes; + final encodedMap = cbor.decode(cborBytes.bytes); + map = encodedMap as CborMap; + } + + return CoseHeaders( + alg: CborUtils.deserializeStringOrInt(map[CoseHeaderKeys.alg]), + kid: CborUtils.deserializeBytes(map[CoseHeaderKeys.kid]), + contentType: + CborUtils.deserializeStringOrInt(map[CoseHeaderKeys.contentType]), + contentEncoding: + CborUtils.deserializeStringOrInt(map[CoseHeaderKeys.contentEncoding]), + type: CborUtils.deserializeUuid(map[CoseHeaderKeys.type]), + id: CborUtils.deserializeUuid(map[CoseHeaderKeys.id]), + ver: CborUtils.deserializeUuid(map[CoseHeaderKeys.ver]), + ref: CborUtils.deserializeReferenceUuid(map[CoseHeaderKeys.ref]), + template: + CborUtils.deserializeReferenceUuid(map[CoseHeaderKeys.template]), + reply: CborUtils.deserializeReferenceUuid(map[CoseHeaderKeys.reply]), + section: CborUtils.deserializeString(map[CoseHeaderKeys.section]), + collabs: CborUtils.deserializeStringList(map[CoseHeaderKeys.collabs]), + encodeAsBytes: encodeAsBytes, + ); + } + + /// Serializes the type as cbor. + CborValue toCbor() { + final map = CborMap({ + if (alg != null) CoseHeaderKeys.alg: alg!.toCbor(), + if (kid != null) CoseHeaderKeys.kid: CborBytes(kid!), + if (contentType != null) + CoseHeaderKeys.contentType: contentType!.toCbor(), + if (contentEncoding != null) + CoseHeaderKeys.contentEncoding: contentEncoding!.toCbor(), + if (type != null) CoseHeaderKeys.type: type!.toCbor(), + if (id != null) CoseHeaderKeys.id: id!.toCbor(), + if (ver != null) CoseHeaderKeys.ver: ver!.toCbor(), + if (ref != null) CoseHeaderKeys.ref: ref!.toCbor(), + if (template != null) CoseHeaderKeys.template: template!.toCbor(), + if (reply != null) CoseHeaderKeys.reply: reply!.toCbor(), + if (section != null) CoseHeaderKeys.section: CborString(section!), + if (collabs != null) + CoseHeaderKeys.collabs: CborUtils.serializeStringList(collabs), + }); + + if (encodeAsBytes) { + return CborBytes(cbor.encode(map)); + } else { + return map; + } + } + + /// Returns a copy of the [CoseHeaders] with overwritten properties. + CoseHeaders copyWith({ + OptionalValueGetter? alg, + OptionalValueGetter? kid, + OptionalValueGetter? contentType, + OptionalValueGetter? contentEncoding, + OptionalValueGetter? type, + OptionalValueGetter? id, + OptionalValueGetter? ver, + OptionalValueGetter? ref, + OptionalValueGetter? template, + OptionalValueGetter? reply, + OptionalValueGetter? section, + OptionalValueGetter?>? collabs, + bool? encodeAsBytes, + }) { + return CoseHeaders( + alg: alg != null ? alg() : this.alg, + kid: kid != null ? kid() : this.kid, + contentType: contentType != null ? contentType() : this.contentType, + contentEncoding: + contentEncoding != null ? contentEncoding() : this.contentEncoding, + type: type != null ? type() : this.type, + id: id != null ? id() : this.id, + ver: ver != null ? ver() : this.ver, + ref: ref != null ? ref() : this.ref, + template: template != null ? template() : this.template, + reply: reply != null ? reply() : this.reply, + section: section != null ? section() : this.section, + collabs: collabs != null ? collabs() : this.collabs, + encodeAsBytes: encodeAsBytes ?? this.encodeAsBytes, + ); + } + + @override + List get props => [ + alg, + kid, + contentType, + contentEncoding, + type, + id, + ver, + ref, + template, + reply, + section, + collabs, + encodeAsBytes, + ]; +} diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/string_or_int.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/string_or_int.dart new file mode 100644 index 00000000000..90ff156fb5a --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/string_or_int.dart @@ -0,0 +1,61 @@ +import 'package:cbor/cbor.dart'; +import 'package:equatable/equatable.dart'; + +/// A union type for either a string or int. +/// +/// In CDDL it's defined as (tstr/int) or (int/tstr). +sealed class StringOrInt extends Equatable { + const StringOrInt(); + + /// Deserializes the type from cbor. + factory StringOrInt.fromCbor(CborValue value) { + if (value is CborString) { + return StringValue.fromCbor(value); + } else { + return IntValue.fromCbor(value); + } + } + + /// Serializes the type as cbor. + CborValue toCbor(); +} + +/// The int value of [StringOrInt]. +final class IntValue extends StringOrInt { + /// The int value of the [StringOrInt] union. + final int value; + + /// The default constructor for [IntValue]. + const IntValue(this.value); + + /// Deserializes the type from cbor. + factory IntValue.fromCbor(CborValue value) { + return IntValue((value as CborSmallInt).value); + } + + @override + CborValue toCbor() => CborSmallInt(value); + + @override + List get props => [value]; +} + +/// The string value of [StringOrInt]. +final class StringValue extends StringOrInt { + /// The string value of the [StringOrInt] union. + final String value; + + /// The default constructor for [StringValue]. + const StringValue(this.value); + + /// Deserializes the type from cbor. + factory StringValue.fromCbor(CborValue value) { + return StringValue((value as CborString).toString()); + } + + @override + CborValue toCbor() => CborString(value); + + @override + List get props => [value]; +} diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart new file mode 100644 index 00000000000..14ee6c34097 --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/types/uuid.dart @@ -0,0 +1,74 @@ +import 'package:catalyst_cose/src/utils/cbor_utils.dart'; +import 'package:cbor/cbor.dart'; +import 'package:equatable/equatable.dart'; +import 'package:uuid/uuid.dart' as uuid; + +/// Represents the Uuid type. +/// +/// Uuid v7 is preferred. The cbor representation +/// is tagged with a tag that defines the uuid type. +extension type const Uuid(String value) { + /// Deserializes the type from cbor. + factory Uuid.fromCbor(CborValue value) { + if (value is CborBytes) { + return Uuid(uuid.Uuid.unparse(value.bytes)); + } else { + throw FormatException('The $value is not a valid uuid!'); + } + } + + /// Serializes the type as cbor. + CborValue toCbor() { + final uuidBytes = uuid.Uuid.parse(value); + return CborBytes(uuidBytes, tags: [CborUtils.uuidTag]); + } +} + +/// A reference to an entity represented by the [id]. +/// Optionally the version of the entity may be specified by the [ver]. +/// +/// What this uuid means depends where and how the class is used. +/// In CDDL it is defined as (UUID / [UUID, UUID]). +final class ReferenceUuid extends Equatable { + /// The referenced entity uuid. + final Uuid id; + + /// The version of the referenced entity. + final Uuid? ver; + + /// The default constructor for the [ReferenceUuid]. + const ReferenceUuid({ + required this.id, + this.ver, + }); + + /// Deserializes the type from cbor. + factory ReferenceUuid.fromCbor(CborValue value) { + if (value is CborList) { + return ReferenceUuid( + id: Uuid.fromCbor(value[0]), + ver: Uuid.fromCbor(value[1]), + ); + } else { + return ReferenceUuid( + id: Uuid.fromCbor(value), + ); + } + } + + /// Serializes the type as cbor. + CborValue toCbor() { + final ver = this.ver; + if (ver != null) { + return CborList([ + id.toCbor(), + ver.toCbor(), + ]); + } else { + return id.toCbor(); + } + } + + @override + List get props => [id, ver]; +} diff --git a/catalyst_voices/packages/libs/catalyst_cose/lib/src/utils/cbor_utils.dart b/catalyst_voices/packages/libs/catalyst_cose/lib/src/utils/cbor_utils.dart new file mode 100644 index 00000000000..969b38f545c --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/lib/src/utils/cbor_utils.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:catalyst_cose/src/types/string_or_int.dart'; +import 'package:catalyst_cose/src/types/uuid.dart'; +import 'package:cbor/cbor.dart'; + +/// A set of utils around cbor encoding/decoding. +final class CborUtils { + const CborUtils._(); + + /// A cbor tag for the UUID type. + static const int uuidTag = 37; + + /// Deserializes optional [StringOrInt] type. + static StringOrInt? deserializeStringOrInt(CborValue? value) { + if (value == null) { + return null; + } + + return StringOrInt.fromCbor(value); + } + + /// Deserializes optional [Uuid] type. + static Uuid? deserializeUuid(CborValue? value) { + if (value == null) { + return null; + } + + return Uuid.fromCbor(value); + } + + /// Deserialized optional [ReferenceUuid] type. + static ReferenceUuid? deserializeReferenceUuid(CborValue? value) { + if (value == null) { + return null; + } + + return ReferenceUuid.fromCbor(value); + } + + /// Deserializes optional [Uint8List] type. + static Uint8List? deserializeBytes(CborValue? value) { + if (value == null) { + return null; + } + + if (value is CborString) { + return utf8.encode(value.toString()); + } + + return Uint8List.fromList((value as CborBytes).bytes); + } + + /// Deserializes optional [String] type. + static String? deserializeString(CborValue? value) { + if (value == null) { + return null; + } + + return (value as CborString).toString(); + } + + /// Deserializes optional `List` type. + static List? deserializeStringList(CborValue? value) { + if (value == null) { + return null; + } + + final list = value as CborList; + return list.map((e) => (e as CborString).toString()).toList(); + } + + /// Serializes optional `List` type. + static CborValue serializeStringList(List? values) { + if (values == null) { + return const CborNull(); + } + + return CborList([ + for (final value in values) CborString(value), + ]); + } +} diff --git a/catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml b/catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml index 64f0c6caf0d..6bebbcbdc82 100644 --- a/catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml +++ b/catalyst_voices/packages/libs/catalyst_cose/pubspec.yaml @@ -10,8 +10,11 @@ environment: dependencies: cbor: ^6.2.0 + collection: ^1.18.0 convert: ^3.1.1 cryptography: ^2.7.0 + equatable: ^2.0.7 + uuid: ^4.5.1 dev_dependencies: catalyst_analysis: ^2.0.0 diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/catalyst_cose_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/catalyst_cose_test.dart deleted file mode 100644 index 39a0f187ee4..00000000000 --- a/catalyst_voices/packages/libs/catalyst_cose/test/catalyst_cose_test.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:catalyst_cose/catalyst_cose.dart'; -import 'package:cbor/cbor.dart'; -import 'package:convert/convert.dart'; -import 'package:cryptography/cryptography.dart'; -import 'package:test/test.dart'; - -const int _coseSign1Tag = 18; - -void main() { - group('CatalystCose', () { - late Ed25519 algorithm; - late SimpleKeyPair keyPair; - late List privateKey; - late SimplePublicKey publicKey; - - setUp(() async { - // Initialize the Ed25519 algorithm and generate a key pair - algorithm = Ed25519(); - keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); - privateKey = await keyPair.extractPrivateKeyBytes(); - publicKey = await keyPair.extractPublicKey(); - }); - - test('sign1 generates a valid COSE_SIGN1 structure', () async { - final payload = List.generate(10, (i) => i); // Example payload - - // Call the sign1 method - final coseSign1 = await CatalystCose.sign1( - privateKey: privateKey, - payload: payload, - kid: CborBytes(publicKey.bytes), - ); - - // Verify that the COSE_SIGN1 structure is a valid CborList - expect(coseSign1, isA()); - - final cborList = coseSign1 as CborList; - expect(cborList.length, 4); // Should contain 4 items - - final protectedHeader = cborList[0]; - expect(protectedHeader, isA()); - - final unprotectedHeader = cborList[1]; - expect(unprotectedHeader, isA()); - - final signedPayload = cborList[2]; - expect(signedPayload, isA()); - expect( - (signedPayload as CborBytes).bytes, - payload, - ); // Check that the payload is as expected - - final signature = cborList[3]; - // The actual signature bytes are not known in advance; - // just verify the type - expect(signature, isA()); - }); - - test('sign1 generates a valid cbor', () async { - final payload = List.generate(10, (i) => i); // Example payload - - // Call the sign1 method - final coseSign1 = await CatalystCose.sign1( - privateKey: privateKey, - payload: payload, - kid: CborBytes(publicKey.bytes), - ); - - expect( - hex.encode(cbor.encode(coseSign1)), - equals( - 'd2845826a201030458203b6a27bcceb6a42d62a3a8d02a6f0d736532157' - '71de243a63ac048a18b59da29a04a00010203040506070809584007ed6c' - '8a0b9bad9c375329a1d2de50d777f7f348c5597e3d963b80b9fd3488715' - '1dc8f0b2a4690f10f3256a7c883b6bd559be4195ca78fccc694f986ed45' - 'b80e', - ), - ); - }); - - test('verifyCoseSign1 validates correct signature', () async { - final payload = List.generate(10, (i) => i); // Example payload - - // Call the sign1 method - final coseSign1 = await CatalystCose.sign1( - privateKey: privateKey, - payload: payload, - kid: CborBytes(publicKey.bytes), - ); - - // Verify the signature - final isValid = await CatalystCose.verifyCoseSign1( - coseSign1: coseSign1, - publicKey: publicKey.bytes, - ); - - expect(isValid, true); // Check that the signature is valid - }); - - test('verifyCoseSign1 rejects invalid signatures', () async { - final payload = List.generate(10, (i) => i); // Example payload - - // Call the sign1 method - final coseSign1 = await CatalystCose.sign1( - privateKey: privateKey, - payload: payload, - kid: CborBytes(publicKey.bytes), - ); - - // Tamper with the signature to invalidate it - final cborList = coseSign1 as CborList; - final tamperedSignature = CborBytes( - List.generate(64, (i) => i), - ); // Example tampered signature - final tamperedCoseSign1 = CborList( - [ - cborList[0], // protected header - cborList[1], // unprotected header - cborList[2], // payload - tamperedSignature, - ], - tags: [_coseSign1Tag], - ); - - // Verify the tampered signature - final isValid = await CatalystCose.verifyCoseSign1( - coseSign1: tamperedCoseSign1, - publicKey: publicKey.bytes, - ); - - expect(isValid, false); // Check that the signature is invalid - }); - - test('verifyCoseSign1 handles invalid COSE_SIGN1 structure', () async { - // Construct an invalid COSE_SIGN1 structure - final invalidCoseSign1 = CborList( - [ - CborBytes( - List.generate(1, (i) => i), - ), // Invalid protected header - CborMap({}), - CborBytes([]), - CborBytes(List.generate(64, (i) => i)), // Invalid signature - ], - tags: [_coseSign1Tag], - ); - - // Verify the invalid COSE_SIGN1 structure - final isValid = await CatalystCose.verifyCoseSign1( - coseSign1: invalidCoseSign1, - publicKey: publicKey.bytes, - ); - - expect(isValid, false); // Check that the verification fails - }); - }); -} diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart new file mode 100644 index 00000000000..83d87972575 --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign1_test.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:catalyst_cose/catalyst_cose.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:test/test.dart'; + +void main() { + group(CoseSign1, () { + const uuidV7 = '0193b535-7196-7cd1-84e6-ad9c316cf2d2'; + late _SignerVerifier signerVerifier; + + setUp(() async { + // Initialize the Ed25519 algorithm and generate a key pair + final algorithm = Ed25519(); + final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); + signerVerifier = _SignerVerifier(algorithm, keyPair); + }); + + test('sign generates a valid COSE_SIGN1 structure', () async { + final coseSign1 = await CoseSign1.sign( + protectedHeaders: const CoseHeaders.protected( + contentType: IntValue(CoseValues.jsonContentType), + contentEncoding: StringValue(CoseValues.brotliContentEncoding), + type: Uuid(uuidV7), + id: Uuid(uuidV7), + ver: Uuid(uuidV7), + ref: ReferenceUuid(id: Uuid(uuidV7)), + template: ReferenceUuid(id: Uuid(uuidV7)), + reply: ReferenceUuid(id: Uuid(uuidV7)), + section: 'section_name', + collabs: ['test@domain.com'], + ), + unprotectedHeaders: const CoseHeaders.unprotected(), + signer: signerVerifier, + payload: utf8.encode('Test payload'), + ); + + // verify whether alg & kid fields were added to protected headers + expect(coseSign1.protectedHeaders.alg, isNotNull); + expect(coseSign1.protectedHeaders.kid, isNotEmpty); + + // test whether signature is valid + final isValid = await coseSign1.verify(verifier: signerVerifier); + expect(isValid, isTrue); + + // test whether serialization/deserialization works + final cborValue = coseSign1.toCbor(); + final deserializedCoseSign1 = CoseSign1.fromCbor(cborValue); + expect(deserializedCoseSign1, equals(deserializedCoseSign1)); + }); + + test('incorrectly signed COSE_SIGN1 structure does not validate', () async { + final coseSign1 = CoseSign1( + protectedHeaders: const CoseHeaders.protected(), + unprotectedHeaders: const CoseHeaders.unprotected(), + payload: utf8.encode('Test payload'), + signature: Uint8List(64), + ); + + // test whether signature is invalid + final isValid = await coseSign1.verify(verifier: signerVerifier); + expect(isValid, isFalse); + }); + }); +} + +final class _SignerVerifier + implements CatalystCoseSigner, CatalystCoseVerifier { + final SignatureAlgorithm _algorithm; + final SimpleKeyPair _keyPair; + + const _SignerVerifier(this._algorithm, this._keyPair); + + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async { + final pk = await _keyPair.extractPublicKey(); + return Uint8List.fromList(pk.bytes); + } + + @override + Future sign(Uint8List data) async { + final signature = await _algorithm.sign(data, keyPair: _keyPair); + return Uint8List.fromList(signature.bytes); + } + + @override + Future verify(Uint8List data, Uint8List signature) async { + final publicKey = await _keyPair.extractPublicKey(); + return _algorithm.verify( + data, + signature: Signature( + signature, + publicKey: SimplePublicKey(publicKey.bytes, type: KeyPairType.ed25519), + ), + ); + } +} diff --git a/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart new file mode 100644 index 00000000000..2acf3925f78 --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_cose/test/cose_sign_test.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:catalyst_cose/catalyst_cose.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:test/test.dart'; + +void main() { + group(CoseSign, () { + const uuidV4 = 'e9aba14f-d05b-49b2-b5b5-100595853384'; + const uuidV7 = '0193b535-7196-7cd1-84e6-ad9c316cf2d2'; + late _SignerVerifier signerVerifier; + + setUp(() async { + // Initialize the Ed25519 algorithm and generate a key pair + final algorithm = Ed25519(); + final keyPair = await algorithm.newKeyPairFromSeed(List.filled(32, 0)); + signerVerifier = _SignerVerifier(algorithm, keyPair); + }); + + test('sign generates a valid COSE_SIGN structure', () async { + final coseSign = await CoseSign.sign( + protectedHeaders: const CoseHeaders.protected( + contentType: IntValue(CoseValues.jsonContentType), + contentEncoding: StringValue(CoseValues.brotliContentEncoding), + type: Uuid(uuidV4), + id: Uuid(uuidV7), + ver: Uuid(uuidV7), + ref: ReferenceUuid(id: Uuid(uuidV7)), + template: ReferenceUuid(id: Uuid(uuidV7)), + reply: ReferenceUuid(id: Uuid(uuidV7)), + section: 'section_name', + collabs: ['test@domain.com'], + ), + unprotectedHeaders: const CoseHeaders.unprotected(), + signers: [signerVerifier], + payload: utf8.encode('Test payload'), + ); + + for (final signature in coseSign.signatures) { + // verify whether alg & kid fields were added to protected headers + expect(signature.protectedHeaders.alg, isNotNull); + expect(signature.protectedHeaders.kid, isNotEmpty); + } + + // test whether signatures are valid + final isValidAll = await coseSign.verifyAll(verifiers: [signerVerifier]); + expect(isValidAll, isTrue); + + final isValid = await coseSign.verify(verifier: signerVerifier); + expect(isValid, isTrue); + + // test whether serialization/deserialization works + final cborValue = coseSign.toCbor(); + final deserializedCoseSign = CoseSign.fromCbor(cborValue); + expect(deserializedCoseSign, equals(deserializedCoseSign)); + }); + + test('incorrectly signed COSE_SIGN structure does not validate', () async { + final coseSign = CoseSign( + protectedHeaders: const CoseHeaders.protected(), + unprotectedHeaders: const CoseHeaders.unprotected(), + payload: utf8.encode('Test payload'), + signatures: [ + CoseSignature( + protectedHeaders: const CoseHeaders.protected(), + unprotectedHeaders: const CoseHeaders.unprotected(), + signature: Uint8List(64), + ), + ], + ); + + // test whether all signatures are invalid + final isValidAll = await coseSign.verifyAll(verifiers: [signerVerifier]); + expect(isValidAll, isFalse); + + // test whether all signatures are invalid + final isValid = await coseSign.verify(verifier: signerVerifier); + expect(isValid, isFalse); + }); + }); +} + +final class _SignerVerifier + implements CatalystCoseSigner, CatalystCoseVerifier { + final SignatureAlgorithm _algorithm; + final SimpleKeyPair _keyPair; + + const _SignerVerifier(this._algorithm, this._keyPair); + + @override + StringOrInt? get alg => const IntValue(CoseValues.eddsaAlg); + + @override + Future get kid async { + final pk = await _keyPair.extractPublicKey(); + return Uint8List.fromList(pk.bytes); + } + + @override + Future sign(Uint8List data) async { + final signature = await _algorithm.sign(data, keyPair: _keyPair); + return Uint8List.fromList(signature.bytes); + } + + @override + Future verify(Uint8List data, Uint8List signature) async { + final publicKey = await _keyPair.extractPublicKey(); + return _algorithm.verify( + data, + signature: Signature( + signature, + publicKey: SimplePublicKey(publicKey.bytes, type: KeyPairType.ed25519), + ), + ); + } +} diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_menu_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_menu_example.dart index 3dcfa8fb0fa..647b15f6b38 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_menu_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_menu_example.dart @@ -13,7 +13,7 @@ class VoicesMenuExample extends StatefulWidget { } class _VoicesMenuExampleState extends State { - int? _selectedItemId; + String? _selectedItemId; @override void dispose() { @@ -45,9 +45,9 @@ class _VoicesMenuExampleState extends State { }, selectedItemId: _selectedItemId, items: const [ - VoicesNodeMenuItem(id: 0, label: 'Start'), - VoicesNodeMenuItem(id: 1, label: 'Vote'), - VoicesNodeMenuItem(id: 2, label: 'Results'), + VoicesNodeMenuItem(id: '0', label: 'Start'), + VoicesNodeMenuItem(id: '1', label: 'Vote'), + VoicesNodeMenuItem(id: '2', label: 'Results'), ], ), ].separatedBy(const SizedBox(height: 12)).toList(), diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_modals_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_modals_example.dart index c7e46e1ff91..9d6286621e8 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_modals_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_modals_example.dart @@ -33,9 +33,6 @@ class VoicesModalsExample extends StatelessWidget { onUpload: (_) async { await Future.delayed(const Duration(seconds: 2)); }, - onCancel: () => debugPrint( - 'onCancel, we can cancel upload here', - ), ); if (file != null) { debugPrint('uploaded file: ${file.name}'); diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_proposal_card_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_proposal_card_example.dart index aa595b9fbe7..5a22344c80d 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_proposal_card_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_proposal_card_example.dart @@ -1,9 +1,8 @@ import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; -import 'package:catalyst_voices/widgets/cards/pending_proposal_card.dart'; -import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices/widgets/cards/proposal_card.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; final _description = """ @@ -29,11 +28,11 @@ class VoicesProposalCardExample extends StatelessWidget { spacing: 16, runSpacing: 16, children: [ - FundedProposalCard( + ProposalCard( image: VoicesAssets.images.proposalBackground1, proposal: FundedProposal( id: 'f14/1', - fund: 'F14', + campaignName: 'F14', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', fundedDate: DateTime(2025, 1, 28), @@ -42,11 +41,11 @@ class VoicesProposalCardExample extends StatelessWidget { description: _description, ), ), - PendingProposalCard( + ProposalCard( image: VoicesAssets.images.proposalBackground2, proposal: PendingProposal( id: 'f14/2', - fund: 'F14', + campaignName: 'F14', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', lastUpdateDate: DateTime.now().minusDays(2), diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart index 059f16ab035..7b97322ed0b 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_text_field_example.dart @@ -85,7 +85,9 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: VoicesTextFieldValidationResult.success(), + validator: (value) { + return const VoicesTextFieldValidationResult.success(); + }, onFieldSubmitted: (value) {}, ), ), @@ -99,8 +101,11 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: - VoicesTextFieldValidationResult.warning('Warning message'), + validator: (value) { + return const VoicesTextFieldValidationResult.warning( + 'Warning message', + ); + }, onFieldSubmitted: (value) {}, ), ), @@ -114,8 +119,11 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: - VoicesTextFieldValidationResult.error('Error message'), + validator: (value) { + return const VoicesTextFieldValidationResult.error( + 'Error message', + ); + }, onFieldSubmitted: (value) {}, ), ), @@ -129,7 +137,9 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: VoicesTextFieldValidationResult.success(), + validator: (value) { + return const VoicesTextFieldValidationResult.success(); + }, enabled: false, onFieldSubmitted: (value) {}, ), @@ -144,8 +154,11 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: - VoicesTextFieldValidationResult.warning('Warning message'), + validator: (value) { + return const VoicesTextFieldValidationResult.warning( + 'Warning message', + ); + }, enabled: false, onFieldSubmitted: (value) {}, ), @@ -160,8 +173,11 @@ class _VoicesTextFieldExampleState extends State { hintText: 'Hint text', ), maxLength: 200, - validator: - VoicesTextFieldValidationResult.error('Error message'), + validator: (value) { + return const VoicesTextFieldValidationResult.error( + 'Error message', + ); + }, enabled: false, onFieldSubmitted: (value) {}, ), diff --git a/catalyst_voices/utilities/uikit_example/pubspec.yaml b/catalyst_voices/utilities/uikit_example/pubspec.yaml index 153a9da1858..f2347d15c6c 100644 --- a/catalyst_voices/utilities/uikit_example/pubspec.yaml +++ b/catalyst_voices/utilities/uikit_example/pubspec.yaml @@ -22,6 +22,8 @@ dependencies: path: ../../packages/internal/catalyst_voices_models catalyst_voices_shared: path: ../../packages/internal/catalyst_voices_shared + catalyst_voices_view_models: + path: ../../packages/internal/catalyst_voices_view_models collection: ^1.18.0 cupertino_icons: ^1.0.6 flutter: diff --git a/cspell.json b/cspell.json index fb377102700..2a698c55eb0 100644 --- a/cspell.json +++ b/cspell.json @@ -175,6 +175,7 @@ "styles.min.css", "web-components.min.js", "**/generated/**", + "**/openapi/**", "**/GeneratedPluginRegistrant.swift", "catalyst_voices/packages/libs/catalyst_key_derivation/cargokit/**", "catalyst_voices/utilities/remote_widgets/example/**/**", diff --git a/docs/src/architecture/08_concepts/signed_document_metadata/.pages b/docs/src/architecture/08_concepts/signed_document_metadata/.pages deleted file mode 100644 index 418ac0f62d5..00000000000 --- a/docs/src/architecture/08_concepts/signed_document_metadata/.pages +++ /dev/null @@ -1,3 +0,0 @@ -title: Signed Document Metadata -arrange: - - metadata.md diff --git a/docs/src/architecture/08_concepts/signed_document_metadata/metadata.md b/docs/src/architecture/08_concepts/signed_document_metadata/metadata.md deleted file mode 100644 index 35976af3c8d..00000000000 --- a/docs/src/architecture/08_concepts/signed_document_metadata/metadata.md +++ /dev/null @@ -1,296 +0,0 @@ ---- -Title: Signed Document Types and Metadata -Category: Catalyst -Status: Proposed -Authors: - - Steven Johnson -Implementors: - - Catalyst Fund 14 -Discussions: [] -Created: 2024-12-03 -License: CC-BY-4.0 ---- - -* [Abstract](#abstract) -* [Motivation: why is this CIP necessary?](#motivation-why-is-this-cip-necessary) -* [Specification](#specification) - * [Document Type : `type`](#document-type--type) - * [Document Type Definitions](#document-type-definitions) - * [Document Templates](#document-templates) - * [Document Content Templates](#document-content-templates) - * [Document Metadata](#document-metadata) - * [Document ID : `id`](#document-id--id) - * [Document Version : `ver`](#document-version--ver) - * [Document Reference : `ref`](#document-reference--ref) - * [Template Reference : `template`](#template-reference--template) - * [Document Reference : `reply`](#document-reference--reply) - * [Document Reference : `section`](#document-reference--section) - * [Authorized Collaborators : `collabs`](#authorized-collaborators--collabs) - * [Document Type Specifications](#document-type-specifications) - * [Proposal Template](#proposal-template) - * [Comment Template](#comment-template) - * [Proposal Document](#proposal-document) - * [Comment Document](#comment-document) -* [Reference Implementation](#reference-implementation) -* [Rationale: how does this CIP achieve its goals?](#rationale-how-does-this-cip-achieve-its-goals) -* [Path to Active](#path-to-active) - * [Acceptance Criteria](#acceptance-criteria) - * [Implementation Plan](#implementation-plan) -* [Copyright](#copyright) - -## Abstract - -Project Catalyst both produces and consumes documents of data. -To ensure the document is authoritative, all documents of this kind will be signed. -In addition to the document contents, documents will also include metadata which describes -what kind of document it is, and how the document relates to other documents. - -## Motivation: why is this CIP necessary? - -As we decentralize project catalyst, it will be required to unambiguously identify who produced a -document, and the purpose of the document. - -A signed document specification will detail the structure of a signed document, this specification -is just the metadata that structure will carry for different kinds of documents. - -## Specification - -### Document Type : `type` - -Each document will have a document type identifier called `type`. -This identifier will be a [CBOR] Encoded [UUID Byte String]. -Only [UUID] V4 is supported and used. - -The document types and their identifiers are listed here: - -#### Document Type Definitions - -##### Document Templates - -Document Templates are themselves signed documents, the templates currently defined or planned are: - -| [UUID] | [CBOR] | Type Description | Payload Type | -| --- | --- | --- | --- | -| 0ce8ab38-9258-4fbc-a62e-7faa6e58318f | `37(h'0ce8ab3892584fbca62e7faa6e58318f')` | Proposal Template | [Brotli] Compressed [JSON Schema] | -| 0b8424d4-ebfd-46e3-9577-1775a69d290c | `37(h'0b8424d4ebfd46e395771775a69d290c')` | Comment Template | [Brotli] Compressed [JSON Schema] | -| ebe5d0bf-5d86-4577-af4d-008fddbe2edc | `37(h'ebe5d0bf5d864577af4d008fddbe2edc')` | Review Template | [Brotli] Compressed [JSON Schema] | -| 65b1e8b0-51f1-46a5-9970-72cdf26884be | `37(h'65b1e8b051f146a5997072cdf26884be')` | Category Parameters Template | [Brotli] Compressed [JSON Schema] | -| 7e8f5fa2-44ce-49c8-bfd5-02af42c179a3 | `37(h'7e8f5fa244ce49c8bfd502af42c179a3')` | Campaign Parameters Template | [Brotli] Compressed [JSON Schema] | -| fd3c1735-80b1-4eea-8d63-5f436d97ea31 | `37(h'fd3c173580b14eea8d635f436d97ea31')` | Brand Parameters Template | [Brotli] Compressed [JSON Schema] | - -##### Document Content Templates - -Document Contents are signed documents, and are typically produced in accordance with a template document. - -| [UUID] | [CBOR] | Type Description | Payload Type | -| --- | --- | --- | --- | -| 7808d2ba-d511-40af-84e8-c0d1625fdfdc | `37(h'7808d2bad51140af84e8c0d1625fdfdc')` | Proposal Document | [Brotli] Compressed [JSON] | -| b679ded3-0e7c-41ba-89f8-da62a17898ea | `37(h'b679ded30e7c41ba89f8da62a17898ea')` | Comment Document | [Brotli] Compressed [JSON] | -| e4caf5f0-098b-45fd-94f3-0702a4573db5 | `37(h'e4caf5f0098b45fd94f30702a4573db5')` | Review Document | [Brotli] Compressed [JSON] | -| 48c20109-362a-4d32-9bba-e0a9cf8b45be | `37(h'48c20109362a4d329bbae0a9cf8b45be')` | Category Parameters Document | [Brotli] Compressed [JSON] | -| 0110ea96-a555-47ce-8408-36efe6ed6f7c | `37(h'0110ea96a55547ce840836efe6ed6f7c')` | Campaign Parameters Document | [Brotli] Compressed [JSON] | -| 3e4808cc-c86e-467b-9702-d60baa9d1fca | `37(h'3e4808ccc86e467b9702d60baa9d1fca')` | Brand Parameters Document | [Brotli] Compressed [JSON] | -| 5e60e623-ad02-4a1b-a1ac-406db978ee48 | `37(h'5e60e623ad024a1ba1ac406db978ee48')` | Proposal Action Document | *TBD* | - -### Document Metadata - -Documents will contain metadata which allows the document to be categorized, versioned and linked. -This data does not properly belong inside the document, -but is critical to ensure the document is properly referenced and indexable. - -#### Document ID : `id` - - -**REQUIRED, PROTECTED HEADER** - - -Every document will have a unique document ID, this is to allow the same document to be referenced. -All documents with the same `doc_id` are considered versions of the same document. -However, `id` uniqueness is only guaranteed on first use. - -If the same `id` is used, by unauthorized publishers, the document is invalid. - -The `id` is a [ULID]. -It will be encoded using [ULID CBOR Encoding]. - -The first time a document is created, it will be assigned by the creator a new `id` which must -be well constructed. - -* The time must be the time the document was first created. -* The random value must be truly random. - -Creating `id` this way ensures there are no collisions, and they can be independently created without central co-ordination. - -*Note: All documents are signed, the first creation of a `id` assigns that `id` to the creator and any assigned collaborators. -A Signed Document that is not signed by the creator, or an assigned collaborator, is invalid. -There is no reasonable way a `id` can collide accidentally. -Therefore, detection of invalid `id`s published by unauthorized publishers, could result in anti-spam -or system integrity mitigations being triggered. -This could result in all actions in the system being blocked by the offending publisher, -including all otherwise legitimate publications by the same author being marked as fraudulent.* - -#### Document Version : `ver` - - -**REQUIRED, PROTECTED HEADER** - - -Every document in the system will be versioned. -There can, and probably will, exist multiple versions of the same document. - -The `ver` is a [ULID]. -It will be encoded using [ULID CBOR Encoding]. - -The initial `ver` assigned the first time a document is published will be identical to the [`id`](#document-id--id). -Subsequent versions will retain the same [`id`](#document-id--id) and will create a new `ver`, -following best practice for creating a new [ULID]. - -#### Document Reference : `ref` - - -**OPTIONAL, PROTECTED HEADER** - - -This is a reference to another document. -The purpose of the `ref` will vary depending on the document [`type`](#document-type--type). - -The `ref` can be either a single [ULID] or a [CBOR] Array of Two [ULID]. - -If the `ref` is a single [ULID], it is a reference to the document of that [`id`](#document-id--id). -If the `ref` is a [CBOR] array, it has the form `[,]` where: - -* `` = the [ULID] of the referenced documents [`id`](#document-id--id) -* `` = the [ULID] of the referenced documents [`ver`](#document-version--ver). - -#### Template Reference : `template` - - -**REQUIRED, IF THE DOCUMENT WAS FORMED FROM A TEMPLATE, PROTECTED HEADER** - - -If the document was formed from a template, this is a reference to that template document. -The format is the same as the [CBOR] Array form of [`ref`](#document-reference--ref). - -It is invalid not to reference the template that formed a document. -If this is missing from such documents, the document will itself be considered invalid. - -Template references must explicitly reference both the Template Document ID, and Version. - -#### Document Reference : `reply` - - -**OPTIONAL, PROTECTED HEADER** - - -This is a reply to another document. -The format is the same as the [CBOR] Array form of [`ref`](#document-reference--ref). - -`reply` is always referencing a document of the same type, and that document must `ref` reference the same document `id`. -However, depending on the document type, it may reference a different `ver` of that `id`. - -#### Document Reference : `section` - - -**OPTIONAL, PROTECTED HEADER** - - -This is a reference to a section of a document. -It is a CBOR String, and contains a [JSON Path] identifying the section in question. - -Because this metadata field uses [JSON Path], it can only be used to reference a document whose payload is [JSON]. - -#### Authorized Collaborators : `collabs` - - -**OPTIONAL, PROTECTED HEADER** - - -This is a list of entities other than the original author that may also publish versions of this document. -This may be updated by the original author, or any collaborator that is given "author" privileges. - -The latest `collabs` list in the latest version, published by an authorized author is the definitive -list of allowed collaborators after that point. - -The `collabs` list is a [CBOR] Array. -The contents of the array are TBD. - -However, they will contain enough information such that each collaborator can be uniquely identified and validated. - -*Note: An Author can unilaterally set the `collabs` list to any list of collaborators. -It does NOT mean that the listed collaborators have agreed to collaborate, only that the Author -gives them permission to.* - -This list can impact actions that can be performed by the `Proposal Action Document`. - -### Document Type Specifications - -Note, not all document types are currently specified. - -#### Proposal Template - -This document provides the template structure which a Proposal must be formatted to, and validated against. - -#### Comment Template - -This document pr provides the template structure which a Comment must be formatted to, and validated against. - -#### Proposal Document - -This is a document, formatted against the referenced proposal template, which defines a proposal which may be submitted -for consideration under one or more brand campaign categories. - -The brand, campaign and category are not part of the document because the document can exist outside this boundary. -They are defined when a specific document is submitted for consideration. - -#### Comment Document - -This is a document which provides a comment against a particular proposal. -Because comments are informed by a particular proposals version, they *MUST* contain a [`ref`](#document-reference--ref) - -They may *OPTIONALLY* also contain a [`reply`](#document-reference--reply) metadata field, which is a reference to another comment, -where the comment is in reply to the referenced comment. - -Comments may only [`reply`](#document-reference--reply) to a single other comment document. -The referenced `comment` must be for the same proposal [`id`](#document-id--id), -but can be for a different proposal [`ver`](#document-version--ver). - -Comments may *OPTIONALLY* also contain a [`subsection`](#document-reference--section) field, -when the comment only applies to a specific section to the document being commented upon, -and not the entire document. - -## Reference Implementation - -The first implementation will be Catalyst Voices. - -*TODO: Generate a set of test vectors which conform to this specification.* - -## Rationale: how does this CIP achieve its goals? - -By specifying metadata attached to signed documents and unambiguous document type identifiers, we allow -documents to be broadcast over insecure means, and for their meaning and relationships to remain intact. - -The Document itself is soft, but the metadata provides concrete relationships between documents. - -## Path to Active - -### Acceptance Criteria - -Working Implementation before Fund 14. - -### Implementation Plan - -Fund 14 project catalyst will deploy this scheme for Key derivation.> - -## Copyright - -This document is licensed under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/legalcode). - -[UUID Byte String]: https://github.com/lucas-clemente/cbor-specs/blob/master/uuid.md -[JSON Schema]: https://json-schema.org/draft-07 -[Brotli]: https://datatracker.ietf.org/doc/html/rfc7932 -[JSON]: https://datatracker.ietf.org/doc/html/rfc7159 -[ULID]: https://github.com/ulid/spec -[ULID CBOR Encoding]: https://github.com/input-output-hk/catalyst-voices/blob/main/docs/src/catalyst-standards/cbor_tags/ulid.md -[CBOR]: https://datatracker.ietf.org/doc/html/rfc8610 -[UUID]: https://www.rfc-editor.org/rfc/rfc9562.html -[JSON Path]: https://datatracker.ietf.org/doc/html/rfc9535 diff --git a/docs/src/architecture/09_architecture_decisions/0009-uuid7-vs-ulid.md b/docs/src/architecture/09_architecture_decisions/0009-uuid7-vs-ulid.md new file mode 100644 index 00000000000..7c300c01cf0 --- /dev/null +++ b/docs/src/architecture/09_architecture_decisions/0009-uuid7-vs-ulid.md @@ -0,0 +1,82 @@ +--- + title: 0009 UUIDv7 vs ULID + adr: + author: Assistant + created: 17-Oct-2024 + status: proposed + tags: + - uuid + - identifiers +--- + +## Context + +The system needs globally unique identifiers for various entities and resources. +Currently, we use ULIDs (Universally Unique Lexicographically Sortable Identifiers) in some parts of the system. +With the recent standardization of UUIDv7 through [RFC 9562], we need to evaluate our identifier strategy. + +## Assumptions + +* We need time-ordered, globally unique identifiers for certain system components +* We need non-time-ordered unique identifiers for type identification +* Our systems must interact with various external systems and databases + +## Decision + +We will: + +* Use UUIDv7 for all time-bound unique identifiers (replacing ULID usage) +* Use UUIDv4 for type-specific identifiers where time ordering is not required +* Phase out ULID usage in favor of UUIDv7 + +### Rationale + +* UUIDv7 provides similar benefits to ULID: + * Time-ordered + * Globally unique + * Contains timestamp information +* UUIDv7 has several advantages over ULID: + * Standardized through RFC 9562 + * Wider system compatibility (native UUID support) + * No special encoding requirements + * Clear disambiguation from UUIDv4 (allowing two distinct identifier spaces) + * Eliminates need for custom ULID code + * Better database indexing support +* Standard UUID implementations are widely available across programming languages and platforms + +### Implementation Guidelines + +* New features should exclusively use: + * UUIDv7 for time-ordered identifiers + * UUIDv4 for non-time-ordered type identifiers +* Existing ULID implementations should be migrated to UUIDv7 during regular maintenance cycles +* Database schemas should use UUID types rather than string/custom types +* API contracts should specify UUID format requirements in their documentation + +## Risks + +* Migration from existing ULID implementations requires careful planning +* Some existing data may need conversion +* Team members need to understand when to use UUIDv4 vs UUIDv7 + +## Consequences + +### Positive + +* Improved system interoperability +* Reduced custom code maintenance +* Better alignment with industry standards +* Simplified database operations +* Two distinct identifier spaces (v4 and v7) for different use cases + +### Negative + +* Migration effort required for existing ULID implementations +* Team training needed for proper UUID version selection + +## More Information + +* [RFC 9562 - UUIDv7 Specification](https://datatracker.ietf.org/doc/rfc9562/) +* [ULID Specification](https://github.com/ulid/spec) + +[RFC 9562]: https://datatracker.ietf.org/doc/rfc9562/ From 3f7e2a09fe4426dc5dd04f056f909ea9b3a1117e Mon Sep 17 00:00:00 2001 From: Nathan Bogale Date: Tue, 31 Dec 2024 21:28:14 +0300 Subject: [PATCH 23/25] Fix: generic schema folder content cleanup --- .../wallet-automation/package-lock.json | 5 + .../wallet-automation/package.json | 1 + .../assets/js/package-lock.json | 20 + .../assets/js/package.json | 7 +- ...38-9258-4fbc-a62e-7faa6e58318f.schema.json | 274 ----- .../proposal.F14.example.json | 21 - .../F14-Generic/extra-definitions.txt | 1079 ----------------- utilities/wallet-tester/package-lock.json | 5 + utilities/wallet-tester/package.json | 1 + 9 files changed, 37 insertions(+), 1376 deletions(-) create mode 100644 catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package-lock.json delete mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json delete mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/proposal.F14.example.json delete mode 100644 docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/extra-definitions.txt diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json index 3719a91ad85..e32f883ee72 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@tomjs/unzip-crx": "^1.1.3", "@types/node-fetch": "^2.6.11", + "catalyst-voices": "file:", "dotenv": "^16.3.1", "fs-extra": "^11.2.0", "install": "^0.13.0", @@ -71,6 +72,10 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/catalyst-voices": { + "resolved": "", + "link": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json index af26c6d9bef..5c87101f7f5 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json @@ -27,6 +27,7 @@ "dependencies": { "@tomjs/unzip-crx": "^1.1.3", "@types/node-fetch": "^2.6.11", + "catalyst-voices": "file:", "dotenv": "^16.3.1", "fs-extra": "^11.2.0", "install": "^0.13.0", diff --git a/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package-lock.json b/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package-lock.json new file mode 100644 index 00000000000..ade0a57615b --- /dev/null +++ b/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "catalyst_key_derivation", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "catalyst_key_derivation", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "catalyst_key_derivation": "file:" + } + }, + "node_modules/catalyst_key_derivation": { + "resolved": "", + "link": true + } + } +} diff --git a/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package.json b/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package.json index 79b195a6ebb..99b4555178f 100644 --- a/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package.json +++ b/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package.json @@ -11,5 +11,8 @@ "catalyst_key_derivation.js" ], "browser": "catalyst_key_derivation.js", - "homepage": "https://input-output-hk.github.io/catalyst-voices" -} \ No newline at end of file + "homepage": "https://input-output-hk.github.io/catalyst-voices", + "dependencies": { + "catalyst_key_derivation": "file:" + } +} diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json deleted file mode 100644 index 831508f8194..00000000000 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ /dev/null @@ -1,274 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Catalyst Fund 14 Base Proposal Template", - "description": "A structured template for creating Fund 14 proposals", - "definitions": { - "singleLineTextEntry": { - "type": "string", - "contentMediaType": "text/plain", - "pattern": "^.*$" - }, - "multiLineTextEntry": { - "type": "string", - "contentMediaType": "text/plain", - "pattern": "^[\\S\\s]$" - }, - "dropDownSingleSelect": { - "type": "string", - "contentMediaType": "text/plain", - "pattern": "^.*$", - "format": "dropDownSingleSelect" - }, - "tokenValueCardanoADA": { - "type": "integer", - "format": "token:cardano:ada" - }, - "durationInMonths": { - "type": "integer", - "format": "datetime:duration:months" - }, - "agreementConfirmation": { - "type": "boolean", - "format": "checkbox", - "default": false, - "const": true - }, - "yesNoChoice": { - "type": "boolean", - "format": "yes/no", - "default": false - }, - "uri": { - "type": "string", - "format": "uri", - "contentMediaType": "text/plain", - "maxLength": 1024 - }, - "uriList": { - "type": "array", - "format": "uriList", - "uniqueItems": true, - "default": [], - "items": { - "$ref": "#/definitions/uri" - } - } - }, - "type": "object", - "properties": { - "$schema": { - "type": "string", - "format": "path", - "const": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", - "readOnly": true - }, - "general": { - "type": "object", - "properties": { - "title": { - "$ref": "#/definitions/singleLineTextEntry", - "title": "Proposal title", - "description": "

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.


The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

", - "maxLength": 80, - "minLength": 0 - }, - "applicant": { - "$ref": "#/definitions/singleLineTextEntry", - "title": "Name and surname of main applicant", - "description": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", - "maxLength": 80, - "minLength": 0 - }, - "applicant_type": { - "$ref": "#/definitions/dropDownSingleSelect", - "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", - "description": "

Please select from one of the following:

", - "enum": [ - "Individual", - "Entity (Incorporated)", - "Entity (Not Incorporated)" - ], - "default": "Individual" - }, - "co-proposers": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "Co-proposers and additional applicants", - "description": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals / accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers.


IMPORTANT - A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund13 rules for added detail.

", - "maxLength": 1024, - "minLength": 0 - }, - "requested_funds": { - "$ref": "#/definitions/tokenValueCardanoADA", - "title": "Requested funds in ada", - "description": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:


Minimum Funding Amount per proposal:

  • Cardano Open: ₳15,000
  • Cardano Uses Cases: ₳15,000
  • Cardano Partners: ₳500,000


Maximum Funding Amount per proposal:

  • Cardano Open: 
  • Developers (technical): ₳200,000
  • Ecosystem (non-technical): ₳100,000
  • Cardano Uses Cases:
  • Concept: ₳150,000
  • Product: ₳500,000
  • Cardano Partners:
  • Enterprise R&D: ₳2,000,000 
  • Growth & Acceleration: ₳2,000,000
", - "minimum": 1, - "maximum": 18446744073709551615 - }, - "duration": { - "$ref": "#/definitions/durationInMonths", - "title": "Please specify how many months you expect your project to last (from 2-12 months)", - "description": "

Minimum 2 months - Maximum 12 months.


The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months.


If you believe your project will take longer than 12 months, consider reducing the project’s scope so that it becomes achievable within 12 months.


If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", - "minimum": 2, - "maximum": 12 - }, - "translated": { - "$ref": "#/definitions/yesNoChoice", - "title": "Please indicate if your proposal has been auto-translated into English from another language", - "description": "

YES/NO - Tick YES so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections.


You can either link a document with your proposal in its original language OR provide your response in your native language after the English language in each question if you wish.


Tick NO if your proposal has not been auto-translated into English from another language.

" - }, - "problem": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "What is the problem you want to solve? (200-character limit including spaces)", - "description": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

", - "maxLength": 200, - "minLength": 1 - }, - "solution": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "Summarize your solution to the problem (200-character limit including spaces)", - "description": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a...'


Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how'.


This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

", - "maxLength": 200, - "minLength": 1 - }, - "links": { - "$ref": "#/definitions/uriList", - "title": "Website / GitHub repository, White paper, Marketing or any other relevant link", - "description": "

Here, provide links to yours or your partner organization’s website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal.


Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere.


If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include ‘https’. Without these steps, the form will not be submittable and show errors.

", - "minItems": 0, - "maxItems": 3 - }, - "dependencies": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "If you have any dependencies then, please describe what the dependency is and why you believe it is essential for your project’s delivery. If NO, please write “No dependencies.”", - "description": "

Here you should list any dependencies and prerequisites for your project’s success. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", - "maxLength": 1024, - "minLength": 0 - }, - "open_source": { - "$ref": "#/definitions/yesNoChoice", - "title": "Will your project’s output/s be fully open source?", - "description": "

Open source refers to something people can modify and share because its design is publicly accessible. 


Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software.

" - }, - "license_info": { - "$ref": "#/definitions/multiLineTextEntry", - "title": "[GENERAL] Please provide here more information on the open source status of your project outputs", - "description": "

If you answered YES to the above question:


If declaring the project is open source in the application form, the project should be open source-available throughout the entire lifecycle of the project with a declared open-source repository.


Please indicate here the type of license you intend to use for open source and provide any further information you feel is relevant to the open source status of your project outputs. 


If only certain elements of your code will be open source please clarify which elements will be open source here. 


If you answered NO to the above question, please give further details as to why your projects outputs will not be open source.

", - "maxLength": 1024, - "minLength": 0 - } - }, - "required": [ - "title", - "applicant", - "applicant_type", - "requested_funds", - "duration", - "translated", - "problem", - "solution", - "open_source", - "license_info" - ] - }, - "metadata": { - "title": "Horizons", - "description": "

Please choose the most relevant category group and tag related to the outcomes of your proposal. Can select only one group and one tag.

", - "format": "nested-tag-selector", - "oneOf": [ - { - "type": "object", - "properties": { - "group": { - "type": "string", - "const": "Governance" - }, - "tag": { - "type": "string", - "enum": [ - "Governance", - "DAO" - ] - } - } - }, - { - "type": "object", - "properties": { - "group": { - "type": "string", - "const": "Education" - }, - "tag": { - "type": "string", - "enum": [ - "Education", - "Learn to Earn", - "Training", - "Translation" - ] - } - } - }, - { - "group": { - "type": "string", - "const": "Community & Outreach" - }, - "tag": { - "type": "string", - "enum": [ - "Connected Community", - "Community", - "Community Outreach", - "Social Media" - ] - } - }, - { - "group": { - "type": "string", - "const": "Development & Tools" - }, - "tag": { - "type": "string", - "enum": [ - "Developer Tools", - "L2", - "Infrastructure", - "Analytics", - "AI", - "Research", - "UTXO", - "P2P" - ] - } - } - ] - }, - "agreements": { - "type": "object", - "properties": { - "fund_rules": { - "$ref": "#/definitions/agreementConfirmation", - "title": "Fund Rules:", - "description": "

By submitting a proposal to Project Catalyst Fund13, I confirm that I have read and agree to be bound by the Fund Rules.

" - }, - "terms_and_conditions": { - "$ref": "#/definitions/agreementConfirmation", - "title": "Terms and Conditions:", - "description": "

By submitting a proposal to Project Catalyst Fund13, I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions.

" - }, - "privacy_policy": { - "$ref": "#/definitions/agreementConfirmation", - "title": "Privacy Policy: ", - "description": "

I acknowledge and agree that any data I share in connection with my participation in Project Catalyst Fund13 will be collected, stored, used and processed in accordance with the Catalyst FC’s Privacy Policy.

" - } - }, - "required": [ - "fund_rules", - "terms_and_conditions", - "privacy_policy" - ] - } - } -} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/proposal.F14.example.json b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/proposal.F14.example.json deleted file mode 100644 index 13813ee0f09..00000000000 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic-Steven/proposal.F14.example.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "./0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json", - "general": { - "title": "Single line plain text .....", - "links": [ - "http://notauri", - "ftp://some_old_site", - "cardano://some-block" - ], - "applicant_type": "Individual", - }, - "metadata": { - "group": "Education", - "tag": "Learn to Earn" - }, - "agreements": { - "fund_rules": true, - "terms_and_conditions": true, - "privacy_policy": true - } -} \ No newline at end of file diff --git a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/extra-definitions.txt b/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/extra-definitions.txt deleted file mode 100644 index 8278ccf1d2f..00000000000 --- a/docs/src/architecture/08_concepts/document_templates/proposal/F14-Generic/extra-definitions.txt +++ /dev/null @@ -1,1079 +0,0 @@ -Not in the F13 proposal, or the F14 base document? - - - "proposalDetails": { - "type": "object", - "title": "Proposal Details", - "description": "Detailed information about your proposal's solution, impact, and feasibility", - "properties": { - "solution": { - "type": "object", - "title": "Solution Description", - "description": "Detailed description of your proposed solution", - "x-guidance": "

YOUR PROJECT AND SOLUTION

How you write this section will depend on what type of proposal you are writing. You might want to include details on:

  • How do you perceive the problem you are solving?
  • What are your reasons for approaching it in the way that you have?
  • Who will your project engage?
  • How will you demonstrate or prove your impact?

Explain what is unique about your solution, who will benefit, and why this is important to Cardano.

", - "properties": { - "description": { - "type": "string", - "title": "Solution Description", - "description": "Provide a comprehensive description of your proposed solution", - "minLength": 100, - "maxLength": 2000, - "examples": [ - "Our solution involves developing a decentralized education platform that will..." - ], - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "uniqueValue": { - "type": "string", - "title": "Unique Value Proposition", - "description": "What makes your solution unique and innovative?", - "minLength": 50, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "targetAudience": { - "type": "array", - "title": "Target Audience", - "description": "Who will benefit from your solution?", - "items": { - "type": "string", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 5, - "uniqueItems": true - }, - "implementation": { - "type": "string", - "title": "Implementation Approach", - "description": "How will you implement your solution?", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "description", - "uniqueValue", - "targetAudience", - "implementation" - ] - }, - "impact": { - "type": "object", - "title": "Project Impact", - "description": "Define and measure the impact of your project", - "x-guidance": "

Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:

  • In what way will the success of your project bring value to the Cardano Community?
  • How will you measure this impact?
  • How will you share the outputs and opportunities that result from your project?
", - "properties": { - "communityBenefit": { - "type": "string", - "title": "Community Benefit", - "description": "How will the Cardano community benefit from your project?", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "metrics": { - "type": "array", - "title": "Impact Metrics", - "description": "Specific metrics to measure project success", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Metric Name", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "description": { - "type": "string", - "title": "Metric Description", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "target": { - "type": "string", - "title": "Target Value", - "minLength": 1, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "measurement": { - "type": "string", - "title": "Measurement Method", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "name", - "description", - "target", - "measurement" - ] - }, - "minItems": 2, - "maxItems": 5 - }, - "outputs": { - "type": "array", - "title": "Project Outputs", - "description": "Tangible outputs and deliverables from the project", - "items": { - "type": "string", - "minLength": 10, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 10, - "uniqueItems": true - } - }, - "required": [ - "communityBenefit", - "metrics", - "outputs" - ] - }, - "capability": { - "type": "object", - "title": "Capability & Feasibility", - "description": "Demonstrate your ability to deliver the project successfully", - "x-guidance": "

Please describe your existing capabilities that demonstrate how and why you believe you're best suited to deliver this project? Please include the steps or processes that demonstrate that you can be trusted to manage funds properly.

", - "properties": { - "teamExperience": { - "type": "string", - "title": "Team Experience", - "description": "Describe your team's relevant experience and capabilities", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "feasibilityApproach": { - "type": "string", - "title": "Feasibility Approach", - "description": "How will you validate the feasibility of your approach?", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "fundManagement": { - "type": "string", - "title": "Fund Management", - "description": "How will you ensure proper management and accountability of funds?", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "teamExperience", - "feasibilityApproach", - "fundManagement" - ] - } - }, - "required": [ - "solution", - "impact", - "capability" - ] -}, -"milestones": { - "type": "object", - "title": "Project Milestones", - "description": "Detailed project milestones and deliverables", - "x-guidance": "

A clear set of milestones and acceptance criteria will demonstrate your capability to deliver the project as proposed. More guidance on submitting milestones as part of your project proposal can be found here

For Grant Amounts of up to 75k ada, at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)

For Grant Amounts over 75k ada up to 150k ada, at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)

For Grant Amounts over 150k ada up to 300k ada, at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)

For Grant Amounts exceeding 300k ada, at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)

", - "properties": { - "milestonesConfig": { - "type": "object", - "title": "Milestones Configuration", - "description": "Configuration for number of milestones", - "properties": { - "grantAmount": { - "type": "number", - "title": "Grant Amount in ADA", - "description": "Total grant amount requested in ADA", - "minimum": 0, - "maximum": 1000000 - }, - "numberOfMilestones": { - "type": "integer", - "title": "Number of Milestones", - "description": "Total number of milestones including the final milestone", - "minimum": 2, - "maximum": 6 - } - }, - "required": [ - "grantAmount", - "numberOfMilestones" - ] - }, - "milestonesList": { - "type": "array", - "title": "List of Milestones", - "description": "Detailed description of each project milestone", - "items": { - "type": "object", - "title": "Milestone", - "properties": { - "title": { - "type": "string", - "title": "Milestone Title", - "description": "Short, descriptive title for the milestone", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "description": { - "type": "string", - "title": "Milestone Description", - "description": "Detailed description of what this milestone entails", - "minLength": 50, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "deliverables": { - "type": "array", - "title": "Deliverables", - "description": "Specific outputs and deliverables for this milestone", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Deliverable Name", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "description": { - "type": "string", - "title": "Deliverable Description", - "minLength": 20, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "type": { - "type": "string", - "enum": [ - "Documentation", - "Software", - "Report", - "Presentation", - "Video", - "Other" - ] - } - }, - "required": [ - "name", - "description", - "type" - ] - }, - "minItems": 1, - "maxItems": 5 - }, - "acceptanceCriteria": { - "type": "array", - "title": "Acceptance Criteria", - "description": "Specific criteria that must be met to consider this milestone complete", - "items": { - "type": "string", - "minLength": 10, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 5 - }, - "evidenceOfCompletion": { - "type": "array", - "title": "Evidence of Completion", - "description": "How will you demonstrate that this milestone is complete?", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "title": "Evidence Type", - "enum": [ - "Code Repository", - "Documentation", - "Demo Video", - "Test Results", - "Metrics Report", - "User Feedback", - "Other" - ] - }, - "description": { - "type": "string", - "title": "Evidence Description", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "type", - "description" - ] - }, - "minItems": 1, - "maxItems": 3 - }, - "timeline": { - "type": "object", - "title": "Timeline", - "properties": { - "startDate": { - "type": "string", - "title": "Start Date", - "format": "date" - }, - "endDate": { - "type": "string", - "title": "End Date", - "format": "date" - }, - "durationInWeeks": { - "type": "integer", - "title": "Duration in Weeks", - "minimum": 1, - "maximum": 52 - } - }, - "required": [ - "startDate", - "endDate", - "durationInWeeks" - ] - }, - "budget": { - "type": "object", - "title": "Milestone Budget", - "properties": { - "amount": { - "type": "number", - "title": "Amount in ADA", - "minimum": 0 - }, - "breakdown": { - "type": "array", - "title": "Budget Breakdown", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "enum": [ - "Development", - "Design", - "Marketing", - "Operations", - "Other" - ] - }, - "amount": { - "type": "number", - "minimum": 0 - }, - "description": { - "type": "string", - "minLength": 10, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "category", - "amount", - "description" - ] - }, - "minItems": 1 - } - }, - "required": [ - "amount", - "breakdown" - ] - } - }, - "required": [ - "title", - "description", - "deliverables", - "acceptanceCriteria", - "evidenceOfCompletion", - "timeline", - "budget" - ] - }, - "minItems": 3, - "maxItems": 10 - } - }, - "required": [ - "milestonesConfig", - "milestonesList" - ] -}, -"finalPitch": { - "type": "object", - "title": "Final Pitch", - "description": "Final project pitch including team, budget, and value proposition", - "properties": { - "team": { - "type": "object", - "title": "Team Information", - "description": "Details about the project team and their capabilities", - "properties": { - "members": { - "type": "array", - "title": "Team Members", - "description": "List of team members and their roles", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "title": "Name", - "description": "Full name of the team member", - "minLength": 2, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "role": { - "type": "string", - "title": "Role", - "description": "Primary role in the project", - "minLength": 5, - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "expertise": { - "type": "array", - "title": "Areas of Expertise", - "items": { - "type": "string", - "minLength": 3, - "maxLength": 50, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 1, - "maxItems": 5, - "uniqueItems": true - }, - "experience": { - "type": "string", - "title": "Relevant Experience", - "description": "Brief description of relevant experience", - "minLength": 50, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "links": { - "type": "array", - "title": "Professional Links", - "description": "Links to professional profiles or past work", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "LinkedIn", - "GitHub", - "Portfolio", - "Twitter", - "Website", - "Other" - ] - }, - "url": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "pattern": "^https?://[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$", - "contentMediaType": "text/uri-list" - }, - "description": { - "type": "string", - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "type", - "url" - ] - }, - "maxItems": 5 - } - }, - "required": [ - "name", - "role", - "expertise", - "experience" - ] - }, - "minItems": 1, - "maxItems": 10 - } - }, - "required": [ - "members" - ] - }, - "budget": { - "type": "object", - "title": "Budget Details", - "description": "Detailed budget breakdown and justification", - "properties": { - "totalBudget": { - "type": "number", - "title": "Total Budget (ADA)", - "description": "Total amount requested in ADA", - "minimum": 0, - "maximum": 1000000 - }, - "categories": { - "type": "array", - "title": "Budget Categories", - "description": "Breakdown of budget by category", - "items": { - "type": "object", - "properties": { - "category": { - "type": "string", - "enum": [ - "Development", - "Design", - "Marketing", - "Operations", - "Research", - "Community Management", - "Legal", - "Other" - ] - }, - "amount": { - "type": "number", - "minimum": 0 - }, - "description": { - "type": "string", - "minLength": 20, - "maxLength": 300, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "category", - "amount", - "description" - ] - }, - "minItems": 1 - } - }, - "required": [ - "totalBudget", - "categories" - ] - }, - "valueProposition": { - "type": "object", - "title": "Value Proposition", - "description": "Justification of the project's value for money", - "properties": { - "costBenefitAnalysis": { - "type": "string", - "title": "Cost-Benefit Analysis", - "description": "Analysis of the project's costs versus its benefits to the Cardano ecosystem", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "longTermValue": { - "type": "string", - "title": "Long-term Value", - "description": "Description of the long-term value and sustainability of the project", - "minLength": 100, - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "communityBenefits": { - "type": "array", - "title": "Community Benefits", - "description": "Specific benefits to the Cardano community", - "items": { - "type": "string", - "minLength": 20, - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "minItems": 2, - "maxItems": 5 - } - }, - "required": [ - "costBenefitAnalysis", - "longTermValue", - "communityBenefits" - ] - } - }, - "required": [ - "team", - "budget", - "valueProposition" - ] -}, -"mandatoryAcknowledgments": { - "type": "object", - "title": "Mandatory Acknowledgments", - "description": "Required acknowledgments and agreements for proposal submission", - "properties": { - "fundRules": { - "type": "object", - "title": "Fund Rules Agreement", - "properties": { - "acknowledgment": { - "type": "boolean", - "title": "Fund Rules Acknowledgment", - "description": "I confirm that I have read and agree to be bound by the Fund Rules", - "const": true - }, - "version": { - "type": "string", - "title": "Fund Rules Version", - "description": "Version of the Fund Rules being acknowledged", - "pattern": "^F[0-9]{1,3}$", - "examples": [ - "F14", - "F15" - ] - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the rules were acknowledged", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] - } - }, - "required": [ - "acknowledgment", - "version", - "timestamp" - ] - }, - "termsAndConditions": { - "type": "object", - "title": "Terms and Conditions Agreement", - "properties": { - "acknowledgment": { - "type": "boolean", - "title": "Terms and Conditions Acknowledgment", - "description": "I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions", - "const": true - }, - "version": { - "type": "string", - "title": "Terms Version", - "description": "Version of the Terms and Conditions being acknowledged", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", - "examples": [ - "1.0.0", - "2.1.3" - ] - }, - "documentUrl": { - "type": "string", - "title": "Terms Document URL", - "description": "URL to the specific version of terms and conditions", - "format": "uri", - "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", - "contentMediaType": "text/html" - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the terms were acknowledged", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] - } - }, - "required": [ - "acknowledgment", - "version", - "timestamp", - "documentUrl" - ] - }, - "privacyPolicy": { - "type": "object", - "title": "Privacy Policy Agreement", - "properties": { - "acknowledgment": { - "type": "boolean", - "title": "Privacy Policy Acknowledgment", - "description": "I acknowledge and agree that any data I share will be processed in accordance with the Catalyst FCS Privacy Policy", - "const": true - }, - "version": { - "type": "string", - "title": "Privacy Policy Version", - "description": "Version of the Privacy Policy being acknowledged", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", - "examples": [ - "1.0.0", - "2.1.3" - ] - }, - "documentUrl": { - "type": "string", - "title": "Privacy Policy URL", - "description": "URL to the specific version of privacy policy", - "format": "uri", - "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", - "contentMediaType": "text/html" - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the privacy policy was acknowledged", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] - } - }, - "required": [ - "acknowledgment", - "version", - "timestamp", - "documentUrl" - ] - }, - "intellectualProperty": { - "type": "object", - "title": "Intellectual Property Declaration", - "properties": { - "acknowledgment": { - "type": "boolean", - "title": "IP Rights Acknowledgment", - "description": "I confirm that I have the necessary rights to all intellectual property included in this proposal", - "const": true - }, - "details": { - "type": "string", - "title": "IP Details", - "description": "Additional details about intellectual property rights (if applicable)", - "maxLength": 1000, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "attachments": { - "type": "array", - "title": "IP Documentation", - "description": "Supporting documentation for IP rights (if applicable)", - "items": { - "type": "object", - "properties": { - "documentType": { - "type": "string", - "enum": [ - "patent", - "trademark", - "copyright", - "license", - "other" - ], - "description": "Type of IP documentation" - }, - "documentUrl": { - "type": "string", - "format": "uri", - "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", - "contentMediaType": "application/pdf" - }, - "description": { - "type": "string", - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "documentType", - "documentUrl", - "description" - ] - }, - "maxItems": 10 - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the IP declaration was made", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] - } - }, - "required": [ - "acknowledgment", - "timestamp" - ] - }, - "compliance": { - "type": "object", - "title": "Compliance Declaration", - "properties": { - "legalCompliance": { - "type": "boolean", - "title": "Legal Compliance", - "description": "I confirm that my proposal complies with all applicable laws and regulations", - "const": true - }, - "noConflictOfInterest": { - "type": "boolean", - "title": "No Conflict of Interest", - "description": "I confirm that there are no undisclosed conflicts of interest", - "const": true - }, - "accurateInformation": { - "type": "boolean", - "title": "Information Accuracy", - "description": "I confirm that all information provided is accurate and complete", - "const": true - }, - "jurisdictions": { - "type": "array", - "title": "Applicable Jurisdictions", - "description": "List of jurisdictions where compliance is declared", - "items": { - "type": "string", - "pattern": "^[A-Z]{2}$", - "description": "ISO 3166-1 alpha-2 country code" - }, - "minItems": 1, - "uniqueItems": true - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "description": "When the compliance declaration was made", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] - } - }, - "required": [ - "legalCompliance", - "noConflictOfInterest", - "accurateInformation", - "jurisdictions", - "timestamp" - ] - }, - "additionalAcknowledgments": { - "type": "array", - "title": "Additional Acknowledgments", - "description": "Any additional acknowledgments required for specific proposal types", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "title": "Acknowledgment Type", - "minLength": 5, - "maxLength": 100, - "pattern": "^[a-zA-Z][a-zA-Z0-9_\\-\\.]*$" - }, - "acknowledgment": { - "type": "boolean", - "title": "Acknowledgment", - "const": true - }, - "description": { - "type": "string", - "title": "Description", - "description": "Detailed description of what is being acknowledged", - "minLength": 10, - "maxLength": 500, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "documentUrl": { - "type": "string", - "title": "Reference Document", - "description": "URL to the document being acknowledged", - "format": "uri", - "pattern": "^https://[\\w\\-\\.]+\\.[a-zA-Z]{2,}/.*$", - "contentMediaType": "text/html" - }, - "timestamp": { - "type": "string", - "title": "Acknowledgment Timestamp", - "format": "date-time", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$", - "examples": [ - "2024-01-20T15:30:00Z" - ] - } - }, - "required": [ - "type", - "acknowledgment", - "description", - "timestamp" - ] - } - } - }, - "required": [ - "fundRules", - "termsAndConditions", - "privacyPolicy", - "intellectualProperty", - "compliance" - ] -} -} -} - "impact": { - "type": "object", - "title": "Project Impact", - "description": "Describe the expected impact of your project", - "properties": { - "timeframe": { - "type": "string", - "enum": [ - "Short-term (0-6 months)", - "Medium-term (6-18 months)", - "Long-term (18+ months)" - ], - "title": "Impact Timeframe", - "description": "Expected timeframe to see meaningful impact" - }, - "scale": { - "type": "string", - "enum": [ - "Local", - "Regional", - "Global" - ], - "title": "Impact Scale", - "description": "Geographic scale of impact" - }, - "metrics": { - "type": "array", - "title": "Impact Metrics", - "description": "Key metrics to measure project success", - "items": { - "type": "object", - "properties": { - "metric": { - "type": "string", - "title": "Metric Name", - "description": "Name of the metric", - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "target": { - "type": "string", - "title": "Target Value", - "description": "Target value or goal for this metric", - "maxLength": 100, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - }, - "measurement": { - "type": "string", - "title": "Measurement Method", - "description": "How this metric will be measured", - "maxLength": 200, - "pattern": "^[\\S\\s]*$", - "contentMediaType": "text/plain" - } - }, - "required": [ - "metric", - "target", - "measurement" - ] - }, - "minItems": 1, - "maxItems": 5 - } - }, - "required": [ - "timeframe", - "scale", - "metrics" - ] - } -}, -"required": [ - "primaryCategory", - "subCategory", - "tags", - "impact" -] -}, diff --git a/utilities/wallet-tester/package-lock.json b/utilities/wallet-tester/package-lock.json index c1873bd412e..a3e8045c93e 100644 --- a/utilities/wallet-tester/package-lock.json +++ b/utilities/wallet-tester/package-lock.json @@ -17,6 +17,7 @@ "ace-builds": "^1.32.7", "cborg": "^4.1.1", "dayjs": "^1.11.10", + "hermes-wallet-connector": "file:", "lodash-es": "^4.17.21", "react": "^18.2.0", "react-ace": "^10.1.0", @@ -4546,6 +4547,10 @@ "node": ">= 0.4" } }, + "node_modules/hermes-wallet-connector": { + "resolved": "", + "link": true + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", diff --git a/utilities/wallet-tester/package.json b/utilities/wallet-tester/package.json index 2be5b52baf4..5d7d4197a62 100644 --- a/utilities/wallet-tester/package.json +++ b/utilities/wallet-tester/package.json @@ -22,6 +22,7 @@ "ace-builds": "^1.32.7", "cborg": "^4.1.1", "dayjs": "^1.11.10", + "hermes-wallet-connector": "file:", "lodash-es": "^4.17.21", "react": "^18.2.0", "react-ace": "^10.1.0", From 50ac0bbe56024c4a0c904a9cc9bd470fd3e0059e Mon Sep 17 00:00:00 2001 From: nathanbogale Date: Wed, 1 Jan 2025 21:18:25 +0300 Subject: [PATCH 24/25] fix: package and lock files fixes --- .../catalyst_cardano/wallet-automation/package-lock.json | 5 ----- .../catalyst_cardano/wallet-automation/package.json | 1 - .../libs/catalyst_key_derivation/assets/js/package.json | 7 ++----- utilities/wallet-tester/package-lock.json | 5 ----- utilities/wallet-tester/package.json | 1 - 5 files changed, 2 insertions(+), 17 deletions(-) diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json index e32f883ee72..3719a91ad85 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@tomjs/unzip-crx": "^1.1.3", "@types/node-fetch": "^2.6.11", - "catalyst-voices": "file:", "dotenv": "^16.3.1", "fs-extra": "^11.2.0", "install": "^0.13.0", @@ -72,10 +71,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/catalyst-voices": { - "resolved": "", - "link": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json index 5c87101f7f5..af26c6d9bef 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/package.json @@ -27,7 +27,6 @@ "dependencies": { "@tomjs/unzip-crx": "^1.1.3", "@types/node-fetch": "^2.6.11", - "catalyst-voices": "file:", "dotenv": "^16.3.1", "fs-extra": "^11.2.0", "install": "^0.13.0", diff --git a/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package.json b/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package.json index 99b4555178f..79b195a6ebb 100644 --- a/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package.json +++ b/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package.json @@ -11,8 +11,5 @@ "catalyst_key_derivation.js" ], "browser": "catalyst_key_derivation.js", - "homepage": "https://input-output-hk.github.io/catalyst-voices", - "dependencies": { - "catalyst_key_derivation": "file:" - } -} + "homepage": "https://input-output-hk.github.io/catalyst-voices" +} \ No newline at end of file diff --git a/utilities/wallet-tester/package-lock.json b/utilities/wallet-tester/package-lock.json index a3e8045c93e..c1873bd412e 100644 --- a/utilities/wallet-tester/package-lock.json +++ b/utilities/wallet-tester/package-lock.json @@ -17,7 +17,6 @@ "ace-builds": "^1.32.7", "cborg": "^4.1.1", "dayjs": "^1.11.10", - "hermes-wallet-connector": "file:", "lodash-es": "^4.17.21", "react": "^18.2.0", "react-ace": "^10.1.0", @@ -4547,10 +4546,6 @@ "node": ">= 0.4" } }, - "node_modules/hermes-wallet-connector": { - "resolved": "", - "link": true - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", diff --git a/utilities/wallet-tester/package.json b/utilities/wallet-tester/package.json index 5d7d4197a62..2be5b52baf4 100644 --- a/utilities/wallet-tester/package.json +++ b/utilities/wallet-tester/package.json @@ -22,7 +22,6 @@ "ace-builds": "^1.32.7", "cborg": "^4.1.1", "dayjs": "^1.11.10", - "hermes-wallet-connector": "file:", "lodash-es": "^4.17.21", "react": "^18.2.0", "react-ace": "^10.1.0", From 81f6e866d25a3569136bfab03d67d53c223b455c Mon Sep 17 00:00:00 2001 From: nathanbogale Date: Wed, 1 Jan 2025 21:20:10 +0300 Subject: [PATCH 25/25] fix: package json file alligned with main --- Overall, | 0 .../assets/js/package-lock.json | 20 ------------------- 2 files changed, 20 deletions(-) create mode 100644 Overall, delete mode 100644 catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package-lock.json diff --git a/Overall, b/Overall, new file mode 100644 index 00000000000..e69de29bb2d diff --git a/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package-lock.json b/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package-lock.json deleted file mode 100644 index ade0a57615b..00000000000 --- a/catalyst_voices/packages/libs/catalyst_key_derivation/assets/js/package-lock.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "catalyst_key_derivation", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "catalyst_key_derivation", - "version": "0.1.0", - "license": "Apache-2.0", - "dependencies": { - "catalyst_key_derivation": "file:" - } - }, - "node_modules/catalyst_key_derivation": { - "resolved": "", - "link": true - } - } -}