diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b466060043..bcf6b159156 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ permissions: jobs: ci: - uses: input-output-hk/catalyst-forge/.github/workflows/ci.yml@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/.github/workflows/ci.yml@ci/v1.5.3 with: forge_version: 0.8.0 diff --git a/.github/workflows/flutter-uikit-example-firebase-deploy.yml b/.github/workflows/flutter-uikit-example-firebase-deploy.yml index bd2bdcaec6a..7fef0e1dc35 100644 --- a/.github/workflows/flutter-uikit-example-firebase-deploy.yml +++ b/.github/workflows/flutter-uikit-example-firebase-deploy.yml @@ -20,13 +20,13 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Forge - uses: input-output-hk/catalyst-forge/actions/install@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/install@ci/v1.5.3 with: version: 0.8.0 - name: Setup CI - uses: input-output-hk/catalyst-forge/actions/setup@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/setup@ci/v1.5.3 - name: Build Flutter Web - uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.3 if: always() continue-on-error: true with: diff --git a/.github/workflows/generate-allure-report.yml b/.github/workflows/generate-allure-report.yml index 3195187d94f..4751151aaff 100644 --- a/.github/workflows/generate-allure-report.yml +++ b/.github/workflows/generate-allure-report.yml @@ -26,16 +26,16 @@ jobs: - uses: actions/checkout@v4 - name: Install Forge - uses: input-output-hk/catalyst-forge/actions/install@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/install@ci/v1.5.3 with: version: 0.8.0 if: always() - name: Setup CI - uses: input-output-hk/catalyst-forge/actions/setup@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/setup@ci/v1.5.3 - name: Get catalyst gateway unit test report - uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.3 if: always() continue-on-error: true with: @@ -43,7 +43,7 @@ jobs: args: ./catalyst-gateway+build - name: Get schemathesis test report - uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.3 if: always() continue-on-error: true with: @@ -51,7 +51,7 @@ jobs: args: ./catalyst-gateway/tests/schemathesis_tests+test-fuzzer-api - name: Get flutter unit test report - uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.3 if: always() continue-on-error: true with: @@ -59,7 +59,7 @@ jobs: args: ./catalyst_voices+test-unit - name: Get python api test report - uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.1 + uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.3 if: always() continue-on-error: true with: diff --git a/catalyst-gateway/bin/Cargo.toml b/catalyst-gateway/bin/Cargo.toml index f55833967ad..39737cc3898 100644 --- a/catalyst-gateway/bin/Cargo.toml +++ b/catalyst-gateway/bin/Cargo.toml @@ -18,6 +18,8 @@ workspace = true 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" } +catalyst-signed-doc = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250116-00" } + pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } pallas-traverse = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } @@ -98,6 +100,8 @@ regex = "1.11.1" minijinja = "2.5.0" bytes = "1.9.0" mime = "0.3.17" +stats_alloc = "0.1.10" +memory-stats = "1.0.0" [dev-dependencies] proptest = "1.5.0" diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs b/catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs index bf4b9e10529..1bf39a76d63 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs +++ b/catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs @@ -47,8 +47,8 @@ impl FullSignedDoc { /// Returns the document author. #[allow(dead_code)] - pub(crate) fn author(&self) -> &String { - self.body.author() + pub(crate) fn authors(&self) -> &Vec { + self.body.authors() } /// Returns the `SignedDocBody`. @@ -57,7 +57,15 @@ impl FullSignedDoc { &self.body } + /// Returns the document raw bytes. + #[allow(dead_code)] + pub(crate) fn raw(&self) -> &Vec { + &self.raw + } + /// Uploads a `FullSignedDoc` to the event db. + /// Returns `true` if document was added into the db, `false` if it was already added + /// previously. /// /// Make an insert query into the `event-db` by adding data into the `signed_docs` /// table. @@ -73,21 +81,21 @@ impl FullSignedDoc { /// - `ver` is a UUID v7 /// - `doc_type` is a UUID v4 #[allow(dead_code)] - pub(crate) async fn store(&self) -> anyhow::Result<()> { + pub(crate) async fn store(&self) -> anyhow::Result { match Self::retrieve(self.id(), Some(self.ver())).await { Ok(res_doc) => { anyhow::ensure!( &res_doc == self, "Document with the same `id` and `ver` already exists" ); - return Ok(()); + Ok(false) }, - Err(err) if err.is::() => {}, - Err(err) => return Err(err), + Err(err) if err.is::() => { + EventDB::modify(INSERT_SIGNED_DOCS, &self.postgres_db_fields()).await?; + Ok(true) + }, + Err(err) => Err(err), } - - EventDB::modify(INSERT_SIGNED_DOCS, &self.postgres_db_fields()).await?; - Ok(()) } /// Loads a `FullSignedDoc` from the event db. @@ -146,7 +154,7 @@ impl FullSignedDoc { *id, ver, row.try_get("type")?, - row.try_get("author")?, + row.try_get("authors")?, row.try_get("metadata")?, ), payload: row.try_get("payload")?, diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs b/catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs index 9948fc92b48..2f8ca4ab220 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs +++ b/catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs @@ -13,7 +13,7 @@ pub(crate) enum DocsQueryFilter { DocId(uuid::Uuid), /// Select docs with the specific `id` and `ver` field DocVer(uuid::Uuid, uuid::Uuid), - /// Select docs with the specific `author` field + /// Select docs with the specific `authors` field Author(String), } @@ -26,7 +26,7 @@ impl Display for DocsQueryFilter { Self::DocVer(id, ver) => { write!(f, "signed_docs.id = '{id}' AND signed_docs.ver = '{ver}'") }, - Self::Author(author) => write!(f, "signed_docs.author = '{author}'"), + Self::Author(author) => write!(f, "signed_docs.authors @> '{{ \"{author}\" }}'"), } } } diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs b/catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs index df85b1c40a0..1e1c6c8ac67 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs +++ b/catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs @@ -23,8 +23,8 @@ pub(crate) struct SignedDocBody { ver: uuid::Uuid, /// `signed_doc` table `type` field doc_type: uuid::Uuid, - /// `signed_doc` table `author` field - author: String, + /// `signed_doc` table `authors` field + authors: Vec, /// `signed_doc` table `metadata` field metadata: Option, } @@ -40,9 +40,9 @@ impl SignedDocBody { &self.ver } - /// Returns the document author. - pub(crate) fn author(&self) -> &String { - &self.author + /// Returns the document authors. + pub(crate) fn authors(&self) -> &Vec { + &self.authors } /// Returns all signed document fields for the event db queries @@ -51,22 +51,21 @@ impl SignedDocBody { &self.id, &self.ver, &self.doc_type, - &self.author, + &self.authors, &self.metadata, ] } /// Creates a `SignedDocBody` instance. - #[allow(dead_code)] pub(crate) fn new( - id: uuid::Uuid, ver: uuid::Uuid, doc_type: uuid::Uuid, author: String, + id: uuid::Uuid, ver: uuid::Uuid, doc_type: uuid::Uuid, authors: Vec, metadata: Option, ) -> Self { Self { id, ver, doc_type, - author, + authors, metadata, } } @@ -91,13 +90,13 @@ impl SignedDocBody { let id = row.try_get("id")?; let ver = row.try_get("ver")?; let doc_type = row.try_get("type")?; - let author = row.try_get("author")?; + let authors = row.try_get("authors")?; let metadata = row.try_get("metadata")?; Ok(Self { id, ver, doc_type, - author, + authors, metadata, }) } diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/sql/filtered_select_signed_documents.sql.jinja b/catalyst-gateway/bin/src/db/event/signed_docs/sql/filtered_select_signed_documents.sql.jinja index 3210e1c6dff..eacbfb327d6 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/sql/filtered_select_signed_documents.sql.jinja +++ b/catalyst-gateway/bin/src/db/event/signed_docs/sql/filtered_select_signed_documents.sql.jinja @@ -2,7 +2,7 @@ SELECT signed_docs.id, signed_docs.ver, signed_docs.type, - signed_docs.author, + signed_docs.authors, signed_docs.metadata FROM signed_docs WHERE 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 index 61183ea693a..2f69b49efad 100644 --- 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 @@ -3,7 +3,7 @@ INSERT INTO signed_docs id, ver, type, - author, + authors, metadata, payload, raw diff --git a/catalyst-gateway/bin/src/db/event/signed_docs/sql/select_signed_documents.sql.jinja b/catalyst-gateway/bin/src/db/event/signed_docs/sql/select_signed_documents.sql.jinja index a1a16a7bcb9..386387fa7a2 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/sql/select_signed_documents.sql.jinja +++ b/catalyst-gateway/bin/src/db/event/signed_docs/sql/select_signed_documents.sql.jinja @@ -1,7 +1,7 @@ SELECT {% if not ver %} signed_docs.ver, {% endif %} signed_docs.type, - signed_docs.author, + signed_docs.authors, signed_docs.metadata, signed_docs.payload, signed_docs.raw 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 index 65fd2c01d3f..c3578f1fdb9 100644 --- a/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs +++ b/catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs @@ -18,7 +18,7 @@ async fn queries_test() { uuid::Uuid::now_v7(), uuid::Uuid::now_v7(), doc_type, - "Alex".to_string(), + vec!["Alex".to_string()], Some(serde_json::Value::Null), ), Some(serde_json::Value::Null), @@ -29,7 +29,7 @@ async fn queries_test() { uuid::Uuid::now_v7(), uuid::Uuid::now_v7(), doc_type, - "Steven".to_string(), + vec!["Steven".to_string()], Some(serde_json::Value::Null), ), Some(serde_json::Value::Null), @@ -40,7 +40,7 @@ async fn queries_test() { uuid::Uuid::now_v7(), uuid::Uuid::now_v7(), doc_type, - "Sasha".to_string(), + vec!["Sasha".to_string()], None, ), None, @@ -49,12 +49,18 @@ async fn queries_test() { ]; for doc in &docs { - doc.store().await.unwrap(); + assert!(doc.store().await.unwrap()); // try to insert the same data again - doc.store().await.unwrap(); + assert!(!doc.store().await.unwrap()); // try another doc with the same `id` and `ver` and with different other fields let another_doc = FullSignedDoc::new( - SignedDocBody::new(*doc.id(), *doc.ver(), doc_type, "Neil".to_string(), None), + SignedDocBody::new( + *doc.id(), + *doc.ver(), + doc_type, + vec!["Neil".to_string()], + None, + ), None, vec![], ); @@ -87,7 +93,7 @@ async fn queries_test() { assert!(res_docs.try_next().await.unwrap().is_none()); let mut res_docs = SignedDocBody::retrieve( - &DocsQueryFilter::Author(doc.author().clone()), + &DocsQueryFilter::Author(doc.authors().first().unwrap().clone()), &QueryLimits::ALL, ) .await diff --git a/catalyst-gateway/bin/src/metrics/memory.rs b/catalyst-gateway/bin/src/metrics/memory.rs index 7da8bd333e9..13874d98744 100644 --- a/catalyst-gateway/bin/src/metrics/memory.rs +++ b/catalyst-gateway/bin/src/metrics/memory.rs @@ -1 +1,167 @@ //! Metrics related to memory analytics. + +use std::{ + alloc::System, + sync::atomic::{AtomicBool, Ordering}, + thread, +}; + +use memory_stats::{memory_stats, MemoryStats}; +use stats_alloc::{Region, StatsAlloc, INSTRUMENTED_SYSTEM}; + +use crate::settings::Settings; + +/// Use the instrumented allocator for gathering allocation statistics. +/// Note: This wraps the global allocator. +/// All structs that use the global allocator can be tracked. +#[global_allocator] +static GLOBAL: &StatsAlloc = &INSTRUMENTED_SYSTEM; + +/// This is to prevent the init function from accidentally being called multiple times. +static IS_INITIALIZED: AtomicBool = AtomicBool::new(false); + +/// Starts a background thread to periodically update memory metrics. +/// +/// This function spawns a thread that updates the global `MemoryMetrics` +/// structure at regular intervals defined by `UPDATE_INTERVAL_MILLI`. +pub(crate) fn init_metrics_updater() { + if IS_INITIALIZED.swap(true, Ordering::SeqCst) { + return; + } + + let stats = Region::new(GLOBAL); + let api_host_names = Settings::api_host_names().join(","); + let service_id = Settings::service_id(); + + thread::spawn(move || { + loop { + { + let allocator_stats = stats.change(); + let mem_stats = memory_stats().unwrap_or({ + MemoryStats { + physical_mem: 0, + virtual_mem: 0, + } + }); + + reporter::MEMORY_PHYSICAL_USAGE + .with_label_values(&[&api_host_names, service_id]) + .set(i64::try_from(mem_stats.physical_mem).unwrap_or(-1)); + reporter::MEMORY_VIRTUAL_USAGE + .with_label_values(&[&api_host_names, service_id]) + .set(i64::try_from(mem_stats.virtual_mem).unwrap_or(-1)); + reporter::MEMORY_ALLOCATION_COUNT + .with_label_values(&[&api_host_names, service_id]) + .set(i64::try_from(allocator_stats.allocations).unwrap_or(-1)); + reporter::MEMORY_DEALLOCATION_COUNT + .with_label_values(&[&api_host_names, service_id]) + .set(i64::try_from(allocator_stats.deallocations).unwrap_or(-1)); + reporter::MEMORY_REALLOCATION_COUNT + .with_label_values(&[&api_host_names, service_id]) + .set(i64::try_from(allocator_stats.reallocations).unwrap_or(-1)); + reporter::MEMORY_BYTES_ALLOCATED + .with_label_values(&[&api_host_names, service_id]) + .set(i64::try_from(allocator_stats.bytes_allocated).unwrap_or(-1)); + reporter::MEMORY_BYTES_DEALLOCATED + .with_label_values(&[&api_host_names, service_id]) + .set(i64::try_from(allocator_stats.bytes_deallocated).unwrap_or(-1)); + reporter::MEMORY_BYTES_REALLOCATED + .with_label_values(&[&api_host_names, service_id]) + .set(i64::try_from(allocator_stats.bytes_reallocated).unwrap_or(-1)); + } + + thread::sleep(Settings::metrics_memory_interval()); + } + }); +} + +/// All the related memory reporting metrics to the Prometheus service are inside this +/// module. +mod reporter { + use std::sync::LazyLock; + + use prometheus::{register_int_gauge_vec, IntGaugeVec}; + + /// Labels for the client metrics + const MEMORY_METRIC_LABELS: [&str; 2] = ["api_host_names", "service_id"]; + + /// The "physical" memory used by this process, in bytes. + pub(super) static MEMORY_PHYSICAL_USAGE: LazyLock = LazyLock::new(|| { + register_int_gauge_vec!( + "memory_physical_usage", + "Amount of physical memory usage in bytes", + &MEMORY_METRIC_LABELS + ) + .unwrap() + }); + + /// The "virtual" memory used by this process, in bytes. + pub(super) static MEMORY_VIRTUAL_USAGE: LazyLock = LazyLock::new(|| { + register_int_gauge_vec!( + "memory_virtual_usage", + "Amount of physical virtual usage in bytes", + &MEMORY_METRIC_LABELS + ) + .unwrap() + }); + + /// The number of allocation count in the heap. + pub(super) static MEMORY_ALLOCATION_COUNT: LazyLock = LazyLock::new(|| { + register_int_gauge_vec!( + "memory_allocation_count", + "Number of allocation count in the heap", + &MEMORY_METRIC_LABELS + ) + .unwrap() + }); + + /// The number of deallocation count in the heap. + pub(super) static MEMORY_DEALLOCATION_COUNT: LazyLock = LazyLock::new(|| { + register_int_gauge_vec!( + "memory_deallocation_count", + "Number of deallocation count in the heap", + &MEMORY_METRIC_LABELS + ) + .unwrap() + }); + + /// The number of reallocation count in the heap. + pub(super) static MEMORY_REALLOCATION_COUNT: LazyLock = LazyLock::new(|| { + register_int_gauge_vec!( + "memory_reallocation_count", + "Number of reallocation count in the heap", + &MEMORY_METRIC_LABELS + ) + .unwrap() + }); + + /// The amount of accumulative allocated bytes in the heap. + pub(super) static MEMORY_BYTES_ALLOCATED: LazyLock = LazyLock::new(|| { + register_int_gauge_vec!( + "memory_bytes_allocated", + "Amount of accumulative allocated bytes in the heap", + &MEMORY_METRIC_LABELS + ) + .unwrap() + }); + + /// The amount of accumulative deallocated bytes in the heap. + pub(super) static MEMORY_BYTES_DEALLOCATED: LazyLock = LazyLock::new(|| { + register_int_gauge_vec!( + "memory_bytes_deallocated", + "Amount of accumulative deallocated bytes in the heap", + &MEMORY_METRIC_LABELS + ) + .unwrap() + }); + + /// The amount of accumulative reallocated bytes in the heap. + pub(super) static MEMORY_BYTES_REALLOCATED: LazyLock = LazyLock::new(|| { + register_int_gauge_vec!( + "memory_bytes_reallocated", + "Amount of accumulative reallocated bytes in the heap", + &MEMORY_METRIC_LABELS + ) + .unwrap() + }); +} diff --git a/catalyst-gateway/bin/src/metrics/mod.rs b/catalyst-gateway/bin/src/metrics/mod.rs index ba8d71503da..74725efee27 100644 --- a/catalyst-gateway/bin/src/metrics/mod.rs +++ b/catalyst-gateway/bin/src/metrics/mod.rs @@ -14,5 +14,7 @@ pub(crate) mod memory; /// Returns the default prometheus registry. #[must_use] pub(crate) fn init_prometheus() -> Registry { + memory::init_metrics_updater(); + default_registry().clone() } diff --git a/catalyst-gateway/bin/src/service/api/documents/get_document.rs b/catalyst-gateway/bin/src/service/api/documents/get_document.rs index 00c5ba4b9ab..f654dfcd804 100644 --- a/catalyst-gateway/bin/src/service/api/documents/get_document.rs +++ b/catalyst-gateway/bin/src/service/api/documents/get_document.rs @@ -1,9 +1,11 @@ //! Implementation of the GET `/document` endpoint -use poem::Body; use poem_openapi::ApiResponse; -use crate::service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor}; +use crate::{ + db::event::{error::NotFoundError, signed_docs::FullSignedDoc}, + service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor}, +}; /// Endpoint responses. #[derive(ApiResponse)] @@ -13,7 +15,7 @@ pub(crate) enum Responses { /// /// The Document that was requested. #[oai(status = 200)] - Ok(Cbor), + Ok(Cbor>), /// ## Not Found /// /// The document could not be found. @@ -27,8 +29,9 @@ pub(crate) type AllResponses = WithErrorResponses; /// # GET `/document` #[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] pub(crate) async fn endpoint(document_id: uuid::Uuid, version: Option) -> AllResponses { - let _doc = document_id; - let _ver = version; - - Responses::NotFound.into() + match FullSignedDoc::retrieve(&document_id, version.as_ref()).await { + Ok(doc) => Responses::Ok(Cbor(doc.raw().clone())).into(), + Err(err) if err.is::() => Responses::NotFound.into(), + Err(err) => AllResponses::handle_error(&err), + } } diff --git a/catalyst-gateway/bin/src/service/api/documents/mod.rs b/catalyst-gateway/bin/src/service/api/documents/mod.rs index 5714232ac31..3d005a06e60 100644 --- a/catalyst-gateway/bin/src/service/api/documents/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/mod.rs @@ -12,10 +12,15 @@ use put_document::{bad_put_request::PutDocumentBadRequest, MAXIMUM_DOCUMENT_SIZE use crate::service::{ common::{ - self, - auth::{none_or_rbac::NoneOrRBAC, rbac::scheme::CatalystRBACSecurityScheme}, + auth::rbac::scheme::CatalystRBACSecurityScheme, tags::ApiTags, - types::{generic::uuidv7::UUIDv7, payload::cbor::Cbor}, + types::{ + generic::{ + query::pagination::{Limit, Page}, + uuidv7::UUIDv7, + }, + payload::cbor::Cbor, + }, }, utilities::middleware::schema_validation::schema_version_validation, }; @@ -46,7 +51,7 @@ impl DocumentApi { /// version. version: Query>, /// No Authorization required, but Token permitted. - _auth: NoneOrRBAC, + _auth: CatalystRBACSecurityScheme, ) -> get_document::AllResponses { let Ok(doc_id) = document_id.0.try_into() else { let err = anyhow!("Invalid UUIDv7"); // Should not happen as UUIDv7 is validating. @@ -76,9 +81,9 @@ impl DocumentApi { _auth: CatalystRBACSecurityScheme, ) -> put_document::AllResponses { match document.0.into_bytes_limit(MAXIMUM_DOCUMENT_SIZE).await { - Ok(document) => put_document::endpoint(document).await, + Ok(doc_bytes) => put_document::endpoint(doc_bytes.to_vec()).await, Err(ReadBodyError::PayloadTooLarge) => put_document::Responses::PayloadTooLarge.into(), - Err(_err) => { + Err(_) => { put_document::Responses::BadRequest(Json(PutDocumentBadRequest::new( "Failed to read document from the request", ))) @@ -103,8 +108,7 @@ impl DocumentApi { async fn post_document( &self, /// The Query Filter Specification query: Json, - page: Query>, - limit: Query>, + page: Query>, limit: Query>, /// Authorization required. _auth: CatalystRBACSecurityScheme, ) -> post_document_index_query::AllResponses { diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs index 26ca8c004ac..7b70aa58eb4 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs @@ -4,7 +4,7 @@ use poem_openapi::{payload::Json, ApiResponse, Object}; use query_filter::DocumentIndexQueryFilter; use response::DocumentIndexListDocumented; -use super::common; +use super::{Limit, Page}; use crate::service::common::responses::WithErrorResponses; pub(crate) mod query_filter; @@ -39,9 +39,7 @@ pub(crate) struct QueryDocumentIndex { /// # POST `/document/index` #[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] pub(crate) async fn endpoint( - filter: DocumentIndexQueryFilter, - page: Option, - limit: Option, + filter: DocumentIndexQueryFilter, page: Option, limit: Option, ) -> AllResponses { let _filter = filter; let _page = page; diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs index 7c82ae2d859..5ad0f38cd36 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs @@ -5,9 +5,9 @@ use poem_openapi::{types::Example, NewType, Object}; -use super::common::types::document::ver::EqOrRangedVerDocumented; use crate::service::common::types::document::{ doc_ref::IdAndVerRefDocumented, doc_type::DocumentType, id::EqOrRangedIdDocumented, + ver::EqOrRangedVerDocumented, }; /// Query Filter for the generation of a signed document index. diff --git a/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs index 5d3bad1903a..99e0a5d79d5 100644 --- a/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs @@ -14,9 +14,9 @@ pub(crate) struct PutDocumentBadRequest { impl PutDocumentBadRequest { /// Create a new instance of `ConfigBadRequest`. - pub(crate) fn new(error: &str) -> Self { + pub(crate) fn new(error: &(impl ToString + ?Sized)) -> Self { Self { - error: error.to_owned(), + error: error.to_string(), } } } diff --git a/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs index ddef4f7fa65..0102ec4cd87 100644 --- a/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs @@ -1,10 +1,14 @@ //! Implementation of the PUT `/document` endpoint +use anyhow::anyhow; use bad_put_request::PutDocumentBadRequest; -use bytes::Bytes; +use catalyst_signed_doc::{CatalystSignedDocument, Decode, Decoder}; use poem_openapi::{payload::Json, ApiResponse}; -use crate::service::common::responses::WithErrorResponses; +use crate::{ + db::event::signed_docs::{FullSignedDoc, SignedDocBody}, + service::common::responses::WithErrorResponses, +}; pub(crate) mod bad_put_request; @@ -42,9 +46,61 @@ pub(crate) enum Responses { pub(crate) type AllResponses = WithErrorResponses; /// # PUT `/document` -#[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] -pub(crate) async fn endpoint(document: Bytes) -> AllResponses { - let _doc = document; +#[allow(clippy::no_effect_underscore_binding)] +pub(crate) async fn endpoint(doc_bytes: Vec) -> AllResponses { + match CatalystSignedDocument::decode(&mut Decoder::new(&doc_bytes), &mut ()) { + Ok(doc) => { + let authors = doc + .signatures() + .kids() + .into_iter() + .map(|kid| kid.to_string()) + .collect(); - Responses::BadRequest(Json(PutDocumentBadRequest::new("unimplemented"))).into() + let doc_meta_json = match serde_json::to_value(doc.doc_meta()) { + Ok(json) => json, + Err(e) => { + return AllResponses::internal_error(&anyhow!( + "Cannot decode document metadata into JSON, err: {e}" + )) + }, + }; + + let doc_body = SignedDocBody::new( + doc.doc_id(), + doc.doc_ver(), + doc.doc_type(), + authors, + Some(doc_meta_json), + ); + + let payload = if doc.doc_content().is_json() { + match serde_json::from_slice(doc.doc_content().bytes()) { + Ok(payload) => Some(payload), + Err(e) => { + return AllResponses::internal_error(&anyhow!( + "Invalid Document Content, not Json encoded: {e}" + )) + }, + } + } else { + None + }; + + match FullSignedDoc::new(doc_body, payload, doc_bytes) + .store() + .await + { + Ok(true) => Responses::Created.into(), + Ok(false) => Responses::NoContent.into(), + Err(err) => AllResponses::handle_error(&err), + } + }, + Err(_) => { + Responses::BadRequest(Json(PutDocumentBadRequest::new( + "Invalid CBOR encoded document", + ))) + .into() + }, + } } diff --git a/catalyst-gateway/bin/src/service/common/auth/rbac/scheme.rs b/catalyst-gateway/bin/src/service/common/auth/rbac/scheme.rs index 0489a5b9721..154814be49b 100644 --- a/catalyst-gateway/bin/src/service/common/auth/rbac/scheme.rs +++ b/catalyst-gateway/bin/src/service/common/auth/rbac/scheme.rs @@ -48,8 +48,7 @@ static CERTS: LazyLock> = LazyLock::new bearer_format = "catalyst-rbac-token", checker = "checker_api_catalyst_auth" )] -#[allow(clippy::module_name_repetitions)] -#[allow(dead_code)] +#[allow(dead_code, clippy::module_name_repetitions)] pub struct CatalystRBACSecurityScheme(pub CatalystRBACTokenV1); /// Error with the Authorization Token diff --git a/catalyst-gateway/bin/src/settings/mod.rs b/catalyst-gateway/bin/src/settings/mod.rs index 6be03945050..d3702d39373 100644 --- a/catalyst-gateway/bin/src/settings/mod.rs +++ b/catalyst-gateway/bin/src/settings/mod.rs @@ -11,7 +11,6 @@ use anyhow::anyhow; use cardano_chain_follower::Network; use clap::Args; use dotenvy::dotenv; -use duration_string::DurationString; use str_env_var::StringEnvVar; use tracing::error; use url::Url; @@ -52,6 +51,9 @@ const API_URL_PREFIX_DEFAULT: &str = "/api"; /// Default `CHECK_CONFIG_TICK` used in development. const CHECK_CONFIG_TICK_DEFAULT: &str = "5s"; +/// Default `METRICS_MEMORY_INTERVAL`. +const METRICS_MEMORY_INTERVAL_DEFAULT: &str = "1s"; + /// Default Event DB URL. const EVENT_DB_URL_DEFAULT: &str = "postgresql://postgres:postgres@localhost/catalyst_events?sslmode=disable"; @@ -144,6 +146,9 @@ struct EnvVars { /// Tick every N seconds until config exists in db #[allow(unused)] check_config_tick: Duration, + + /// Interval for updating and sending memory metrics. + metrics_memory_interval: Duration, } // Lazy initialization of all env vars which are not command line parameters. @@ -157,19 +162,6 @@ static ENV_VARS: LazyLock = LazyLock::new(|| { // Support env vars in a `.env` file, doesn't need to exist. dotenv().ok(); - let check_interval = StringEnvVar::new("CHECK_CONFIG_TICK", CHECK_CONFIG_TICK_DEFAULT.into()); - let check_config_tick = match DurationString::try_from(check_interval.as_string()) { - Ok(duration) => duration.into(), - Err(error) => { - error!( - "Invalid Check Config Tick Duration: {} : {}. Defaulting to 5 seconds.", - check_interval.as_str(), - error - ); - Duration::from_secs(5) - }, - }; - EnvVars { github_repo_owner: StringEnvVar::new("GITHUB_REPO_OWNER", GITHUB_REPO_OWNER_DEFAULT.into()), github_repo_name: StringEnvVar::new("GITHUB_REPO_NAME", GITHUB_REPO_NAME_DEFAULT.into()), @@ -194,7 +186,14 @@ static ENV_VARS: LazyLock = LazyLock::new(|| { ), chain_follower: chain_follower::EnvVars::new(), internal_api_key: StringEnvVar::new_optional("INTERNAL_API_KEY", true), - check_config_tick, + check_config_tick: StringEnvVar::new_as_duration( + "CHECK_CONFIG_TICK", + CHECK_CONFIG_TICK_DEFAULT, + ), + metrics_memory_interval: StringEnvVar::new_as_duration( + "METRICS_MEMORY_INTERVAL", + METRICS_MEMORY_INTERVAL_DEFAULT, + ), } }); @@ -288,6 +287,11 @@ impl Settings { ENV_VARS.service_id.as_str() } + /// The memory metrics interval + pub(crate) fn metrics_memory_interval() -> Duration { + ENV_VARS.metrics_memory_interval + } + /// Get a list of all host names to serve the API on. /// /// Used by the `OpenAPI` Documentation to point to the correct backend. diff --git a/catalyst-gateway/bin/src/settings/str_env_var.rs b/catalyst-gateway/bin/src/settings/str_env_var.rs index 18e8a6414b7..4676e29eb93 100644 --- a/catalyst-gateway/bin/src/settings/str_env_var.rs +++ b/catalyst-gateway/bin/src/settings/str_env_var.rs @@ -1,10 +1,15 @@ //! Processing for String Environment Variables + +// cspell: words smhdwy + use std::{ env::{self, VarError}, fmt::{self, Display}, str::FromStr, + time::Duration, }; +use duration_string::DurationString; use strum::VariantNames; use tracing::{error, info}; @@ -168,6 +173,39 @@ impl StringEnvVar { value } + /// Convert an Envvar into the required Duration type. + pub(crate) fn new_as_duration(var_name: &str, default: &str) -> Duration { + let choices = "A value in the format of `[0-9]+(ns|us|ms|[smhdwy])`"; + + let raw_value = StringEnvVar::new( + var_name, + (default.to_string().as_str(), false, choices).into(), + ) + .as_string(); + + match DurationString::try_from(raw_value.clone()) { + Ok(duration) => duration.into(), + Err(error) => { + error!( + "Invalid Duration: {} : {}. Defaulting to {}.", + raw_value, error, default + ); + + match DurationString::try_from(default.to_string()) { + Ok(duration) => duration.into(), + // The error from parsing the default value must not happen + Err(error) => { + error!( + "Invalid Default Duration: {} : {}. Defaulting to 1s.", + default, error + ); + Duration::from_secs(1) + }, + } + }, + } + } + /// Convert an Envvar into an integer in the bounded range. pub(super) fn new_as(var_name: &str, default: T, min: T, max: T) -> T where diff --git a/catalyst-gateway/event-db/migrations/V2__signed_documents.sql b/catalyst-gateway/event-db/migrations/V2__signed_documents.sql index 28ff361d4a3..2dc073ec812 100644 --- a/catalyst-gateway/event-db/migrations/V2__signed_documents.sql +++ b/catalyst-gateway/event-db/migrations/V2__signed_documents.sql @@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS signed_docs ( id UUID NOT NULL, -- UUID v7 ver UUID NOT NULL, -- UUID v7 type UUID NOT NULL, -- UUID v4 - author TEXT NOT NULL, + authors TEXT [] NOT NULL, metadata JSONB NULL, payload JSONB NULL, raw BYTEA NOT NULL, @@ -32,8 +32,8 @@ COMMENT ON COLUMN signed_docs.ver IS 'The Signed Documents Document Version Number (ULID).'; COMMENT ON COLUMN signed_docs.type IS 'The Signed Document type identifier.'; -COMMENT ON COLUMN signed_docs.author IS -'The Primary Author of the Signed Document.'; +COMMENT ON COLUMN signed_docs.authors IS +'The Primary Author`s list of the Signed Document.'; COMMENT ON COLUMN signed_docs.metadata IS 'Extra metadata extracted from the Signed Document, and encoded as JSON.'; COMMENT ON COLUMN signed_docs.payload IS @@ -45,13 +45,13 @@ CREATE INDEX IF NOT EXISTS idx_signed_docs_type ON signed_docs (type); COMMENT ON INDEX idx_signed_docs_type IS 'Index to help finding documents by a known type faster.'; -CREATE INDEX IF NOT EXISTS idx_signed_docs_author ON signed_docs (author); -COMMENT ON INDEX idx_signed_docs_author IS -'Index to help finding documents by a known author faster.'; +CREATE INDEX IF NOT EXISTS idx_signed_docs_authors ON signed_docs (authors); +COMMENT ON INDEX idx_signed_docs_authors IS +'Index to help finding documents by a known authors faster.'; -CREATE INDEX IF NOT EXISTS idx_signed_docs_type_author ON signed_docs (type, author); -COMMENT ON INDEX idx_signed_docs_type_author IS -'Index to help finding documents by a known author for a specific document type faster.'; +CREATE INDEX IF NOT EXISTS idx_signed_docs_type_authors ON signed_docs (type, authors); +COMMENT ON INDEX idx_signed_docs_type_authors IS +'Index to help finding documents by a known authors for a specific document type faster.'; CREATE INDEX IF NOT EXISTS idx_signed_docs_metadata ON signed_docs USING gin (metadata); diff --git a/catalyst-gateway/tests/api_tests/api_tests/put_document.hurl b/catalyst-gateway/tests/api_tests/api_tests/put_document.hurl new file mode 100644 index 00000000000..6e058e87fc2 --- /dev/null +++ b/catalyst-gateway/tests/api_tests/api_tests/put_document.hurl @@ -0,0 +1,9 @@ +PUT http://localhost:3030/api/draft/document +Content-Type: application/cbor +hex, 84585da503183270436f6e74656e742d456e636f64696e676262726474797065d82550913c9265f9f944fcb3cf9d9516ae9baf626964d8255001946ea1818a7e0eb6b16169f02ffd4e63766572d82550913c9265f9f944fcb3cf9d9516ae9bafa0581c8b0b807b22616765223a32392c226e616d65223a22416c6578227d0380; +HTTP 201 + +PUT http://localhost:3030/api/draft/document +Content-Type: application/cbor +hex, 84585da503183270436f6e74656e742d456e636f64696e676262726474797065d82550913c9265f9f944fcb3cf9d9516ae9baf626964d8255001946ea1818a7e0eb6b16169f02ffd4e63766572d82550913c9265f9f944fcb3cf9d9516ae9bafa0581c8b0b807b22616765223a32392c226e616d65223a22416c6578227d0380; +HTTP 204 \ No newline at end of file diff --git a/catalyst-gateway/tests/schemathesis_tests/Earthfile b/catalyst-gateway/tests/schemathesis_tests/Earthfile index ac66d900f4f..f5cdb0f9a4c 100644 --- a/catalyst-gateway/tests/schemathesis_tests/Earthfile +++ b/catalyst-gateway/tests/schemathesis_tests/Earthfile @@ -3,9 +3,8 @@ VERSION 0.8 package-schemathesis: FROM python:3.12-alpine3.20 # TODO: https://github.com/input-output-hk/catalyst-voices/issues/465 - ARG openapi_spec - # optional argument that can be used to pass a --hypothesis-seed to replicate specific test failures - ARG seed + ARG api_spec + ARG seed # optional argument that can be used to pass a --hypothesis-seed to replicate specific test failures ARG version=3.39.5 RUN apk add --no-cache gcc musl-dev @@ -13,9 +12,10 @@ package-schemathesis: RUN mkdir /results COPY ./hooks/hooks.py . VOLUME /results - ENTRYPOINT st run --include-path-regex '^/api/v1/health/' \ + + ENTRYPOINT st run --exclude-path-regex 'draft' \ --exclude-path '/api/v1/health/inspection' \ #excluding since this is a internal debug endpoint - $openapi_spec \ + $api_spec \ --workers=2 \ --wait-for-schema=500 \ --max-response-time=5000 \ @@ -37,12 +37,12 @@ test-fuzzer-api: FROM earthly/dind:alpine-3.19-docker-25.0.5-r0 RUN apk update && apk add iptables-legacy curl # workaround for https://github.com/earthly/earthly/issues/3784 COPY schemathesis-docker-compose.yml . - LET OPENAPI_SPEC="http://0.0.0.0:3030/docs/cat-gateway.json" + LET api_spec="http://0.0.0.0:3030/docs/cat-gateway.json" ARG seed WITH DOCKER \ --compose schemathesis-docker-compose.yml \ - --load schemathesis:latest=(+package-schemathesis --openapi_spec=$OPENAPI_SPEC --seed=$seed) \ + --load schemathesis:latest=(+package-schemathesis --api_spec=$api_spec --seed=$seed) \ --load event-db:latest=(../../event-db+build) \ --load cat-gateway:latest=(../+package-cat-gateway-integration) \ --service event-db \ @@ -60,4 +60,64 @@ test-fuzzer-api: IF [ -f fail ] RUN --no-cache echo "Schemathesis test failed. Check the logs for more details" && \ exit 1 + END + +nightly-package-schemathesis: + FROM python:3.12-alpine3.20 + # TODO: https://github.com/input-output-hk/catalyst-voices/issues/465 + ARG api_spec + # optional argument that can be used to pass a --hypothesis-seed to replicate specific test failures + ARG seed + ARG version=3.39.5 + + RUN apk add --no-cache gcc musl-dev + RUN python -m pip install schemathesis==$version + RUN mkdir /results + COPY ./hooks/hooks.py . + VOLUME /results + ENTRYPOINT st run --checks=all $api_spec \ + --workers=2 \ + --wait-for-schema=120 \ + --max-response-time=300 \ + --hypothesis-max-examples=1000 \ + --data-generation-method=all \ + --exclude-deprecated \ + --force-schema-version=30 \ + --show-trace \ + --force-color \ + --junit-xml=/results/junit-report.xml \ + --cassette-path=/results/cassette.yaml \ + $seed + + ARG tag="latest" + SAVE IMAGE schemathesis:$tag + +# nightly-test-fuzzer-api - Fuzzy test cat-gateway using openapi specs. +nightly-test-fuzzer-api: + FROM earthly/dind:alpine-3.19-docker-25.0.5-r0 + RUN apk update && apk add iptables-legacy curl # workaround for https://github.com/earthly/earthly/issues/3784 + COPY schemathesis-docker-compose.yml . + LET api_spec="http://0.0.0.0:3030/docs/cat-gateway.json" + ARG seed + + WITH DOCKER \ + --compose schemathesis-docker-compose.yml \ + --load schemathesis:latest=(+nightly-package-schemathesis --api_spec=$api_spec --seed=$seed) \ + --load event-db:latest=(../../event-db+build) \ + --load cat-gateway:latest=(../+package-cat-gateway-integration) \ + --service event-db \ + --service cat-gateway \ + --allow-privileged + + RUN --no-cache docker run --net=host --name=st schemathesis:latest || echo fail > fail; \ + docker cp st:/results/junit-report.xml junit-report.xml && \ + docker cp st:/results/cassette.yaml cassette.yaml + END + WAIT + SAVE ARTIFACT junit-report.xml AS LOCAL schemathesis-nightly.junit-report.xml + SAVE ARTIFACT cassette.yaml AS LOCAL cassette.yaml + END + IF [ -f fail ] + RUN --no-cache echo "Nightly schemathesis test failed. Check the logs for more details" && \ + exit 1 END \ No newline at end of file diff --git a/catalyst-gateway/tests/schemathesis_tests/blueprint.cue b/catalyst-gateway/tests/schemathesis_tests/blueprint.cue index ab7bb81baf9..134e32aaa5e 100644 --- a/catalyst-gateway/tests/schemathesis_tests/blueprint.cue +++ b/catalyst-gateway/tests/schemathesis_tests/blueprint.cue @@ -4,6 +4,7 @@ project: { ci: { targets: { "test-fuzzer-api": privileged: true + "nightly-test-fuzzer-api": privileged: true } } } diff --git a/catalyst_voices/.gitignore b/catalyst_voices/.gitignore index 361c63c808d..8f05462529f 100644 --- a/catalyst_voices/.gitignore +++ b/catalyst_voices/.gitignore @@ -2,7 +2,6 @@ # See https://www.dartlang.org/guides/libraries/private-files devtools_options.yaml - # Generated files from code generation tools *.g.dart *.freezed.dart @@ -153,5 +152,3 @@ coverage/ # Fastlane.swift runner binary **/fastlane/FastlaneRunner - -devtools_options.yaml \ No newline at end of file diff --git a/catalyst_voices/apps/voices/integration_test/pageobject/common_page.dart b/catalyst_voices/apps/voices/integration_test/pageobject/common_page.dart index 6732e683fb6..12296dfb200 100644 --- a/catalyst_voices/apps/voices/integration_test/pageobject/common_page.dart +++ b/catalyst_voices/apps/voices/integration_test/pageobject/common_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; class CommonPage { - static const decoratorData = Key('DecoratorData'); - static const decoratorIconBefore = Key('DecoratorIconBefore'); - static const decoratorIconAfter = Key('DecoratorIconAfter'); + static const decorData = Key('DecoratorData'); + static const decorIconBefore = Key('DecoratorIconBefore'); + static const decorIconAfter = Key('DecoratorIconAfter'); static const dialogCloseButton = Key('DialogCloseButton'); + static const voicesTextField = Key('VoicesTextField'); } diff --git a/catalyst_voices/apps/voices/integration_test/pageobject/onboarding_page.dart b/catalyst_voices/apps/voices/integration_test/pageobject/onboarding_page.dart index 5a10edf034b..ab8291b3988 100644 --- a/catalyst_voices/apps/voices/integration_test/pageobject/onboarding_page.dart +++ b/catalyst_voices/apps/voices/integration_test/pageobject/onboarding_page.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patrol_finders/patrol_finders.dart'; +import '../types/password_validation_states.dart'; import '../types/registration_state.dart'; import '../utils/selector_utils.dart'; +import '../utils/test_context.dart'; import '../utils/translations_utils.dart'; import 'common_page.dart'; @@ -26,6 +28,32 @@ class OnboardingPage { static const seedPhraseStoredCheckbox = Key('SeedPhraseStoredCheckbox'); static const uploadKeyButton = Key('UploadKeyButton'); static const resetButton = Key('ResetButton'); + static const seedPhrasesPicker = Key('SeedPhrasesPicker'); + static const nextStepTitle = Key('NextStepTitle'); + static const nextStepBody = Key('NextStepBody'); + static const passwordInputField = Key('PasswordInputField'); + static const passwordConfirmInputField = Key('PasswordConfirmInputField'); + static const passwordStrengthIndicator = Key('PasswordStrengthIndicator'); + static const passwordStrengthLabel = Key('PasswordStrengthLabel'); + static const finishAccountCreationPanel = Key('FinishAccountCreationPanel'); + static const finishAccountKeychainCreated = + Key('StepRegistrationProgressStepGroup.createKeychainRowKey'); + static const finishAccountLinkWallet = + Key('StepRegistrationProgressStepGroup.linkWalletRowKey'); + static const finishAccountAccountComplete = + Key('StepRegistrationProgressStepGroup.accountCompletedRowKey'); + static const linkWalletAndRolesButton = Key('LinkWalletAndRolesButton'); + static const chooseCardanoWalletButton = Key('ChooseCardanoWalletButton'); + static const seeAllSupportedWalletsBtn = Key('SeeAllSupportedWalletsButton'); + static const walletsLinkBuilder = Key('WalletsLinkBuilder'); + static const recoverKeychainMethodsTitle = Key('RecoverKeychainMethodsTitle'); + static const keychainNotFoundIndicator = Key('KeychainNotFoundIndicator'); + static const onDeviceKeychainsWidget = Key('BlocOnDeviceKeychains'); + static const recoverKeychainMethodsSubtitle = + Key('RecoverKeychainMethodsSubtitle'); + static const recoverKeychainMethodsListTitle = + Key('RecoverKeychainMethodsListTitle'); + static const registrationTileTitle = Key('RegistrationTileTitle'); static Future writedownSeedPhraseNumber( PatrolTester $, @@ -37,37 +65,41 @@ class OnboardingPage { return int.parse(rawNumber!.split('.').first); } - static Future writedownSeedPhraseWord( + static Future writedownSeedPhraseWord( PatrolTester $, int index, ) async { - final rawNumber = + final rawWord = $(Key('SeedPhrase${index}CellKey')).$(const Key('SeedPhraseWord')).text; - return int.parse(rawNumber!.split('.').first); + return rawWord!; } - static Future inputSeedPhraseCompleterWord( + static Future inputSeedPhraseCompleterText( PatrolTester $, int index, ) async { - final seedWord = await getChildNodeText( - $, - $(Key('CompleterSeedPhrase${index}CellKey')).$(CommonPage.decoratorData), - ); + final seedWord = $(Key('CompleterSeedPhrase${index}CellKey')) + .$(CommonPage.decorData) + .$(Text) + .text; return seedWord!; } - static Future inputSeedPhrasePickerWord( + static Future inputSeedPhrasePickerText( PatrolTester $, int index, ) async { - final seedWord = await getChildNodeText( - $, - $(Key('PickerSeedPhrase${index + 1}CellKey')), - ); + final seedWord = $(Key('PickerSeedPhrase${index + 1}CellKey')).$(Text).text; return seedWord!; } + static Future inputSeedPhrasePicker( + PatrolTester $, + String word, + ) async { + return $(seedPhrasesPicker).$(find.text(word)); + } + static Future infoPartHeaderTitleText(PatrolTester $) async { return $(registrationInfoPanel).$(headerTitle).text; } @@ -86,44 +118,25 @@ class OnboardingPage { .waitUntilVisible(); } - static Future getChildNodeText( - PatrolTester $, - FinderBase parent, - ) async { - final child = find.descendant( - of: parent, - matching: find.byType(Text), - ); - return $(child).text; - } - static Finder infoPartTaskPicture(PatrolTester $) { - final child = find.descendant( - of: $(registrationInfoPanel).$(registrationInfoPictureContainer), - matching: find.byType(IconTheme), - ); - return child; + return $(registrationInfoPanel) + .$(registrationInfoPictureContainer) + .$(IconTheme); } - static void voicesFilledButtonIsEnabled( + static void voicesButtonIsEnabled( PatrolTester $, Key button, ) { - final child = find.descendant( - of: $(button), - matching: find.byType(FilledButton), - ); + final child = $(button).$(FilledButton); SelectorUtils.isEnabled($, $(child)); } - static void voicesFilledButtonIsDisabled( + static void voicesButtonIsDisabled( PatrolTester $, Key button, ) { - final child = find.descendant( - of: $(button), - matching: find.byType(FilledButton), - ); + final child = $(button).$(FilledButton); SelectorUtils.isDisabled($, $(child)); } @@ -172,10 +185,7 @@ class OnboardingPage { expect(await infoPartHeaderTitleText($), T.get('Get Started')); expect(infoPartTaskPicture($), findsOneWidget); expect( - await getChildNodeText( - $, - $(registrationInfoPanel).$(CommonPage.decoratorData), - ), + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, T.get('Learn More'), ); break; @@ -185,10 +195,7 @@ class OnboardingPage { expect(infoPartTaskPicture($), findsOneWidget); expect($(progressBar), findsOneWidget); expect( - await getChildNodeText( - $, - $(registrationInfoPanel).$(CommonPage.decoratorData), - ), + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, T.get('Learn More'), ); break; @@ -208,10 +215,7 @@ class OnboardingPage { expect(infoPartTaskPicture($), findsOneWidget); expect($(progressBar), findsOneWidget); expect( - await getChildNodeText( - $, - $(registrationInfoPanel).$(CommonPage.decoratorData), - ), + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, T.get('Learn More'), ); break; @@ -220,12 +224,10 @@ class OnboardingPage { expect(infoPartTaskPicture($), findsOneWidget); expect($(progressBar), findsOneWidget); expect( - await getChildNodeText( - $, - $(registrationInfoPanel).$(CommonPage.decoratorData), - ), + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, T.get('Learn More'), ); + break; case RegistrationState.keychainCreateMnemonicInput: expect(await infoPartHeaderTitleText($), T.get('Catalyst Keychain')); expect( @@ -241,33 +243,95 @@ class OnboardingPage { expect(infoPartTaskPicture($), findsOneWidget); expect($(progressBar), findsOneWidget); expect( - await getChildNodeText( - $, - $(registrationInfoPanel).$(CommonPage.decoratorData), - ), + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, T.get('Learn More'), ); break; case RegistrationState.keychainCreateMnemonicVerified: - throw UnimplementedError(); + expect(await infoPartHeaderTitleText($), T.get('Catalyst Keychain')); + //temporary: check for specific picture (green checked icon) + expect(infoPartTaskPicture($), findsOneWidget); + expect($(progressBar), findsOneWidget); + expect( + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, + T.get('Learn More'), + ); + break; + case RegistrationState.passwordInfo: + expect(await infoPartHeaderTitleText($), T.get('Catalyst Keychain')); + //temporary: check for specific picture (locked icon) + expect(infoPartTaskPicture($), findsOneWidget); + expect($(progressBar), findsOneWidget); + expect( + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, + T.get('Learn More'), + ); + break; + case RegistrationState.passwordInput: + expect(await infoPartHeaderTitleText($), T.get('Catalyst Keychain')); + expect( + await infoPartHeaderSubtitleText($), + T.get('Catalyst unlock password'), + ); + expect( + await infoPartHeaderBodyText($), + T.get( + 'Please provide a password for your Catalyst Keychain.', + ), + ); + //temporary: check for specific picture (locked icon) + expect(infoPartTaskPicture($), findsOneWidget); + expect($(progressBar), findsOneWidget); + expect( + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, + T.get('Learn More'), + ); + break; + case RegistrationState.keychainCreateSuccess: + expect(await infoPartHeaderTitleText($), T.get('Catalyst Keychain')); + //temporary: check for specific picture (green key locked icon) + expect(infoPartTaskPicture($), findsOneWidget); + expect($(progressBar), findsOneWidget); + expect( + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, + T.get('Learn More'), + ); + break; case RegistrationState.keychainRestoreChoice: - throw UnimplementedError(); + expect( + await infoPartHeaderTitleText($), + T.get('Restore Catalyst keychain'), + ); + expect(infoPartTaskPicture($), findsOneWidget); + expect( + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, + T.get('Learn More'), + ); + break; case RegistrationState.keychainRestoreMnemonicInfo: throw UnimplementedError(); case RegistrationState.keychainRestoreMnemonicInput: throw UnimplementedError(); case RegistrationState.keychainRestoreSuccess: throw UnimplementedError(); - case RegistrationState.passwordInfo: - throw UnimplementedError(); - case RegistrationState.passwordInput: - throw UnimplementedError(); - case RegistrationState.keychainCreateSuccess: - throw UnimplementedError(); case RegistrationState.linkWalletInfo: - throw UnimplementedError(); case RegistrationState.linkWalletSelect: - throw UnimplementedError(); + expect( + await infoPartHeaderTitleText($), + T.get('Link keys to your Catalyst Keychain'), + ); + expect( + await infoPartHeaderSubtitleText($), + T.get('Link your Cardano wallet'), + ); + //temporary: check for specific picture (blue key icon) + expect(infoPartTaskPicture($), findsOneWidget); + expect($(progressBar), findsOneWidget); + expect( + $(registrationInfoPanel).$(CommonPage.decorData).$(Text).text, + T.get('Learn More'), + ); + break; case RegistrationState.linkWalletSuccess: throw UnimplementedError(); case RegistrationState.rolesSelect: @@ -290,17 +354,11 @@ class OnboardingPage { switch (step) { case RegistrationState.getStarted: expect( - await getChildNodeText( - $, - $(registrationDetailsPanel).$(registrationDetailsTitle), - ), + $(registrationDetailsPanel).$(registrationDetailsTitle).$(Text).text, T.get('Welcome to Catalyst'), ); expect( - await getChildNodeText( - $, - $(registrationDetailsPanel).$(registrationDetailsBody), - ), + $(registrationDetailsPanel).$(registrationDetailsBody).$(Text).text, isNotEmpty, ); expect( @@ -312,17 +370,11 @@ class OnboardingPage { break; case RegistrationState.createKeychainInfo: expect( - await getChildNodeText( - $, - $(registrationDetailsPanel).$(registrationDetailsTitle), - ), + $(registrationDetailsPanel).$(registrationDetailsTitle).$(Text).text, T.get('Create your Catalyst Keychain'), ); expect( - await getChildNodeText( - $, - $(registrationDetailsPanel).$(registrationDetailsBody), - ), + $(registrationDetailsPanel).$(registrationDetailsBody).$(Text).text, isNotEmpty, ); expect(await detailsPartCreateKeychainBtn($), findsOneWidget); @@ -330,17 +382,11 @@ class OnboardingPage { break; case RegistrationState.keychainCreated: expect( - await getChildNodeText( - $, - $(registrationDetailsPanel).$(registrationDetailsTitle), - ), + $(registrationDetailsPanel).$(registrationDetailsTitle).$(Text).text, T.get('Great! Your Catalyst Keychain 
has been created.'), ); expect( - await getChildNodeText( - $, - $(registrationDetailsPanel).$(registrationDetailsBody), - ), + $(registrationDetailsPanel).$(registrationDetailsBody).$(Text).text, isNotEmpty, ); expect($(backButton), findsOneWidget); @@ -350,15 +396,12 @@ class OnboardingPage { await writedownSeedPhrasesAreDisplayed($); expect($(downloadSeedPhraseButton), findsOneWidget); expect( - await getChildNodeText( - $, - $(downloadSeedPhraseButton).$(CommonPage.decoratorData), - ), + $(downloadSeedPhraseButton).$(CommonPage.decorData).$(Text).text, T.get('Download Catalyst key'), ); expect($(seedPhraseStoredCheckbox), findsOneWidget); expect( - await getChildNodeText($, $(seedPhraseStoredCheckbox)), + $(seedPhraseStoredCheckbox).$(Text).text, T.get('I have written down/downloaded my 12 words'), ); expect($(backButton), findsOneWidget); @@ -366,17 +409,11 @@ class OnboardingPage { break; case RegistrationState.keychainCreateMnemonicInputInfo: expect( - await getChildNodeText( - $, - $(registrationDetailsPanel).$(registrationDetailsTitle), - ), + $(registrationDetailsPanel).$(registrationDetailsTitle).$(Text).text, T.get('Check your Catalyst security keys'), ); expect( - await getChildNodeText( - $, - $(registrationDetailsPanel).$(registrationDetailsBody), - ), + $(registrationDetailsPanel).$(registrationDetailsBody).$(Text).text, isNotEmpty, ); break; @@ -384,35 +421,154 @@ class OnboardingPage { await inputSeedPhrasesAreDisplayed($); expect($(uploadKeyButton), findsOneWidget); expect( - await getChildNodeText( - $, - $(uploadKeyButton).$(CommonPage.decoratorData), - ), + $(uploadKeyButton).$(CommonPage.decorData).$(Text).text, T.get('Upload Catalyst Key'), ); expect($(backButton), findsOneWidget); expect($(nextButton), findsOneWidget); break; case RegistrationState.keychainCreateMnemonicVerified: - throw UnimplementedError(); + await $(registrationDetailsPanel) + .$(registrationDetailsTitle) + .waitUntilVisible(); + expect( + $(registrationDetailsPanel).$(registrationDetailsTitle).$(Text).text, + T.get("Nice job! You've successfully verified the seed phrase for " + 'your keychain.'), + ); + expect( + $(registrationDetailsPanel).$(registrationDetailsBody).$(Text).text, + isNotEmpty, + ); + expect( + $(nextStepTitle).$(Text).text, + T.get('Your next step'), + ); + expect( + $(nextStepBody).text, + T.get('Now let’s set your Unlock password ' + 'for this device!'), + ); + expect($(backButton), findsOneWidget); + expect($(nextButton), findsOneWidget); + break; + case RegistrationState.passwordInfo: + expect( + $(registrationDetailsPanel).$(registrationDetailsTitle).$(Text).text, + T.get('Set your Catalyst unlock password 
for this device'), + ); + expect( + $(registrationDetailsPanel).$(registrationDetailsBody).$(Text).text, + isNotEmpty, + ); + expect($(backButton), findsOneWidget); + expect($(nextButton), findsOneWidget); + break; + case RegistrationState.passwordInput: + expect( + $(registrationDetailsPanel).$(passwordInputField).$(Text).text, + T.get('Enter password'), + ); + expect( + $(registrationDetailsPanel).$(passwordConfirmInputField).$(Text).text, + T.get('Confirm password'), + ); + expect($(passwordStrengthLabel), findsNothing); + expect($(passwordStrengthIndicator), findsNothing); + expect($(backButton), findsOneWidget); + expect($(nextButton), findsOneWidget); + OnboardingPage.voicesButtonIsDisabled($, OnboardingPage.nextButton); + break; + case RegistrationState.keychainCreateSuccess: + expect( + $(finishAccountCreationPanel).$(Text).text, + T.get('Congratulations your Catalyst 
Keychain is created!'), + ); + expect( + $(finishAccountKeychainCreated).$(Text).text, + T.get('Catalyst Keychain created'), + ); + expect( + $(finishAccountLinkWallet).$(Text).text, + T.get('Link Cardano Wallet & Roles'), + ); + expect( + $(finishAccountAccountComplete).$(Text).text, + T.get('Catalyst account creation completed!'), + ); + expect( + $(nextStepTitle).$(Text).text, + T.get('Your next step'), + ); + expect( + $(nextStepBody).text, + T.get('In the next step you write your Catalyst roles and 
account ' + 'to the Cardano Mainnet.'), + ); + expect( + $(linkWalletAndRolesButton).$(Text).text, + T.get('Link your Cardano Wallet & Roles'), + ); + break; case RegistrationState.keychainRestoreChoice: - throw UnimplementedError(); + expect( + $(recoverKeychainMethodsTitle).text, + T.get('Restore your Catalyst Keychain'), + ); + expect( + $(onDeviceKeychainsWidget).$(keychainNotFoundIndicator).$(Text).text, + T.get('No Catalyst Keychain found
on this device.'), + ); + expect( + $(recoverKeychainMethodsSubtitle).text, + T.get('Not to worry, in the next step you can choose the recovery ' + 'option that applies to you for this device!'), + ); + expect( + $(recoverKeychainMethodsListTitle).text, + T.get('How do you want Restore your Catalyst Keychain?'), + ); + expect( + $(registrationTileTitle).text, + T.get('12 security words'), + ); + break; case RegistrationState.keychainRestoreMnemonicInfo: throw UnimplementedError(); case RegistrationState.keychainRestoreMnemonicInput: throw UnimplementedError(); case RegistrationState.keychainRestoreSuccess: throw UnimplementedError(); - case RegistrationState.passwordInfo: - throw UnimplementedError(); - case RegistrationState.passwordInput: - throw UnimplementedError(); - case RegistrationState.keychainCreateSuccess: - throw UnimplementedError(); case RegistrationState.linkWalletInfo: - throw UnimplementedError(); + expect( + $(registrationDetailsPanel).$(registrationDetailsTitle).$(Text).text, + T.get('Link Cardano Wallet & Catalyst Roles to you Catalyst ' + 'Keychain.'), + ); + expect( + $(registrationDetailsPanel).$(registrationDetailsBody).$(Text).text, + isNotEmpty, + ); + expect( + $(chooseCardanoWalletButton).$(Text).text, + T.get('Choose Cardano Wallet'), + ); + break; case RegistrationState.linkWalletSelect: - throw UnimplementedError(); + expect( + $(registrationDetailsPanel).$(registrationDetailsTitle).$(Text).text, + T.get( + 'Select the Cardano wallet to link\nto your Catalyst Keychain.', + ), + ); + expect( + $(registrationDetailsPanel).$(registrationDetailsBody).$(Text).text, + isNotEmpty, + ); + expect($(walletsLinkBuilder), findsOneWidget); + expect($(backButton), findsOneWidget); + expect($(seeAllSupportedWalletsBtn), findsOneWidget); + break; case RegistrationState.linkWalletSuccess: throw UnimplementedError(); case RegistrationState.rolesSelect: @@ -437,8 +593,77 @@ class OnboardingPage { static Future inputSeedPhrasesAreDisplayed(PatrolTester $) async { for (var i = 0; i < 12; i++) { - expect(await inputSeedPhrasePickerWord($, i), isNotEmpty); - expect(await inputSeedPhraseCompleterWord($, i), isNotEmpty); + expect(await inputSeedPhrasePickerText($, i), isNotEmpty); + expect(await inputSeedPhraseCompleterText($, i), isNotEmpty); + } + } + + static Future storeSeedPhrases(PatrolTester $) async { + for (var i = 0; i < 12; i++) { + final v1 = await writedownSeedPhraseWord($, i); + TestContext.save(key: 'word$i', value: v1); + } + } + + static Future enterStoredSeedPhrases(PatrolTester $) async { + for (var i = 0; i < 12; i++) { + await inputSeedPhrasePicker($, TestContext.get(key: 'word$i')).tap(); + } + } + + static Future enterPassword(PatrolTester $, String password) async { + await $(passwordInputField).enterText(password); + } + + static Future enterPasswordConfirm( + PatrolTester $, + String password, + ) async { + await $(passwordConfirmInputField).enterText(password); + } + + static void checkValidationIndicator( + PatrolTester $, + PasswordValidationStatus validationStatus, + ) { + expect($(passwordStrengthLabel), findsOneWidget); + + switch (validationStatus) { + case PasswordValidationStatus.weak: + expect( + $(passwordStrengthLabel).text, + T.get('Weak password strength'), + ); + break; + case PasswordValidationStatus.normal: + expect( + $(passwordStrengthLabel).text, + T.get('Normal password strength'), + ); + break; + case PasswordValidationStatus.good: + expect( + $(passwordStrengthLabel).text, + T.get('Good password strength'), + ); + break; + } + } + + static void passwordConfirmErrorIconIsShown( + PatrolTester $, { + bool reverse = false, + }) { + if (reverse) { + expect( + $(registrationDetailsPanel).$(CommonPage.voicesTextField).$(Icon), + findsNothing, + ); + } else { + expect( + $(registrationDetailsPanel).$(CommonPage.voicesTextField).$(Icon), + findsOneWidget, + ); } } } 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 index b355c211485..2c03468c3f7 100644 --- a/catalyst_voices/apps/voices/integration_test/pageobject/spaces_drawer_page.dart +++ b/catalyst_voices/apps/voices/integration_test/pageobject/spaces_drawer_page.dart @@ -116,11 +116,10 @@ class SpacesDrawerPage { $(userMenuContainer(Space.treasury)).$(userSectionHeader(Space.treasury)), findsOneWidget, ); - final children = find.descendant( - of: $(userMenuContainer(Space.treasury)), - matching: $(userDrawerMenuItem), + expect( + $(userMenuContainer(Space.treasury)).$(userDrawerMenuItem), + findsAtLeast(1), ); - expect($(children), findsAtLeast(1)); } static void userVotingLooksAsExpected(PatrolTester $) { @@ -132,11 +131,10 @@ class SpacesDrawerPage { $(userMenuContainer(Space.voting)).$(userSectionHeader(Space.voting)), findsOneWidget, ); - final children = find.descendant( - of: $(userMenuContainer(Space.voting)), - matching: $(userDrawerMenuItem), + expect( + $(userMenuContainer(Space.voting)).$(userDrawerMenuItem), + findsAtLeast(1), ); - expect($(children), findsAtLeast(1)); } static void userWorkspaceLooksAsExpected(PatrolTester $) { @@ -149,10 +147,5 @@ class SpacesDrawerPage { .$(userSectionHeader(Space.workspace)), findsOneWidget, ); - final children = find.descendant( - of: $(userMenuContainer(Space.workspace)), - matching: $(userDrawerMenuItem), - ); - expect($(children), findsAtLeast(1)); } } diff --git a/catalyst_voices/apps/voices/integration_test/suites/account_test.dart b/catalyst_voices/apps/voices/integration_test/suites/account_test.dart index 6bae0391148..3ebf83182d4 100644 --- a/catalyst_voices/apps/voices/integration_test/suites/account_test.dart +++ b/catalyst_voices/apps/voices/integration_test/suites/account_test.dart @@ -9,6 +9,7 @@ import 'package:patrol_finders/patrol_finders.dart'; import '../pageobject/account_dropdown_page.dart'; import '../pageobject/app_bar_page.dart'; import '../pageobject/overall_spaces_page.dart'; +import '../utils/constants.dart'; void main() async { late final GoRouter router; @@ -34,7 +35,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.userShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await $(AppBarPage.accountPopupBtn).tap(); await AccountDropdownPage.accountDropdownLooksAsExpected($); await AccountDropdownPage.accountDropdownContainsSpecificData($); diff --git a/catalyst_voices/apps/voices/integration_test/suites/app_test.dart b/catalyst_voices/apps/voices/integration_test/suites/app_test.dart index 11d0f9a6d30..b52e4d56bf3 100644 --- a/catalyst_voices/apps/voices/integration_test/suites/app_test.dart +++ b/catalyst_voices/apps/voices/integration_test/suites/app_test.dart @@ -10,6 +10,7 @@ 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/constants.dart'; import '../utils/selector_utils.dart'; void main() async { @@ -34,7 +35,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.visitorShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); expect($(AppBarPage.spacesDrawerButton).exists, false); }, ); @@ -46,7 +47,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.guestShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); SpacesDrawerPage.commonElementsLookAsExpected($); @@ -66,7 +67,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.guestShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); // iterate thru spaces by clicking next @@ -92,7 +93,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.userShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); SpacesDrawerPage.commonElementsLookAsExpected($); for (final space in Space.values) { @@ -107,7 +108,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.guestShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); await $(SpacesDrawerPage.allSpacesBtn).tap(); expect($(OverallSpacesPage.spacesListView), findsOneWidget); @@ -119,7 +120,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.userShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); await $(SpacesDrawerPage.allSpacesBtn).tap(); expect($(OverallSpacesPage.spacesListView), findsOneWidget); @@ -138,7 +139,7 @@ void main() async { }; await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.userShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await $(AppBarPage.spacesDrawerButton).waitUntilVisible().tap(); for (final space in Space.values) { await $(SpacesDrawerPage.chooserItem(space)).tap(); diff --git a/catalyst_voices/apps/voices/integration_test/suites/onboarding_test.dart b/catalyst_voices/apps/voices/integration_test/suites/onboarding_test.dart index 3a520bc4664..62d832c33cc 100644 --- a/catalyst_voices/apps/voices/integration_test/suites/onboarding_test.dart +++ b/catalyst_voices/apps/voices/integration_test/suites/onboarding_test.dart @@ -9,7 +9,11 @@ import 'package:patrol_finders/patrol_finders.dart'; import '../pageobject/app_bar_page.dart'; import '../pageobject/onboarding_page.dart'; import '../pageobject/overall_spaces_page.dart'; +import '../types/password_validation_states.dart'; import '../types/registration_state.dart'; +import '../utils/constants.dart'; +import '../utils/test_context.dart'; +import '../utils/translations_utils.dart'; void main() async { late final GoRouter router; @@ -25,6 +29,7 @@ void main() async { tearDown(() async { await restartDependencies(); + TestContext.clearContext(); }); group( @@ -35,7 +40,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(OverallSpacesPage.visitorShortcutBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await $(AppBarPage.getStartedBtn).tap(); expect($(OnboardingPage.registrationInfoPanel), findsOneWidget); expect($(OnboardingPage.registrationDetailsPanel), findsOneWidget); @@ -47,7 +52,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.onboardingScreenLooksAsExpected( $, RegistrationState.getStarted, @@ -60,7 +65,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.closeBtn($).tap(); expect($(OnboardingPage.registrationDialog), findsNothing); }, @@ -71,7 +76,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.onboardingScreenLooksAsExpected( $, @@ -85,7 +90,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await ($(OnboardingPage.backButton)).waitUntilVisible().tap(); await OnboardingPage.registrationInfoPanelLooksAsExpected( @@ -100,12 +105,12 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await OnboardingPage.onboardingScreenLooksAsExpected( $, - RegistrationState.createKeychainInfo, + RegistrationState.keychainCreated, ); }, ); @@ -115,7 +120,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await ($(OnboardingPage.backButton)).waitUntilVisible().tap(); @@ -130,7 +135,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await $(OnboardingPage.nextButton).tap(); @@ -145,7 +150,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await $(OnboardingPage.nextButton).tap(); @@ -162,7 +167,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await $(OnboardingPage.nextButton).tap(); @@ -170,10 +175,7 @@ void main() async { $, RegistrationState.keychainCreateMnemonicWritedown, ); - OnboardingPage.voicesFilledButtonIsDisabled( - $, - OnboardingPage.nextButton, - ); + OnboardingPage.voicesButtonIsDisabled($, OnboardingPage.nextButton); }, ); @@ -182,7 +184,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await $(OnboardingPage.nextButton).tap(); @@ -200,7 +202,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await $(OnboardingPage.nextButton).tap(); @@ -219,7 +221,7 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await $(OnboardingPage.nextButton).tap(); @@ -227,6 +229,7 @@ void main() async { await $(OnboardingPage.nextButton).tap(); await $(OnboardingPage.nextButton).tap(); //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 await $(OnboardingPage.resetButton).tap(); await OnboardingPage.onboardingScreenLooksAsExpected( $, @@ -240,13 +243,86 @@ void main() async { (PatrolTester $) async { await $.pumpWidgetAndSettle(App(routerConfig: router)); await $(AppBarPage.getStartedBtn) - .tap(settleTimeout: const Duration(seconds: 10)); + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await ($(OnboardingPage.backButton)).waitUntilVisible().tap(); + await OnboardingPage.registrationInfoPanelLooksAsExpected( + $, + RegistrationState.keychainCreateMnemonicInputInfo, + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - mnemonic input - correct words unlock next button', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531//temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + OnboardingPage.voicesButtonIsEnabled($, OnboardingPage.nextButton); + }, + ); + + patrolWidgetTest( + 'visitor - create - mnemonic input verified screen looks OK', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.keychainCreateMnemonicVerified, + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - mnemonic input verified screen back button works', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); await $(OnboardingPage.nextButton).tap(); await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); await ($(OnboardingPage.backButton)).waitUntilVisible().tap(); await OnboardingPage.registrationInfoPanelLooksAsExpected( $, @@ -254,6 +330,453 @@ void main() async { ); }, ); + + patrolWidgetTest( + 'visitor - create - password info screen looks OK', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.passwordInfo, + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - password info screen back button works', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await ($(OnboardingPage.backButton)).waitUntilVisible().tap(); + expect( + $(OnboardingPage.nextStepBody).text, + T.get('Now let’s set your Unlock password ' + 'for this device!'), + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - password input screen looks OK', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.passwordInput, + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - password input screen back button works', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await ($(OnboardingPage.backButton)).waitUntilVisible().tap(); + await OnboardingPage.registrationDetailsPanelLooksAsExpected( + $, + RegistrationState.passwordInfo, + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - password input - valid minimum length password', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test1234'); + await OnboardingPage.enterPasswordConfirm($, 'Test1234'); + OnboardingPage.passwordConfirmErrorIconIsShown( + $, + reverse: true, + ); + OnboardingPage.checkValidationIndicator( + $, + PasswordValidationStatus.normal, + ); + OnboardingPage.passwordConfirmErrorIconIsShown( + $, + reverse: true, + ); + OnboardingPage.voicesButtonIsEnabled($, OnboardingPage.nextButton); + }, + ); + + patrolWidgetTest( + 'visitor - create - password input - valid long password', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test1234Test1234'); + await OnboardingPage.enterPasswordConfirm($, 'Test1234Test1234'); + OnboardingPage.checkValidationIndicator( + $, + PasswordValidationStatus.good, + ); + OnboardingPage.passwordConfirmErrorIconIsShown( + $, + reverse: true, + ); + OnboardingPage.voicesButtonIsEnabled($, OnboardingPage.nextButton); + }, + ); + + patrolWidgetTest( + 'visitor - create - password input - too short password', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test123'); + OnboardingPage.checkValidationIndicator( + $, + PasswordValidationStatus.weak, + ); + OnboardingPage.passwordConfirmErrorIconIsShown( + $, + reverse: true, + ); + OnboardingPage.voicesButtonIsDisabled($, OnboardingPage.nextButton); + }, + ); + + patrolWidgetTest( + 'visitor - create - password input - valid password, no confirmation', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test1234'); + OnboardingPage.checkValidationIndicator( + $, + PasswordValidationStatus.normal, + ); + OnboardingPage.passwordConfirmErrorIconIsShown( + $, + reverse: true, + ); + OnboardingPage.voicesButtonIsDisabled($, OnboardingPage.nextButton); + }, + ); + + patrolWidgetTest( + 'visitor - create - password input - not matching confirmation', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test1234'); + await OnboardingPage.enterPasswordConfirm($, 'Test123'); + OnboardingPage.checkValidationIndicator( + $, + PasswordValidationStatus.normal, + ); + OnboardingPage.passwordConfirmErrorIconIsShown($); + OnboardingPage.voicesButtonIsDisabled($, OnboardingPage.nextButton); + }, + ); + + patrolWidgetTest( + 'visitor - create - keychain created success screen looks OK', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test1234'); + await OnboardingPage.enterPasswordConfirm($, 'Test1234'); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.keychainCreateSuccess, + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - link wallet info screen looks OK', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test1234'); + await OnboardingPage.enterPasswordConfirm($, 'Test1234'); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.linkWalletAndRolesButton).tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.linkWalletInfo, + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - link wallet select screen looks OK', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test1234'); + await OnboardingPage.enterPasswordConfirm($, 'Test1234'); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.linkWalletAndRolesButton).tap(); + await $(OnboardingPage.chooseCardanoWalletButton).tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.linkWalletSelect, + ); + }, + ); + + patrolWidgetTest( + 'visitor - create - link wallet select screen back button works', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedCreateNewBtn($).tap(); + await OnboardingPage.detailsPartCreateKeychainBtn($).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.storeSeedPhrases($); + await $(OnboardingPage.seedPhraseStoredCheckbox).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + //temporary: remove reset when seeds are no longer prefilled + //https://github.com/input-output-hk/catalyst-voices/issues/1531 + await $(OnboardingPage.resetButton).tap(); + await OnboardingPage.enterStoredSeedPhrases($); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.nextButton).tap(); + await OnboardingPage.enterPassword($, 'Test1234'); + await OnboardingPage.enterPasswordConfirm($, 'Test1234'); + await $(OnboardingPage.nextButton).tap(); + await $(OnboardingPage.linkWalletAndRolesButton).tap(); + await $(OnboardingPage.chooseCardanoWalletButton).tap(); + await ($(OnboardingPage.backButton)).waitUntilVisible().tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.linkWalletInfo, + ); + }, + ); + + patrolWidgetTest( + 'visitor - restore - keychain choice screen looks OK', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedRecoverBtn($).tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.keychainRestoreChoice, + ); + }, + ); + + patrolWidgetTest( + 'visitor - restore - keychain choice screen back button works', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn) + .tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedRecoverBtn($).tap(); + await ($(OnboardingPage.backButton)).waitUntilVisible().tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.getStarted, + ); + }, + ); + }, + skip: true, + ); + + patrolWidgetTest( + 'visitor - restore - keychain choice screen looks OK', + (PatrolTester $) async { + await $.pumpWidgetAndSettle(App(routerConfig: router)); + await $(AppBarPage.getStartedBtn).tap(settleTimeout: Time.long.duration); + await OnboardingPage.detailsPartGetStartedRecoverBtn($).tap(); + await OnboardingPage.onboardingScreenLooksAsExpected( + $, + RegistrationState.keychainRestoreChoice, + ); }, ); } diff --git a/catalyst_voices/apps/voices/integration_test/types/password_validation_states.dart b/catalyst_voices/apps/voices/integration_test/types/password_validation_states.dart new file mode 100644 index 00000000000..a0ee5e6297b --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/types/password_validation_states.dart @@ -0,0 +1,5 @@ +enum PasswordValidationStatus { + weak, + normal, + good; +} diff --git a/catalyst_voices/apps/voices/integration_test/utils/constants.dart b/catalyst_voices/apps/voices/integration_test/utils/constants.dart new file mode 100644 index 00000000000..b03074474cd --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/utils/constants.dart @@ -0,0 +1,9 @@ +enum Time { + veryShort(Duration(milliseconds: 500)), + short(Duration(seconds: 2)), + long(Duration(minutes: 10)); + + final Duration duration; + + const Time(this.duration); +} diff --git a/catalyst_voices/apps/voices/integration_test/utils/selector_utils.dart b/catalyst_voices/apps/voices/integration_test/utils/selector_utils.dart index 67a0c980078..9ab6c5b57e6 100644 --- a/catalyst_voices/apps/voices/integration_test/utils/selector_utils.dart +++ b/catalyst_voices/apps/voices/integration_test/utils/selector_utils.dart @@ -7,11 +7,11 @@ class SelectorUtils { PatrolFinder widget, { bool? reverse = false, }) { - final widgetProps = $.tester.widget(widget).toString().split('(').last; + final dynamic widgetProps = $.tester.widget(widget); final expectedState = reverse! ? 'enabled' : 'disabled'; expect( - widgetProps.contains('disabled'), - !reverse, + widgetProps.enabled, // ignore: avoid_dynamic_calls + reverse, reason: 'Expected $expectedState (${widget.description})', ); } diff --git a/catalyst_voices/apps/voices/integration_test/utils/test_context.dart b/catalyst_voices/apps/voices/integration_test/utils/test_context.dart new file mode 100644 index 00000000000..b3c951aecac --- /dev/null +++ b/catalyst_voices/apps/voices/integration_test/utils/test_context.dart @@ -0,0 +1,42 @@ +class TestContext { + TestContext._privateConstructor(); + static final TestContext instance = TestContext._privateConstructor(); + Map context = {}; + + static bool has({required String key}) { + return TestContext.instance.context.containsKey(key); + } + + static void save({required String key, required String value}) { + if (has(key: key)) { + throw Exception('You tried to override "$key" property. Its not allowed'); + } + TestContext.instance.context[key] = value; + } + + static void delete({required String key}) { + if (has(key: key)) { + TestContext.instance.context.remove(key); + } + } + + static void saveWithOverride({required String key, required String value}) { + if (has(key: key)) { + delete(key: key); + } + TestContext.instance.context[key] = value; + } + + static String get({required String key}) { + if (has(key: key)) { + return TestContext.instance.context[key]!; + } + throw Exception( + 'You tried to access $key property, but it does not exist.', + ); + } + + static void clearContext() { + TestContext.instance.context.clear(); + } +} diff --git a/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart deleted file mode 100644 index 5ab4aed3de9..00000000000 --- a/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; -import 'package:catalyst_voices_localization/generated/catalyst_voices_localizations.dart'; -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; - -extension GuidanceExt on GuidanceType { - String localizedType(VoicesLocalizations localizations) => switch (this) { - GuidanceType.mandatory => localizations.mandatoryGuidanceType, - GuidanceType.education => localizations.educationGuidanceType, - GuidanceType.tips => localizations.tipsGuidanceType, - }; - - // TODO(LynxLynxx): when designers will - // provide us with icon, change here accordingly - SvgGenImage get icon { - return switch (this) { - GuidanceType.education => VoicesAssets.icons.newspaper, - GuidanceType.mandatory => VoicesAssets.icons.newspaper, - GuidanceType.tips => VoicesAssets.icons.newspaper, - }; - } -} diff --git a/catalyst_voices/apps/voices/lib/common/ext/space_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/space_ext.dart index b71cfe26f93..c399cc5c4ea 100644 --- a/catalyst_voices/apps/voices/lib/common/ext/space_ext.dart +++ b/catalyst_voices/apps/voices/lib/common/ext/space_ext.dart @@ -41,19 +41,19 @@ extension SpaceExt on Space { Color backgroundColor(BuildContext context) => switch (this) { Space.discovery => - Theme.of(context).colors.iconsSecondary!.withOpacity(0.16), + Theme.of(context).colors.iconsSecondary.withOpacity(0.16), Space.workspace => Theme.of(context).colorScheme.primaryContainer, - Space.voting => Theme.of(context).colors.warningContainer!, + Space.voting => Theme.of(context).colors.warningContainer, Space.fundedProjects => - Theme.of(context).colors.iconsSecondary!.withOpacity(0.16), - Space.treasury => Theme.of(context).colors.successContainer!, + Theme.of(context).colors.iconsSecondary.withOpacity(0.16), + Space.treasury => Theme.of(context).colors.successContainer, }; Color foregroundColor(BuildContext context) => switch (this) { - Space.discovery => Theme.of(context).colors.iconsSecondary!, + Space.discovery => Theme.of(context).colors.iconsSecondary, Space.workspace => Theme.of(context).colorScheme.primary, - Space.voting => Theme.of(context).colors.iconsWarning!, - Space.fundedProjects => Theme.of(context).colors.iconsSecondary!, - Space.treasury => Theme.of(context).colors.iconsSuccess!, + Space.voting => Theme.of(context).colors.iconsWarning, + Space.fundedProjects => Theme.of(context).colors.iconsSecondary, + Space.treasury => Theme.of(context).colors.iconsSuccess, }; } diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart index 3912fd712a8..98e91faaeb9 100644 --- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart @@ -26,8 +26,9 @@ final class Dependencies extends DependencyProvider { registerSingleton(config); _registerStorages(); - _registerServices(); + _registerUtils(); _registerRepositories(); + _registerServices(); _registerBlocsWithDependencies(); _isInitialized = true; @@ -80,7 +81,7 @@ final class Dependencies extends DependencyProvider { ) ..registerFactory(() { return CampaignDetailsBloc( - get(), + get(), ); }) ..registerLazySingleton(() { @@ -101,6 +102,7 @@ final class Dependencies extends DependencyProvider { ..registerFactory(() { return ProposalBuilderBloc( get(), + get(), ); }); } @@ -118,6 +120,11 @@ final class Dependencies extends DependencyProvider { get(), get(), ); + }) + ..registerLazySingleton(() { + return DocumentRepository( + get(), + ); }); } @@ -157,11 +164,13 @@ final class Dependencies extends DependencyProvider { registerLazySingleton(() { return CampaignService( get(), + get(), ); }); registerLazySingleton(() { return ProposalService( get(), + get(), ); }); registerLazySingleton(() { @@ -176,4 +185,8 @@ final class Dependencies extends DependencyProvider { registerLazySingleton(SharedPreferencesAsync.new); registerLazySingleton(SecureUserStorage.new); } + + void _registerUtils() { + registerLazySingleton(SignedDocumentManager.new); + } } diff --git a/catalyst_voices/apps/voices/lib/pages/account/account_page.dart b/catalyst_voices/apps/voices/lib/pages/account/account_page.dart index c288bbfaaf8..8862b4f0794 100644 --- a/catalyst_voices/apps/voices/lib/pages/account/account_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/account/account_page.dart @@ -120,7 +120,7 @@ class _KeychainCard extends StatelessWidget { Radius.circular(16), ), border: Border.all( - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, width: 1, ), color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, 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 index c83dd1977db..634a1671534 100644 --- 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 @@ -163,7 +163,7 @@ class CampaignAdminToolsDialog extends StatelessWidget { color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, borderRadius: BorderRadius.circular(16), border: Border.all( - color: Theme.of(context).colors.onSurfaceNeutral012!, + color: Theme.of(context).colors.onSurfaceNeutral012, width: 1, ), ), 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 index 0e1b9463dde..3ca93482df5 100644 --- 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 @@ -82,7 +82,7 @@ class _CampaignStatusIndicator extends StatelessWidget { decoration: BoxDecoration( color: currentStatus == campaignStatus ? theme.colors.success - : theme.colors.onSurfaceNeutral012?.withOpacity(.12), + : theme.colors.onSurfaceNeutral012.withOpacity(.12), borderRadius: BorderRadius.circular(8), ), child: Padding( 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 b4db9d6f1ec..ece5d20da1e 100644 --- a/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/discovery/discovery_page.dart @@ -112,7 +112,7 @@ class _Segment extends StatelessWidget { child: Container( decoration: BoxDecoration( color: theme.colors.elevationsOnSurfaceNeutralLv1White, - border: Border.all(color: theme.colors.outlineBorderVariant!), + border: Border.all(color: theme.colors.outlineBorderVariant), borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.all(16), diff --git a/catalyst_voices/apps/voices/lib/pages/overall_spaces/brands_navigation.dart b/catalyst_voices/apps/voices/lib/pages/overall_spaces/brands_navigation.dart index 72082995827..40373a17d76 100644 --- a/catalyst_voices/apps/voices/lib/pages/overall_spaces/brands_navigation.dart +++ b/catalyst_voices/apps/voices/lib/pages/overall_spaces/brands_navigation.dart @@ -166,7 +166,7 @@ final class _BackgroundColor implements WidgetStateProperty { @override Color? resolve(Set states) { if (states.contains(WidgetState.selected)) { - return colors.onSurfacePrimaryContainer?.withOpacity(0.12); + return colors.onSurfacePrimaryContainer.withOpacity(0.12); } return Colors.transparent; @@ -181,7 +181,7 @@ final class _ForegroundColor implements WidgetStateProperty { @override Color? resolve(Set states) { if (states.contains(WidgetState.disabled)) { - return colors.textOnPrimaryLevel0?.withOpacity(0.3); + return colors.textOnPrimaryLevel0.withOpacity(0.3); } return colors.textOnPrimaryLevel0; diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_body.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_body.dart deleted file mode 100644 index 3f3adc7a7b0..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_body.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:catalyst_voices/widgets/widgets.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 ProposalBuilderBody extends StatelessWidget { - final ItemScrollController itemScrollController; - - const ProposalBuilderBody({ - super.key, - required this.itemScrollController, - }); - - @override - Widget build(BuildContext context) { - return SegmentsListViewBuilder( - builder: (context, value, child) { - return SegmentsListView( - itemScrollController: itemScrollController, - items: value, - sectionBuilder: (context, data) { - return Text( - '${data.id}', - ); - }, - ); - }, - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_error.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_error.dart new file mode 100644 index 00000000000..ebeaf273bf2 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_error.dart @@ -0,0 +1,53 @@ +import 'package:catalyst_voices/widgets/widgets.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 ProposalBuilderErrorSelector extends StatelessWidget { + final VoidCallback onRetryTap; + + const ProposalBuilderErrorSelector({ + super.key, + required this.onRetryTap, + }); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.showError ? state.error : null, + builder: (context, state) { + final errorMessage = state?.message(context); + + return Offstage( + offstage: errorMessage == null, + child: _ProposalBuilderError( + message: errorMessage ?? '', + onRetryTap: onRetryTap, + ), + ); + }, + ); + } +} + +class _ProposalBuilderError extends StatelessWidget { + final String message; + final VoidCallback onRetryTap; + + const _ProposalBuilderError({ + required this.message, + required this.onRetryTap, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: VoicesErrorIndicator( + message: message, + onRetry: onRetryTap, + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_guidance_view.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_guidance_view.dart deleted file mode 100644 index a214deeb5b0..00000000000 --- a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_guidance_view.dart +++ /dev/null @@ -1,91 +0,0 @@ -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_models/catalyst_voices_models.dart'; -import 'package:flutter/material.dart'; - -class ProposalBuilderGuidanceView extends StatefulWidget { - final List guidances; - - const ProposalBuilderGuidanceView(this.guidances, {super.key}); - - @override - State createState() { - return _ProposalBuilderGuidanceViewState(); - } -} - -class _ProposalBuilderGuidanceViewState - extends State { - final List filteredGuidances = []; - - GuidanceType? selectedType; - - @override - void initState() { - super.initState(); - filteredGuidances - ..clear() - ..addAll(widget.guidances); - } - - @override - void didUpdateWidget(ProposalBuilderGuidanceView oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.guidances != widget.guidances) { - filteredGuidances - ..clear() - ..addAll(widget.guidances); - _filterGuidances(selectedType); - } - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - FilterByDropdown( - items: GuidanceType.values - .map( - (e) => VoicesDropdownMenuEntry( - label: e.localizedType(context.l10n), - value: e, - context: context, - ), - ) - .toList(), - onChanged: (value) { - setState(() { - _filterGuidances(value); - }); - }, - value: selectedType, - ), - if (filteredGuidances.isEmpty) - Center( - child: Text(context.l10n.noGuidanceOfThisType), - ), - Column( - children: filteredGuidances - .sortedByWeight() - .toList() - .map((e) => GuidanceCard(guidance: e)) - .toList(), - ), - ], - ); - } - - void _filterGuidances(GuidanceType? type) { - selectedType = type; - filteredGuidances - ..clear() - ..addAll( - type == null - ? widget.guidances - : widget.guidances.where((e) => e.type == type).toList(), - ); - } -} diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_loading.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_loading.dart new file mode 100644 index 00000000000..dde67558770 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_loading.dart @@ -0,0 +1,39 @@ +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 ProposalBuilderLoadingSelector extends StatelessWidget { + const ProposalBuilderLoadingSelector({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 _ProposalBuilderLoading(), + ), + ); + }, + ); + } +} + +class _ProposalBuilderLoading extends StatelessWidget { + const _ProposalBuilderLoading(); + + @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/proposal_builder/proposal_builder_page.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart index 06f283bae83..c8b8930a65a 100644 --- a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_page.dart @@ -1,11 +1,12 @@ import 'dart:async'; -import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_body.dart'; +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_error.dart'; +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_loading.dart'; import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_navigation_panel.dart'; +import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_segments.dart'; import 'package:catalyst_voices/pages/proposal_builder/proposal_builder_setup_panel.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:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -13,11 +14,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class ProposalBuilderPage extends StatefulWidget { - final String proposalId; + final String? proposalId; + final String? templateId; const ProposalBuilderPage({ super.key, - required this.proposalId, + this.proposalId, + this.templateId, }); @override @@ -26,9 +29,8 @@ class ProposalBuilderPage extends StatefulWidget { class _ProposalBuilderPageState extends State { late final SegmentsController _segmentsController; - late final ItemScrollController _bodyItemScrollController; + late final ItemScrollController _segmentsScrollController; - NodeId? _activeSegmentId; StreamSubscription>? _segmentsSub; @override @@ -38,27 +40,27 @@ class _ProposalBuilderPageState extends State { final bloc = context.read(); _segmentsController = SegmentsController(); - _bodyItemScrollController = ItemScrollController(); + _segmentsScrollController = ItemScrollController(); _segmentsController - ..addListener(_handleSectionsControllerChange) - ..attachItemsScrollController(_bodyItemScrollController); + ..addListener(_handleSegmentsControllerChange) + ..attachItemsScrollController(_segmentsScrollController); _segmentsSub = bloc.stream .map((event) => event.segments) .distinct(listEquals) .listen(_updateSegments); - bloc.add(LoadProposalEvent(id: widget.proposalId)); + _updateSource(bloc: bloc); } @override void didUpdateWidget(covariant ProposalBuilderPage oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.proposalId != oldWidget.proposalId) { - final event = LoadProposalEvent(id: widget.proposalId); - context.read().add(event); + if (widget.proposalId != oldWidget.proposalId || + widget.templateId != oldWidget.templateId) { + _updateSource(); } } @@ -77,8 +79,15 @@ class _ProposalBuilderPageState extends State { controller: _segmentsController, child: SpaceScaffold( left: const ProposalBuilderNavigationPanel(), - body: ProposalBuilderBody( - itemScrollController: _bodyItemScrollController, + body: Stack( + fit: StackFit.expand, + children: [ + ProposalBuilderErrorSelector(onRetryTap: _updateSource), + ProposalBuilderSegmentsSelector( + itemScrollController: _segmentsScrollController, + ), + const ProposalBuilderLoadingSelector(), + ], ), right: const ProposalBuilderSetupPanel(), ), @@ -95,14 +104,31 @@ class _ProposalBuilderPageState extends State { _segmentsController.value = newState; } - void _handleSectionsControllerChange() { + void _handleSegmentsControllerChange() { final activeSectionId = _segmentsController.value.activeSectionId; - if (_activeSegmentId != activeSectionId) { - _activeSegmentId = activeSectionId; + final event = ActiveNodeChangedEvent(activeSectionId); + context.read().add(event); + } + + void _updateSource({ + ProposalBuilderBloc? bloc, + }) { + bloc ??= context.read(); + + final proposalId = widget.proposalId; + final templateId = widget.templateId; - final event = ActiveStepChangedEvent(activeSectionId); - context.read().add(event); + if (proposalId != null) { + bloc.add(LoadProposalEvent(id: proposalId)); + return; } + + if (templateId != null) { + bloc.add(LoadProposalTemplateEvent(id: templateId)); + return; + } + + bloc.add(const LoadDefaultProposalTemplateEvent()); } } diff --git a/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_segments.dart b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_segments.dart new file mode 100644 index 00000000000..7973c14d023 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/pages/proposal_builder/proposal_builder_segments.dart @@ -0,0 +1,60 @@ +import 'package:catalyst_voices/widgets/widgets.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'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +class ProposalBuilderSegmentsSelector extends StatelessWidget { + final ItemScrollController itemScrollController; + + const ProposalBuilderSegmentsSelector({ + super.key, + required this.itemScrollController, + }); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.showSegments, + builder: (context, state) { + return Offstage( + offstage: !state, + child: _ProposalBuilderSegments( + itemScrollController: itemScrollController, + ), + ); + }, + ); + } +} + +class _ProposalBuilderSegments extends StatelessWidget { + final ItemScrollController itemScrollController; + + const _ProposalBuilderSegments({ + required this.itemScrollController, + }); + + @override + Widget build(BuildContext context) { + return SegmentsListViewBuilder( + builder: (context, value, child) { + return SegmentsListView( + itemScrollController: itemScrollController, + items: value, + sectionBuilder: (context, data) { + return DocumentBuilderSectionTile( + key: key, + section: data.documentSection, + onChanged: (value) { + final event = SectionChangedEvent(changes: value); + context.read().add(event); + }, + ); + }, + ); + }, + ); + } +} 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 index 7589a0134d7..fcfcf1a9f98 100644 --- 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 @@ -1,4 +1,3 @@ -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'; @@ -55,7 +54,8 @@ class _GuidanceSelector extends StatelessWidget { } else if (state.showEmptyState) { return Text(context.l10n.noGuidanceForThisSection); } else { - return ProposalBuilderGuidanceView(state.guidances); + // TODO(damian-molinski): not implemented using rich text. + return const Text(''); } }, ); diff --git a/catalyst_voices/apps/voices/lib/pages/registration/finish_account/finish_account_creation_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/finish_account/finish_account_creation_panel.dart index 137d69cb600..9e2c3140b1c 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/finish_account/finish_account_creation_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/finish_account/finish_account_creation_panel.dart @@ -13,6 +13,7 @@ class FinishAccountCreationPanel extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + key: const Key('FinishAccountCreationPanel'), crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 24), @@ -78,6 +79,7 @@ class _LinkWalletAndRolesButton extends StatelessWidget { @override Widget build(BuildContext context) { return VoicesFilledButton( + key: const Key('LinkWalletAndRolesButton'), onTap: onTap, leading: VoicesAssets.icons.wallet.buildIcon(size: 18), child: Text(context.l10n.createKeychainLinkWalletAndRoles), diff --git a/catalyst_voices/apps/voices/lib/pages/registration/recover/recover_method_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/recover/recover_method_panel.dart index eb6b9c49d0c..93697d74aba 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/recover/recover_method_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/recover/recover_method_panel.dart @@ -24,6 +24,7 @@ class RecoverMethodPanel extends StatelessWidget { children: [ const SizedBox(height: 24), Text( + key: const Key('RecoverKeychainMethodsTitle'), context.l10n.recoverKeychainMethodsTitle, style: theme.textTheme.titleMedium?.copyWith(color: colorLvl1), ), @@ -31,11 +32,13 @@ class RecoverMethodPanel extends StatelessWidget { _BlocOnDeviceKeychains(onUnlockTap: _unlockKeychain), const SizedBox(height: 12), Text( + key: const Key('RecoverKeychainMethodsSubtitle'), context.l10n.recoverKeychainMethodsSubtitle, style: theme.textTheme.bodyMedium?.copyWith(color: colorLvl1), ), const SizedBox(height: 32), Text( + key: const Key('RecoverKeychainMethodsListTitle'), context.l10n.recoverKeychainMethodsListTitle, style: theme.textTheme.titleSmall?.copyWith(color: colorLvl0), ), @@ -89,6 +92,7 @@ class _BlocOnDeviceKeychains extends StatelessWidget { @override Widget build(BuildContext context) { return BlocRecoverBuilder( + key: const Key('BlocOnDeviceKeychains'), selector: (state) => state.foundKeychain, builder: (context, state) { return _OnDeviceKeychains( @@ -151,6 +155,7 @@ class _KeychainNotFoundIndicator extends StatelessWidget { @override Widget build(BuildContext context) { return VoicesIndicator( + key: const Key('KeychainNotFoundIndicator'), type: VoicesIndicatorType.error, icon: VoicesAssets.icons.exclamation, message: Text( diff --git a/catalyst_voices/apps/voices/lib/pages/registration/upload_seed_phrase_confirmation_dialog.dart b/catalyst_voices/apps/voices/lib/pages/registration/upload_seed_phrase_confirmation_dialog.dart index d5fec619e9e..e3cb38e26d4 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/upload_seed_phrase_confirmation_dialog.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/upload_seed_phrase_confirmation_dialog.dart @@ -34,7 +34,7 @@ class UploadSeedPhraseConfirmationDialog extends StatelessWidget { color: Theme.of(context).colors.iconsError, ), border: Border.all( - color: Theme.of(context).colors.iconsError!, + color: Theme.of(context).colors.iconsError, width: 3, ), ), diff --git a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/account_role_dialog.dart b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/account_role_dialog.dart index 1b670d444ec..270bba61c30 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/account_role_dialog.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/account_role_dialog.dart @@ -134,7 +134,7 @@ class _InfoContainer extends StatelessWidget { color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, borderRadius: BorderRadius.circular(10), border: Border.all( - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, ), ), child: child, diff --git a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/intro_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/intro_panel.dart index 7a1de2efca9..512fe5f4b80 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/intro_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/intro_panel.dart @@ -20,6 +20,7 @@ class IntroPanel extends StatelessWidget { ), const Spacer(), VoicesFilledButton( + key: const Key('ChooseCardanoWalletButton'), leading: VoicesAssets.icons.wallet.buildIcon(), onTap: () { RegistrationCubit.of(context).nextStep(); diff --git a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart index 11e4598ee23..c3c0f25358d 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart @@ -166,7 +166,7 @@ class _Summary extends StatelessWidget { borderRadius: BorderRadius.circular(8), border: Border.all( width: 1.5, - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, ), ), child: Column( diff --git a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/select_wallet_panel.dart b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/select_wallet_panel.dart index 0a962b81d54..d4520daab78 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/select_wallet_panel.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/wallet_link/stage/select_wallet_panel.dart @@ -60,6 +60,7 @@ class _SelectWalletPanelState extends State { ), const SizedBox(height: 10), VoicesTextButton( + key: const Key('SeeAllSupportedWalletsButton'), trailing: VoicesAssets.icons.externalLink.buildIcon(), onTap: () async => _launchSupportedWalletsLink(), child: Text(context.l10n.seeAllSupportedWallets), @@ -101,6 +102,7 @@ class _BlocWallets extends StatelessWidget { @override Widget build(BuildContext context) { return BlocWalletLinkBuilder, Exception>?>( + key: const Key('WalletsLinkBuilder'), selector: (state) => state.wallets, builder: (context, state) { return _Wallets( diff --git a/catalyst_voices/apps/voices/lib/pages/registration/widgets/exit_confirm_dialog.dart b/catalyst_voices/apps/voices/lib/pages/registration/widgets/exit_confirm_dialog.dart index 95d0948a673..e952ba0bd53 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/widgets/exit_confirm_dialog.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/widgets/exit_confirm_dialog.dart @@ -80,7 +80,7 @@ class _WarningIcon extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final color = theme.colors.iconsError!; + final color = theme.colors.iconsError; return VoicesAvatar( border: Border.all( diff --git a/catalyst_voices/apps/voices/lib/pages/registration/widgets/next_step.dart b/catalyst_voices/apps/voices/lib/pages/registration/widgets/next_step.dart index ce7bec1065c..7f2309cab32 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/widgets/next_step.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/widgets/next_step.dart @@ -25,6 +25,7 @@ class NextStep extends StatelessWidget { const SizedBox(height: 12), if (data != null) ...[ Text( + key: const Key('NextStepBody'), data, style: theme.textTheme.bodySmall?.copyWith(color: textColor), textAlign: TextAlign.center, diff --git a/catalyst_voices/apps/voices/lib/pages/registration/widgets/registration_tile.dart b/catalyst_voices/apps/voices/lib/pages/registration/widgets/registration_tile.dart index 0db8b0ed560..dca3b9d14a6 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/widgets/registration_tile.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/widgets/registration_tile.dart @@ -47,6 +47,7 @@ class RegistrationTile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( + key: const Key('RegistrationTileTitle'), title, maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/catalyst_voices/apps/voices/lib/pages/registration/widgets/unlock_password_form.dart b/catalyst_voices/apps/voices/lib/pages/registration/widgets/unlock_password_form.dart index 54bbf18a171..5fb20445671 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/widgets/unlock_password_form.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/widgets/unlock_password_form.dart @@ -55,6 +55,7 @@ class _UnlockPasswordTextField extends StatelessWidget { @override Widget build(BuildContext context) { return VoicesPasswordTextField( + key: const Key('PasswordInputField'), controller: controller, textInputAction: TextInputAction.next, decoration: VoicesTextFieldDecoration( @@ -78,6 +79,7 @@ class _ConfirmUnlockPasswordTextField extends StatelessWidget { @override Widget build(BuildContext context) { return VoicesPasswordTextField( + key: const Key('PasswordConfirmInputField'), controller: controller, decoration: VoicesTextFieldDecoration( labelText: context.l10n.confirmPassword, diff --git a/catalyst_voices/apps/voices/lib/pages/registration/widgets/wallet_summary.dart b/catalyst_voices/apps/voices/lib/pages/registration/widgets/wallet_summary.dart index 59006f07430..a17839ef699 100644 --- a/catalyst_voices/apps/voices/lib/pages/registration/widgets/wallet_summary.dart +++ b/catalyst_voices/apps/voices/lib/pages/registration/widgets/wallet_summary.dart @@ -27,7 +27,7 @@ class WalletSummary extends StatelessWidget { borderRadius: BorderRadius.circular(8), border: Border.all( width: 1.5, - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, ), ), child: Column( diff --git a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_header.dart b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_header.dart index e482a01ffa6..7e3992649b7 100644 --- a/catalyst_voices/apps/voices/lib/pages/workspace/workspace_header.dart +++ b/catalyst_voices/apps/voices/lib/pages/workspace/workspace_header.dart @@ -121,12 +121,8 @@ class _DraftProposalButton extends StatelessWidget { @override Widget build(BuildContext context) { return VoicesFilledButton( - onTap: () async { - final id = await context.read().createNewDraftProposal(); - - if (context.mounted) { - ProposalBuilderRoute(proposalId: id).go(context); - } + onTap: () { + const ProposalBuilderDraftRoute().go(context); }, leading: VoicesAssets.icons.plus.buildIcon(), child: Text(context.l10n.newDraftProposal), 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 fdb45992f80..ed98c88d338 100644 --- a/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.dart +++ b/catalyst_voices/apps/voices/lib/routes/routing/spaces_route.dart @@ -22,8 +22,17 @@ const _prefix = Routes.currentMilestone; @TypedShellRoute( routes: >[ TypedGoRoute(path: '/$_prefix/discovery'), - TypedGoRoute(path: '/$_prefix/workspace'), - TypedGoRoute(path: '/$_prefix/workspace/:proposalId'), + TypedGoRoute( + path: '/$_prefix/workspace', + routes: [ + TypedGoRoute( + path: 'proposal_builder/draft', + ), + TypedGoRoute( + path: 'proposal_builder/:proposalId', + ), + ], + ), TypedGoRoute(path: '/$_prefix/voting'), TypedGoRoute(path: '/$_prefix/funded_projects'), TypedGoRoute(path: '/$_prefix/treasury'), @@ -79,9 +88,9 @@ final class WorkspaceRoute extends GoRouteData const WorkspaceRoute(); @override - List get routeGuards => [ - const SessionUnlockedGuard(), - const UserAccessGuard(), + List get routeGuards => const [ + SessionUnlockedGuard(), + UserAccessGuard(), ]; @override @@ -90,6 +99,26 @@ final class WorkspaceRoute extends GoRouteData } } +final class ProposalBuilderDraftRoute extends GoRouteData + with FadePageTransitionMixin, CompositeRouteGuardMixin { + final String? templateId; + + const ProposalBuilderDraftRoute({ + this.templateId, + }); + + @override + List get routeGuards => const [ + SessionUnlockedGuard(), + UserAccessGuard(), + ]; + + @override + Widget build(BuildContext context, GoRouterState state) { + return ProposalBuilderPage(templateId: templateId); + } +} + final class ProposalBuilderRoute extends GoRouteData with FadePageTransitionMixin, CompositeRouteGuardMixin { final String proposalId; @@ -99,9 +128,9 @@ final class ProposalBuilderRoute extends GoRouteData }); @override - List get routeGuards => [ - const SessionUnlockedGuard(), - const UserAccessGuard(), + List get routeGuards => const [ + SessionUnlockedGuard(), + UserAccessGuard(), ]; @override @@ -115,9 +144,9 @@ final class VotingRoute extends GoRouteData const VotingRoute(); @override - List get routeGuards => [ - const SessionUnlockedGuard(), - const UserAccessGuard(), + List get routeGuards => const [ + SessionUnlockedGuard(), + UserAccessGuard(), ]; @override @@ -131,9 +160,9 @@ final class FundedProjectsRoute extends GoRouteData const FundedProjectsRoute(); @override - List get routeGuards => [ - const SessionUnlockedGuard(), - const UserAccessGuard(), + List get routeGuards => const [ + SessionUnlockedGuard(), + UserAccessGuard(), ]; @override @@ -147,9 +176,9 @@ final class TreasuryRoute extends GoRouteData const TreasuryRoute(); @override - List get routeGuards => [ - const SessionUnlockedGuard(), - const AdminAccessGuard(), + List get routeGuards => const [ + SessionUnlockedGuard(), + AdminAccessGuard(), ]; @override diff --git a/catalyst_voices/apps/voices/lib/widgets/buttons/voices_icon_button.dart b/catalyst_voices/apps/voices/lib/widgets/buttons/voices_icon_button.dart index 9476318b3ff..e1ee79eb71a 100644 --- a/catalyst_voices/apps/voices/lib/widgets/buttons/voices_icon_button.dart +++ b/catalyst_voices/apps/voices/lib/widgets/buttons/voices_icon_button.dart @@ -93,10 +93,10 @@ class VoicesIconButton extends StatelessWidget { side: WidgetStateProperty.resolveWith( (states) { if (states.contains(WidgetState.disabled)) { - return BorderSide(color: colors.onSurfaceNeutral012!); + return BorderSide(color: colors.onSurfaceNeutral012); } - return BorderSide(color: colors.outlineBorderVariant!); + return BorderSide(color: colors.outlineBorderVariant); }, ), ), 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 index c1aae85b85c..8e2014cb738 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart @@ -22,7 +22,7 @@ class CampaignStageCard extends StatelessWidget { decoration: BoxDecoration( color: theme.colors.elevationsOnSurfaceNeutralLv1White, border: Border.all( - color: theme.colors.outlineBorderVariant!, + color: theme.colors.outlineBorderVariant, ), borderRadius: BorderRadius.circular(20), ), 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 aed06e0da37..12d82b8a204 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 @@ -195,7 +195,7 @@ class _FundsAndComments extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( - color: Theme.of(context).colors.success?.withOpacity(0.08), + color: Theme.of(context).colors.success.withOpacity(0.08), borderRadius: BorderRadius.circular(8), ), child: Row( diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart deleted file mode 100644 index 3cf6253019d..00000000000 --- a/catalyst_voices/apps/voices/lib/widgets/cards/guidance_card.dart +++ /dev/null @@ -1,70 +0,0 @@ -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_models/catalyst_voices_models.dart'; -import 'package:flutter/material.dart'; - -class GuidanceCard extends StatelessWidget { - final Guidance guidance; - - const GuidanceCard({ - super.key, - required this.guidance, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8), - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colors.onSurfacePrimary08, - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - guidance.type.icon.buildIcon(), - const SizedBox(width: 8), - Text( - _buildTypeTitle(context), - style: Theme.of(context).textTheme.titleSmall, - ), - ], - ), - const SizedBox(height: 10), - Text( - guidance.title, - style: Theme.of(context) - .textTheme - .titleSmall - ?.copyWith(color: Theme.of(context).colors.textOnPrimary), - ), - const SizedBox(height: 10), - Text( - guidance.description, - ), - ], - ), - ), - ), - ); - } - - 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/role_chooser_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/role_chooser_card.dart index 611222ddc44..d848be69b46 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/role_chooser_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/role_chooser_card.dart @@ -54,7 +54,7 @@ class RoleChooserCard extends StatelessWidget { ? null : BoxDecoration( border: Border.all( - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, width: 1, ), borderRadius: BorderRadius.circular(8), diff --git a/catalyst_voices/apps/voices/lib/widgets/chips/voices_chip.dart b/catalyst_voices/apps/voices/lib/widgets/chips/voices_chip.dart index 02c09d73b00..5a7f3177d72 100644 --- a/catalyst_voices/apps/voices/lib/widgets/chips/voices_chip.dart +++ b/catalyst_voices/apps/voices/lib/widgets/chips/voices_chip.dart @@ -70,7 +70,7 @@ class VoicesChip extends StatelessWidget { border: backgroundColor != null ? null : Border.all( - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, ), borderRadius: borderRadius, ), diff --git a/catalyst_voices/apps/voices/lib/widgets/containers/roles_summary_container.dart b/catalyst_voices/apps/voices/lib/widgets/containers/roles_summary_container.dart index 10f243ac1c9..340ed363559 100644 --- a/catalyst_voices/apps/voices/lib/widgets/containers/roles_summary_container.dart +++ b/catalyst_voices/apps/voices/lib/widgets/containers/roles_summary_container.dart @@ -26,7 +26,7 @@ class RolesSummaryContainer extends StatelessWidget { return DecoratedBox( decoration: BoxDecoration( border: Border.all( - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, width: 1.5, ), borderRadius: BorderRadius.circular(8), diff --git a/catalyst_voices/apps/voices/lib/widgets/containers/workspace_text_tile_container.dart b/catalyst_voices/apps/voices/lib/widgets/containers/workspace_text_tile_container.dart index 4af7621277e..45379141fcd 100644 --- a/catalyst_voices/apps/voices/lib/widgets/containers/workspace_text_tile_container.dart +++ b/catalyst_voices/apps/voices/lib/widgets/containers/workspace_text_tile_container.dart @@ -33,7 +33,7 @@ class WorkspaceTextTileContainer extends StatelessWidget { ), boxShadow: [ BoxShadow( - color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv0!, + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv0, offset: const Offset(0, 1), blurRadius: 4, ), diff --git a/catalyst_voices/apps/voices/lib/widgets/dropdown/voices_dropdown.dart b/catalyst_voices/apps/voices/lib/widgets/dropdown/voices_dropdown.dart index 9805a2b0700..3a0739c9233 100644 --- a/catalyst_voices/apps/voices/lib/widgets/dropdown/voices_dropdown.dart +++ b/catalyst_voices/apps/voices/lib/widgets/dropdown/voices_dropdown.dart @@ -127,7 +127,7 @@ class SingleSelectDropdown extends StatelessWidget { OutlineInputBorder _border(BuildContext context) => OutlineInputBorder( borderSide: BorderSide( - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, ), ); } 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 e1dbcacaf02..7cc4c761a2b 100644 --- a/catalyst_voices/apps/voices/lib/widgets/indicators/voices_indicator.dart +++ b/catalyst_voices/apps/voices/lib/widgets/indicators/voices_indicator.dart @@ -9,9 +9,9 @@ enum VoicesIndicatorType { Color _iconColor(BuildContext context) { return switch (this) { - VoicesIndicatorType.normal => Theme.of(context).colors.iconsForeground!, - VoicesIndicatorType.error => Theme.of(context).colors.iconsError!, - VoicesIndicatorType.success => Theme.of(context).colors.iconsSuccess!, + VoicesIndicatorType.normal => Theme.of(context).colors.iconsForeground, + VoicesIndicatorType.error => Theme.of(context).colors.iconsError, + VoicesIndicatorType.success => Theme.of(context).colors.iconsSuccess, }; } } @@ -38,7 +38,7 @@ class VoicesIndicator extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), decoration: BoxDecoration( border: Border.all( - color: theme.colors.outlineBorderVariant!, + color: theme.colors.outlineBorderVariant, width: 1, ), borderRadius: BorderRadius.circular(8), diff --git a/catalyst_voices/apps/voices/lib/widgets/indicators/voices_password_strength_indicator.dart b/catalyst_voices/apps/voices/lib/widgets/indicators/voices_password_strength_indicator.dart index 41be4d15bd4..92b9126a447 100644 --- a/catalyst_voices/apps/voices/lib/widgets/indicators/voices_password_strength_indicator.dart +++ b/catalyst_voices/apps/voices/lib/widgets/indicators/voices_password_strength_indicator.dart @@ -37,6 +37,7 @@ class _Label extends StatelessWidget { @override Widget build(BuildContext context) { return Text( + key: const Key('PasswordStrengthLabel'), switch (passwordStrength) { PasswordStrength.weak => context.l10n.weakPasswordStrength, PasswordStrength.normal => context.l10n.normalPasswordStrength, @@ -63,6 +64,7 @@ class _Indicator extends StatelessWidget { return SizedBox( height: _foregroundTrackHeight, child: LayoutBuilder( + key: const Key('PasswordStrengthIndicator'), builder: (context, constraints) { final totalWidthOfAllGaps = (PasswordStrength.values.length - 1) * _tracksGap; 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 e76cdeb296f..0f00cc9b304 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 @@ -122,7 +122,7 @@ class _VoicesDesktopDialog extends StatelessWidget { ? RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( - color: Theme.of(context).colors.outlineBorderVariant!, + color: Theme.of(context).colors.outlineBorderVariant, ), ) : Theme.of(context).dialogTheme.shape, 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 3d6ccc2073b..4d9644995c1 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 @@ -174,7 +174,7 @@ class _InfoContainer extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( - color: Theme.of(context).colors.iconsPrimary!, + color: Theme.of(context).colors.iconsPrimary, ), ), child: Row( @@ -243,7 +243,7 @@ class _UploadContainerState extends State<_UploadContainer> { borderType: BorderType.RRect, radius: const Radius.circular(12), dashPattern: const [8, 6], - color: Theme.of(context).colors.iconsPrimary!, + color: Theme.of(context).colors.iconsPrimary, child: Stack( children: [ // We allow drag&drop only on web @@ -303,7 +303,7 @@ class _UploadContainerState extends State<_UploadContainer> { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: Theme.of(context).colors.iconsPrimary!, + color: Theme.of(context).colors.iconsPrimary, width: 3, ), ), 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 index 36c79ccaf4e..4c85a5df8c0 100644 --- a/catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart +++ b/catalyst_voices/apps/voices/lib/widgets/pickers/voices_time_picker.dart @@ -119,7 +119,7 @@ class _TimeText extends StatelessWidget { child: ColoredBox( color: !isSelected ? Colors.transparent - : Theme.of(context).colors.onSurfaceNeutral08!, + : Theme.of(context).colors.onSurfaceNeutral08, child: Padding( key: key, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), diff --git a/catalyst_voices/apps/voices/lib/widgets/seed_phrase/seed_phrases_sequencer.dart b/catalyst_voices/apps/voices/lib/widgets/seed_phrase/seed_phrases_sequencer.dart index 6126b193ec5..f83f8a736a1 100644 --- a/catalyst_voices/apps/voices/lib/widgets/seed_phrase/seed_phrases_sequencer.dart +++ b/catalyst_voices/apps/voices/lib/widgets/seed_phrase/seed_phrases_sequencer.dart @@ -41,6 +41,7 @@ class SeedPhrasesSequencer extends StatelessWidget { ), const SizedBox(height: 10), SeedPhrasesPicker( + key: const Key('SeedPhrasesPicker'), words: words, selectedWords: selectedWords, onWordTap: _selectWord, diff --git a/catalyst_voices/apps/voices/lib/widgets/separators/voices_text_divider.dart b/catalyst_voices/apps/voices/lib/widgets/separators/voices_text_divider.dart index c9e90b0d1c0..6758fbd6d78 100644 --- a/catalyst_voices/apps/voices/lib/widgets/separators/voices_text_divider.dart +++ b/catalyst_voices/apps/voices/lib/widgets/separators/voices_text_divider.dart @@ -47,6 +47,7 @@ class VoicesTextDivider extends StatelessWidget { Expanded(child: Divider(indent: indent)), SizedBox(width: nameGap), DefaultTextStyle( + key: const Key('NextStepTitle'), style: (theme.textTheme.bodyLarge ?? const TextStyle()) .copyWith(color: theme.colors.textOnPrimary), child: child, diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/seed_phrase_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/seed_phrase_field.dart index 22319666c03..7c0150f79d8 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/seed_phrase_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/seed_phrase_field.dart @@ -92,7 +92,7 @@ class _SeedPhraseFieldState extends State { child: DecoratedBox( decoration: BoxDecoration( border: Border.all( - color: theme.colors.outlineBorder!, + color: theme.colors.outlineBorder, width: 1.5, ), borderRadius: BorderRadius.circular(12), 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 index 5fb72e87e57..af52a27c71d 100644 --- 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 @@ -38,7 +38,7 @@ class VoicesDateTimeTextField extends StatelessWidget { final borderSide = !dimBorder ? BorderSide( - color: theme.colors.outlineBorderVariant!, + color: theme.colors.outlineBorderVariant, width: 0.75, ) : BorderSide( 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 61171b04800..0f497bb6d93 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 @@ -216,6 +216,7 @@ class _VoicesTextFieldState extends State { minHeight: widget.maxLines == null ? 65 : 48, iconBottomSpacing: widget.maxLines == null ? 18 : 0, child: TextFormField( + key: const Key('VoicesTextField'), textAlignVertical: TextAlignVertical.top, autofocus: widget.autofocus, expands: resizable, @@ -467,9 +468,9 @@ class _VoicesTextFieldState extends State { case VoicesTextFieldStatus.none: return orDefault; case VoicesTextFieldStatus.success: - return Theme.of(context).colors.success!; + return Theme.of(context).colors.success; case VoicesTextFieldStatus.warning: - return Theme.of(context).colors.warning!; + return Theme.of(context).colors.warning; case VoicesTextFieldStatus.error: return Theme.of(context).colorScheme.error; } diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart index 0ee13f19d83..8fcc3541ba6 100644 --- a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart @@ -1,11 +1,4 @@ -import 'package:catalyst_voices/widgets/document_builder/agreement_confirmation_widget.dart'; -import 'package:catalyst_voices/widgets/document_builder/document_token_value_widget.dart'; -import 'package:catalyst_voices/widgets/document_builder/multiline_text_entry_markdown_widget.dart'; -import 'package:catalyst_voices/widgets/document_builder/simple_text_entry_widget.dart'; -import 'package:catalyst_voices/widgets/document_builder/single_dropdown_selection_widget.dart'; -import 'package:catalyst_voices/widgets/document_builder/single_grouped_tag_selector_widget.dart'; -import 'package:catalyst_voices/widgets/document_builder/single_line_https_url_widget.dart.dart'; -import 'package:catalyst_voices/widgets/document_builder/yes_no_choice_widget.dart'; + import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; @@ -209,6 +202,8 @@ class _PropertyBuilder extends StatelessWidget { switch (definition) { case SegmentDefinition(): case SectionDefinition(): + case TagGroupDefinition(): + case TagSelectionDefinition(): throw UnsupportedError( '${property.schema.definition} unsupported ' 'by $DocumentBuilderSectionTile', @@ -220,21 +215,19 @@ class _PropertyBuilder extends StatelessWidget { case SingleLineHttpsURLEntryListDefinition(): case NestedQuestionsListDefinition(): case NestedQuestionsDefinition(): - case TagGroupDefinition(): - case TagSelectionDefinition(): case DurationInMonthsDefinition(): case SPDXLicenceOrUrlDefinition(): case LanguageCodeDefinition(): - throw UnimplementedError('${definition.type} not implemented'); + return Text('${definition.runtimeType} not implemented'); case SingleLineHttpsURLEntryDefinition(): - final castProperty = definition.castProperty(property); + /* final castProperty = definition.castProperty(property); return SingleLineHttpsUrlWidget( property: castProperty, isEditMode: isEditMode, onChanged: onChanged, - ); + );*/ case SingleGroupedTagSelectorDefinition(): - final castProperty = definition.castProperty(property); + /* final castProperty = definition.castProperty(property); return SingleGroupedTagSelectorWidget( id: castProperty.schema.nodeId, selection: castProperty.value ?? const GroupedTagsSelection(), @@ -242,9 +235,9 @@ class _PropertyBuilder extends StatelessWidget { isEditMode: isEditMode, onChanged: onChanged, isRequired: castProperty.schema.isRequired, - ); + );*/ case DropDownSingleSelectDefinition(): - final castProperty = definition.castProperty(property); + /* final castProperty = definition.castProperty(property); return SingleDropdownSelectionWidget( value: castProperty.value ?? castProperty.schema.defaultValue ?? '', items: castProperty.schema.enumValues ?? [], @@ -254,9 +247,9 @@ class _PropertyBuilder extends StatelessWidget { isEditMode: isEditMode, isRequired: castProperty.schema.isRequired, onChanged: onChanged, - ); + );*/ case AgreementConfirmationDefinition(): - final castProperty = definition.castProperty(property); + /*final castProperty = definition.castProperty(property); return AgreementConfirmationWidget( value: castProperty.value, definition: definition, @@ -265,25 +258,25 @@ class _PropertyBuilder extends StatelessWidget { title: castProperty.schema.title ?? '', isEditMode: isEditMode, onChanged: onChanged, - ); + );*/ case TokenValueCardanoADADefinition(): - final castProperty = definition.castProperty(property); + /*final castProperty = definition.castProperty(property); return DocumentTokenValueWidget( - property: castProperty, + property: castProperty as DocumentProperty, currency: const Currency.ada(), isEditMode: isEditMode, onChanged: onChanged, - ); + );*/ case SingleLineTextEntryDefinition(): case MultiLineTextEntryDefinition(): - final castProperty = definition.castProperty(property); + /*final castProperty = definition.castProperty(property); return SimpleTextEntryWidget( property: castProperty as DocumentProperty, isEditMode: isEditMode, onChanged: onChanged, - ); + );*/ case YesNoChoiceDefinition(): - final castProperty = definition.castProperty(property); + /*final castProperty = definition.castProperty(property); return YesNoChoiceWidget( property: castProperty, onChanged: onChanged, @@ -298,6 +291,9 @@ class _PropertyBuilder extends StatelessWidget { isEditMode: isEditMode, isRequired: castProperty.schema.isRequired, ); + );*/ + // TODO(dtscalac): uncomment tiles when casting works. + return Text('${definition.runtimeType} casting problem'); } } } diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/selectable_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/selectable_tile.dart index e4f6d023633..e293842c7de 100644 --- a/catalyst_voices/apps/voices/lib/widgets/tiles/selectable_tile.dart +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/selectable_tile.dart @@ -23,7 +23,7 @@ class SelectableTile extends StatelessWidget { borderRadius: _borderRadius(isSelected), boxShadow: [ BoxShadow( - color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv0!, + color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv0, offset: const Offset(0, 1), blurRadius: 4, ), diff --git a/catalyst_voices/apps/voices/lib/widgets/toggles/voices_checkbox.dart b/catalyst_voices/apps/voices/lib/widgets/toggles/voices_checkbox.dart index 0138d3465ba..36d165c6801 100644 --- a/catalyst_voices/apps/voices/lib/widgets/toggles/voices_checkbox.dart +++ b/catalyst_voices/apps/voices/lib/widgets/toggles/voices_checkbox.dart @@ -59,7 +59,7 @@ class VoicesCheckbox extends StatelessWidget { side: isDisabled ? BorderSide( width: 2, - color: Theme.of(context).colors.onSurfaceNeutral012!, + color: Theme.of(context).colors.onSurfaceNeutral012, ) : null, ), diff --git a/catalyst_voices/melos.yaml b/catalyst_voices/melos.yaml index 3fa787bd622..8fc059c53f0 100644 --- a/catalyst_voices/melos.yaml +++ b/catalyst_voices/melos.yaml @@ -132,6 +132,7 @@ command: scrollable_positioned_list: ^0.3.8 shared_preferences: ^2.3.3 shared_preferences_platform_interface: ^2.4.1 + synchronized: ^3.3.0+3 # 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 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_assets/assets/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json similarity index 98% rename from catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json rename to catalyst_voices/packages/internal/catalyst_voices_assets/assets/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index effa6a83c1d..a66ce3133ce 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/catalyst_voices/packages/internal/catalyst_voices_assets/assets/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -1,1006 +1,1006 @@ -{ - "$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." - }, - "languageCode": { - "$comment": "UI - ISO 639-1 language code selection", - "type": "string", - "title": "Language Code", - "description": "Two-letter ISO 639-1 language code", - "enum": [ - "aa", "ab", "af", "ak", "am", "ar", "as", "ay", "az", "ba", "be", "bg", "bh", "bi", "bn", - "bo", "br", "bs", "ca", "ce", "ch", "co", "cs", "cu", "cv", "cy", "da", "de", "dv", "dz", - "ee", "el", "en", "eo", "es", "et", "eu", "fa", "ff", "fi", "fj", "fo", "fr", "fy", "ga", - "gd", "gl", "gn", "gu", "gv", "ha", "he", "hi", "ho", "hr", "ht", "hu", "hy", "hz", "ia", - "id", "ie", "ig", "ii", "ik", "io", "is", "it", "iu", "ja", "jv", "ka", "kg", "ki", "kj", - "kk", "kl", "km", "kn", "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la", "lb", "lg", "li", - "ln", "lo", "lt", "lu", "lv", "mg", "mh", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", - "na", "nb", "nd", "ne", "ng", "nl", "nn", "no", "nr", "nv", "ny", "oc", "oj", "om", "or", - "os", "pa", "pi", "pl", "ps", "pt", "qu", "rm", "rn", "ro", "ru", "rw", "sa", "sc", "sd", - "se", "sg", "si", "sk", "sl", "sm", "sn", "so", "sq", "sr", "ss", "st", "su", "sv", "sw", - "ta", "te", "tg", "th", "ti", "tk", "tl", "tn", "to", "tr", "ts", "tt", "tw", "ty", "ug", - "uk", "ur", "uz", "ve", "vi", "vo", "wa", "wo", "xh", "yi", "yo", "za", "zh", "zu" - ], - "default": "en", - "x-note": "Select the ISO 639-1 two-letter code for the language. For example: 'en' for English, 'es' for Spanish, 'fr' for French, etc." - } - }, - "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**\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." - } - }, - "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:\n\n1. Individual\n2. Entity (Incorporated)\n3. 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:\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 - } - }, - "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/languageCode", - "title": "Original Language", - "description": "If auto-translated, specify the original language of your proposal" - }, - "originalDocumentLink": { - "$ref": "#/definitions/singleLineHttpsURLEntry", - "title": "Original Document Link", - "description": "Provide a link to the original proposal document in its original language" - } - } - }, - "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, - "minLength": 10 - }, - "innovationAspects": { - "$ref": "#/definitions/singleLineTextEntryList", - "title": "Innovation Aspects", - "description": "Key innovative aspects of your solution", - "minItems": 1, - "maxItems": 5, - "items": { - "maxLength": 200, - "minLength": 10 - } - } - }, - "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:\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, - "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:\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", - "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?\n\nPlease 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:\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"] - } - }, - "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.\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 - } - } - }, - "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.\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 - } - } - }, - "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.\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 - } - } - } - }, - "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](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](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](https://docs.projectcatalyst.io/current-fund/fund-basics/project-catalyst-terms-and-conditions/catalyst-fc-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" - ] +{ + "$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." + }, + "languageCode": { + "$comment": "UI - ISO 639-1 language code selection", + "type": "string", + "title": "Language Code", + "description": "Two-letter ISO 639-1 language code", + "enum": [ + "aa", "ab", "af", "ak", "am", "ar", "as", "ay", "az", "ba", "be", "bg", "bh", "bi", "bn", + "bo", "br", "bs", "ca", "ce", "ch", "co", "cs", "cu", "cv", "cy", "da", "de", "dv", "dz", + "ee", "el", "en", "eo", "es", "et", "eu", "fa", "ff", "fi", "fj", "fo", "fr", "fy", "ga", + "gd", "gl", "gn", "gu", "gv", "ha", "he", "hi", "ho", "hr", "ht", "hu", "hy", "hz", "ia", + "id", "ie", "ig", "ii", "ik", "io", "is", "it", "iu", "ja", "jv", "ka", "kg", "ki", "kj", + "kk", "kl", "km", "kn", "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la", "lb", "lg", "li", + "ln", "lo", "lt", "lu", "lv", "mg", "mh", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", + "na", "nb", "nd", "ne", "ng", "nl", "nn", "no", "nr", "nv", "ny", "oc", "oj", "om", "or", + "os", "pa", "pi", "pl", "ps", "pt", "qu", "rm", "rn", "ro", "ru", "rw", "sa", "sc", "sd", + "se", "sg", "si", "sk", "sl", "sm", "sn", "so", "sq", "sr", "ss", "st", "su", "sv", "sw", + "ta", "te", "tg", "th", "ti", "tk", "tl", "tn", "to", "tr", "ts", "tt", "tw", "ty", "ug", + "uk", "ur", "uz", "ve", "vi", "vo", "wa", "wo", "xh", "yi", "yo", "za", "zh", "zu" + ], + "default": "en", + "x-note": "Select the ISO 639-1 two-letter code for the language. For example: 'en' for English, 'es' for Spanish, 'fr' for French, etc." + } + }, + "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**\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." + } + }, + "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:\n\n1. Individual\n2. Entity (Incorporated)\n3. 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:\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 + } + }, + "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/languageCode", + "title": "Original Language", + "description": "If auto-translated, specify the original language of your proposal" + }, + "originalDocumentLink": { + "$ref": "#/definitions/singleLineHttpsURLEntry", + "title": "Original Document Link", + "description": "Provide a link to the original proposal document in its original language" + } + } + }, + "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, + "minLength": 10 + }, + "innovationAspects": { + "$ref": "#/definitions/singleLineTextEntryList", + "title": "Innovation Aspects", + "description": "Key innovative aspects of your solution", + "minItems": 1, + "maxItems": 5, + "items": { + "maxLength": 200, + "minLength": 10 + } + } + }, + "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:\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, + "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:\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", + "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?\n\nPlease 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:\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"] + } + }, + "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.\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 + } + } + }, + "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.\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 + } + } + }, + "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.\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 + } + } + } + }, + "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](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](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](https://docs.projectcatalyst.io/current-fund/fund-basics/project-catalyst-terms-and-conditions/catalyst-fc-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_assets/assets/document_templates/proposal/F14-Generic/example.proposal.json similarity index 97% rename from catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json rename to catalyst_voices/packages/internal/catalyst_voices_assets/assets/document_templates/proposal/F14-Generic/example.proposal.json index 94a0859908f..93918bc53ef 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json +++ b/catalyst_voices/packages/internal/catalyst_voices_assets/assets/document_templates/proposal/F14-Generic/example.proposal.json @@ -1,135 +1,135 @@ -{ - "$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": "de", - "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 - } - } +{ + "$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": "de", + "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_assets/lib/src/catalyst_voices_assets.dart b/catalyst_voices/packages/internal/catalyst_voices_assets/lib/src/catalyst_voices_assets.dart index ffd3cf26a3c..3e84da93ad2 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_assets/lib/src/catalyst_voices_assets.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_assets/lib/src/catalyst_voices_assets.dart @@ -5,3 +5,5 @@ export 'package:catalyst_voices_assets/src/assets_ext.dart'; export 'package:catalyst_voices_assets/src/catalyst_image.dart'; export 'package:catalyst_voices_assets/src/catalyst_svg_icon.dart'; export 'package:catalyst_voices_assets/src/catalyst_svg_picture.dart'; + +export 'voices_documents_templates.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_assets/lib/src/voices_documents_templates.dart b/catalyst_voices/packages/internal/catalyst_voices_assets/lib/src/voices_documents_templates.dart new file mode 100644 index 00000000000..bac2562cb8a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_assets/lib/src/voices_documents_templates.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +// TODO(damian-molinski): This class should be removed once Repository does not +// build document's base on it. +class VoicesDocumentsTemplates { + VoicesDocumentsTemplates._(); + + static final Future> proposalF14Schema = _getJsonAsset( + 'assets/document_templates/proposal/F14-Generic/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json', + ); + static final Future> proposalF14Document = _getJsonAsset( + 'assets/document_templates/proposal/F14-Generic/example.proposal.json', + ); + + static Future> _getJsonAsset(String name) async { + final encodedData = await rootBundle.loadString( + 'packages/catalyst_voices_assets/$name', + ); + final decodedData = json.decode(encodedData) as Map; + + return decodedData; + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_assets/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_assets/pubspec.yaml index 923ef26e73a..39bf40c0c20 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_assets/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_assets/pubspec.yaml @@ -16,12 +16,15 @@ dev_dependencies: build_runner: ^2.4.12 catalyst_analysis: ^2.0.1 flutter_gen_runner: ^5.3.2 + flutter_test: + sdk: flutter flutter: generate: true assets: - assets/images/ - assets/icons/ + - assets/document_templates/proposal/F14-Generic/ fonts: - family: SF-Pro fonts: diff --git a/catalyst_voices/packages/internal/catalyst_voices_assets/test/src/voices_documents_templates_test.dart b/catalyst_voices/packages/internal/catalyst_voices_assets/test/src/voices_documents_templates_test.dart new file mode 100644 index 00000000000..f8598d343ff --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_assets/test/src/voices_documents_templates_test.dart @@ -0,0 +1,22 @@ +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group(VoicesDocumentsTemplates, () { + group('proposalF14', () { + test('schema', () async { + final json = await VoicesDocumentsTemplates.proposalF14Schema; + + expect(json, isNotEmpty); + }); + + test('document', () async { + final json = await VoicesDocumentsTemplates.proposalF14Document; + + expect(json, isNotEmpty); + }); + }); + }); +} 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 index 33ce035756d..10f2ad43e0e 100644 --- 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 @@ -1,15 +1,15 @@ 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_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 CampaignDetailsBloc extends Bloc { - final CampaignRepository _campaignRepository; + final CampaignService _campaignService; CampaignDetailsBloc( - this._campaignRepository, + this._campaignService, ) : super(const CampaignDetailsState()) { on(_onLoadCampaignEvent); } @@ -27,7 +27,7 @@ final class CampaignDetailsBloc ), ); - final campaign = await _campaignRepository.getCampaign(id: id); + final campaign = await _campaignService.getCampaign(id: id); final listItems = _mapCampaignToListItems(campaign); emit( @@ -39,8 +39,21 @@ final class CampaignDetailsBloc } List _mapCampaignToListItems(Campaign campaign) { - final sections = - campaign.sections.map(CampaignCategorySection.fromCategory).toList(); + // TODO(damian-molinski): mapping of campaign is not ready. + const sections = [ + CampaignCategorySection( + id: '1', + category: CampaignCategory(id: '1', name: 'Concept'), + title: 'Introduction', + body: _tmpBody, + ), + CampaignCategorySection( + id: '2', + category: CampaignCategory(id: '2', name: 'Product'), + title: 'Motivation', + body: 'Different body here\n$_tmpBody', + ), + ]; return [ CampaignDetailsListItem( @@ -54,3 +67,28 @@ final class CampaignDetailsBloc ]; } } + +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.'''; diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart index fc33760f34c..e59ccac2379 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/proposal_builder/proposal_builder_bloc.dart @@ -1,27 +1,180 @@ 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_shared/catalyst_voices_shared.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +final _logger = Logger('ProposalBuilderBloc'); + final class ProposalBuilderBloc extends Bloc { - // ignore: unused_field final CampaignService _campaignService; + final ProposalService _proposalService; + + DocumentBuilder? _documentBuilder; + + // ignore: unused_field + NodeId? _activeNodeId; ProposalBuilderBloc( this._campaignService, + this._proposalService, ) : super(const ProposalBuilderState()) { + on(_loadDefaultProposalTemplate); + on(_loadProposalTemplate); on(_loadProposal); - on(_handleActiveStepEvent); + on( + _handleActiveNodeChangedEvent, + transformer: (events, mapper) => events.distinct(), + ); + on(_handleSectionChangedEvent); + } + + void _handleActiveNodeChangedEvent( + ActiveNodeChangedEvent event, + Emitter emit, + ) { + _logger.info('Active node changed to [${event.id}]'); + + // TODO(damian-molinski): Show guidance for this node and its parents. + _activeNodeId = event.id; + } + + void _handleSectionChangedEvent( + SectionChangedEvent event, + Emitter emit, + ) { + final documentBuilder = _documentBuilder; + assert(documentBuilder != null, 'DocumentBuilder not initialized'); + + documentBuilder!.addChanges(event.changes); + final document = documentBuilder.build(); + final segments = _mapDocumentToSegments(document); + + // TODO(damian-molinski): Get guidance + convert to MarkdownData + emit(state.copyWith(segments: segments)); + } + + Future _loadDefaultProposalTemplate( + LoadDefaultProposalTemplateEvent event, + Emitter emit, + ) async { + await _loadDocument( + documentBuilderGetter: () async { + _logger.info('Loading default proposal template'); + + final campaign = await _campaignService.getActiveCampaign(); + + final proposalTemplateRef = campaign?.proposalTemplateRef; + + if (proposalTemplateRef == null) { + throw const ActiveCampaignNotFoundException(); + } + + final proposalTemplate = await _proposalService.getProposalTemplate( + ref: proposalTemplateRef, + ); + + return DocumentBuilder.fromSchema( + // TODO(damian-molinski): not sure what should go here. + schemaUrl: proposalTemplate.schema.propertiesSchema, + schema: proposalTemplate.schema, + ); + }, + emit: emit, + ); + } + + Future _loadProposalTemplate( + LoadProposalTemplateEvent event, + Emitter emit, + ) async { + await _loadDocument( + documentBuilderGetter: () async { + _logger.info('Loading proposal template[${event.id}]'); + + final ref = SignedDocumentRef(id: event.id); + final proposalTemplate = await _proposalService.getProposalTemplate( + ref: ref, + ); + + return DocumentBuilder.fromSchema( + // TODO(damian-molinski): not sure what should go here. + schemaUrl: proposalTemplate.schema.propertiesSchema, + schema: proposalTemplate.schema, + ); + }, + emit: emit, + ); } Future _loadProposal( LoadProposalEvent event, Emitter emit, - ) async {} + ) async { + await _loadDocument( + documentBuilderGetter: () async { + _logger.info('Loading proposal[${event.id}]'); - void _handleActiveStepEvent( - ActiveStepChangedEvent event, - Emitter emit, - ) {} + final proposal = await _proposalService.getProposal(id: event.id); + final document = proposal.document.document; + + return DocumentBuilder.fromDocument(document); + }, + emit: emit, + ); + } + + Future _loadDocument({ + required AsyncValueGetter documentBuilderGetter, + required Emitter emit, + }) async { + try { + _logger.finer('Changing source to new document'); + + emit(const ProposalBuilderState(isLoading: true)); + + _documentBuilder = null; + + final documentBuilder = await documentBuilderGetter(); + + _documentBuilder = documentBuilder; + + final document = documentBuilder.build(); + final segments = _mapDocumentToSegments(document); + + // TODO(damian-molinski): Get guidance + convert to MarkdownData + emit(ProposalBuilderState(segments: segments)); + } on LocalizedException catch (error) { + emit(ProposalBuilderState(error: error)); + } catch (error) { + emit(const ProposalBuilderState(error: LocalizedUnknownException())); + } finally { + emit(state.copyWith(isLoading: false)); + } + } + + List _mapDocumentToSegments(Document document) { + return document.segments.map((segment) { + final sections = segment.sections.map( + (section) { + return ProposalBuilderSection( + id: section.schema.nodeId, + documentSection: section, + isEnabled: true, + isEditable: true, + ); + }, + ).toList(); + + return ProposalBuilderSegment( + id: segment.schema.nodeId, + sections: sections, + documentSegment: segment, + ); + }).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 index bc3c05c1e15..cc5c457937e 100644 --- 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 @@ -5,6 +5,24 @@ sealed class ProposalBuilderEvent extends Equatable { const ProposalBuilderEvent(); } +final class LoadDefaultProposalTemplateEvent extends ProposalBuilderEvent { + const LoadDefaultProposalTemplateEvent(); + + @override + List get props => []; +} + +final class LoadProposalTemplateEvent extends ProposalBuilderEvent { + final String id; + + const LoadProposalTemplateEvent({ + required this.id, + }); + + @override + List get props => [id]; +} + final class LoadProposalEvent extends ProposalBuilderEvent { final String id; @@ -16,11 +34,22 @@ final class LoadProposalEvent extends ProposalBuilderEvent { List get props => [id]; } -final class ActiveStepChangedEvent extends ProposalBuilderEvent { +final class ActiveNodeChangedEvent extends ProposalBuilderEvent { final NodeId? id; - const ActiveStepChangedEvent(this.id); + const ActiveNodeChangedEvent(this.id); @override List get props => [id]; } + +final class SectionChangedEvent extends ProposalBuilderEvent { + final List changes; + + const SectionChangedEvent({ + required this.changes, + }); + + @override + List get props => [changes]; +} 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 index 50a1439e724..1af3f6560f2 100644 --- 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 @@ -3,19 +3,31 @@ import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:equatable/equatable.dart'; final class ProposalBuilderState extends Equatable { + final bool isLoading; + final LocalizedException? error; final List segments; final ProposalGuidance guidance; const ProposalBuilderState({ + this.isLoading = false, + this.error, this.segments = const [], this.guidance = const ProposalGuidance(), }); + bool get showSegments => !isLoading && segments.isNotEmpty && error == null; + + bool get showError => !isLoading && error != null; + ProposalBuilderState copyWith({ + bool? isLoading, + Optional? error, List? segments, ProposalGuidance? guidance, }) { return ProposalBuilderState( + isLoading: isLoading ?? this.isLoading, + error: error.dataOr(this.error), segments: segments ?? this.segments, guidance: guidance ?? this.guidance, ); @@ -23,6 +35,8 @@ final class ProposalBuilderState extends Equatable { @override List get props => [ + isLoading, + error, segments, guidance, ]; @@ -30,18 +44,18 @@ final class ProposalBuilderState extends Equatable { final class ProposalGuidance extends Equatable { final bool isNoneSelected; - final List guidances; + final List guidanceList; const ProposalGuidance({ this.isNoneSelected = false, - this.guidances = const [], + this.guidanceList = const [], }); - bool get showEmptyState => !isNoneSelected && guidances.isEmpty; + bool get showEmptyState => !isNoneSelected && guidanceList.isEmpty; @override List get props => [ isNoneSelected, - guidances, + guidanceList, ]; } 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 index 1b0382bca93..af47ad85b6c 100644 --- 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 @@ -26,10 +26,6 @@ final class WorkspaceBloc extends Bloc { ); } - Future createNewDraftProposal() async { - return 'new-draft-id'; - } - Future _loadProposals( LoadProposalsEvent event, Emitter emit, diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_blocs/pubspec.yaml index 49d5bfbcada..c8aedb7112b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: formz: ^0.7.0 meta: ^1.10.0 result_type: ^0.2.0 + uuid: ^4.5.1 dev_dependencies: bloc_test: ^9.1.4 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 index 78b6561917a..84e5003e9aa 100644 --- 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 @@ -8,26 +8,29 @@ 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 Campaign campaign; + late CampaignStage campaignStage; late CampaignService campaignService; late AdminToolsCubit adminToolsCubit; + setUpAll(() { + campaign = Campaign( + id: 'campaign-id', + name: 'name', + description: 'description', + startDate: DateTime.now(), + endDate: DateTime.now().plusDays(2), + proposalsCount: 0, + publish: CampaignPublish.draft, + ); + + campaignStage = CampaignStage.fromCampaign( + campaign, + DateTimeExt.now(), + ); + }); + setUp(() { campaignService = _FakeCampaignService(campaign); adminToolsCubit = AdminToolsCubit(); 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 index bb7d31a76f0..b22233d96ff 100644 --- 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 @@ -6,9 +6,31 @@ 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'; +import 'package:uuid/uuid.dart'; void main() { group(ProposalsCubit, () { + const proposalTemplate = DocumentSchema( + jsonSchema: '', + title: '', + description: '', + segments: [], + order: [], + propertiesSchema: '', + ); + + final proposalDocument = ProposalDocument( + metadata: ProposalMetadata( + id: const Uuid().v7(), + version: const Uuid().v7(), + ), + document: const Document( + schemaUrl: '', + schema: proposalTemplate, + segments: [], + ), + ); + final campaign = Campaign( id: 'F14', name: 'campaign', @@ -16,9 +38,7 @@ void main() { startDate: DateTime.now(), endDate: DateTime.now().plusDays(1), proposalsCount: 0, - sections: const [], publish: CampaignPublish.published, - proposalTemplate: const ProposalTemplate(sections: []), ); final proposal = Proposal( @@ -32,19 +52,7 @@ void main() { 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, - ), - ], - ); - }), + document: proposalDocument, ); final pendingProposal = PendingProposal.fromProposal( diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/theme_extensions/voices_color_scheme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/theme_extensions/voices_color_scheme.dart index ffb7218eba6..0724bf7f5e3 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/theme_extensions/voices_color_scheme.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/theme_extensions/voices_color_scheme.dart @@ -7,64 +7,66 @@ import 'package:flutter/material.dart'; /// attributes that can be used in the Voices application to ensure consistency. @immutable class VoicesColorScheme extends ThemeExtension { - final Color? textPrimary; - final Color? textOnPrimary; - final Color? textOnPrimaryLevel0; - final Color? textOnPrimaryLevel1; - final Color? textOnPrimaryWhite; - final Color? textOnPrimaryContainer; - final Color? textDisabled; - final Color? success; - final Color? onSuccess; - final Color? successContainer; - final Color? onSuccessContainer; - final Color? warning; - final Color? onWarning; - final Color? warningContainer; - final Color? onWarningContainer; - final Color? onSurfaceNeutral08; - final Color? onSurfaceNeutral012; - final Color? onSurfaceNeutral016; - final Color? onSurfaceNeutralOpaqueLv0; - final Color? onSurfaceNeutralOpaqueLv1; - final Color? onSurfaceNeutralOpaqueLv2; - final Color? onSurfacePrimaryContainer; - final Color? onSurfacePrimary08; - final Color? onSurfacePrimary012; - final Color? onSurfacePrimary016; - final Color? onSurfaceSecondary08; - final Color? onSurfaceSecondary012; - final Color? onSurfaceSecondary016; - final Color? onSurfaceError08; - final Color? onSurfaceError012; - final Color? onSurfaceError016; - final Color? iconsForeground; - final Color? iconsBackground; - final Color? iconsBackgroundVariant; - final Color? iconsOnImage; - final Color? iconsDisabled; - final Color? iconsPrimary; - final Color? iconsSecondary; - final Color? iconsSuccess; - final Color? iconsWarning; - final Color? iconsError; - final Color? avatarsPrimary; - final Color? avatarsSecondary; - final Color? avatarsSuccess; - final Color? avatarsWarning; - final Color? avatarsError; - final Color? elevationsOnSurfaceNeutralLv0; - final Color? elevationsOnSurfaceNeutralLv1Grey; - final Color? elevationsOnSurfaceNeutralLv1White; - final Color? elevationsOnSurfaceNeutralLv2; - final Color? outlineBorder; - final Color? outlineBorderVariant; - final Color? primary98; - final Color? primaryContainer; - final Color? onPrimaryContainer; - final Color? onErrorVariant; - final Color? errorContainer; - final Color? onErrorContainer; + final Color textPrimary; + final Color textOnPrimary; + final Color textOnPrimaryLevel0; + final Color textOnPrimaryLevel1; + final Color textOnPrimaryWhite; + final Color textOnPrimaryContainer; + final Color textDisabled; + final Color success; + final Color onSuccess; + final Color successContainer; + final Color onSuccessContainer; + final Color warning; + final Color onWarning; + final Color warningContainer; + final Color onWarningContainer; + final Color onSurfaceNeutral08; + final Color onSurfaceNeutral012; + final Color onSurfaceNeutral016; + final Color onSurfaceNeutralOpaqueLv0; + final Color onSurfaceNeutralOpaqueLv1; + final Color onSurfaceNeutralOpaqueLv2; + final Color onSurfacePrimaryContainer; + final Color onSurfacePrimary08; + final Color onSurfacePrimary012; + final Color onSurfacePrimary016; + final Color onSurfaceSecondary08; + final Color onSurfaceSecondary012; + final Color onSurfaceSecondary016; + final Color onSurfaceError08; + final Color onSurfaceError012; + final Color onSurfaceError016; + final Color iconsForeground; + final Color iconsBackground; + final Color iconsBackgroundVariant; + final Color iconsOnImage; + final Color iconsDisabled; + final Color iconsPrimary; + final Color iconsSecondary; + final Color iconsSuccess; + final Color iconsWarning; + final Color iconsError; + final Color avatarsPrimary; + final Color avatarsSecondary; + final Color avatarsSuccess; + final Color avatarsWarning; + final Color avatarsError; + final Color elevationsOnSurfaceNeutralLv0; + final Color elevationsOnSurfaceNeutralLv1Grey; + final Color elevationsOnSurfaceNeutralLv1White; + final Color elevationsOnSurfaceNeutralLv2; + final Color outlineBorder; + final Color outlineBorderVariant; + final Color primary98; + final Color primaryContainer; + final Color onPrimaryContainer; + final Color onErrorVariant; + final Color errorContainer; + final Color onErrorContainer; + final Color overlay; + final Color dropShadow; const VoicesColorScheme({ required this.textPrimary, @@ -125,68 +127,72 @@ class VoicesColorScheme extends ThemeExtension { required this.onErrorVariant, required this.errorContainer, required this.onErrorContainer, + required this.overlay, + required this.dropShadow, }); @visibleForTesting const VoicesColorScheme.optional({ - this.textPrimary, - this.textOnPrimary, - this.textOnPrimaryLevel0, - this.textOnPrimaryLevel1, - this.textOnPrimaryWhite, - this.textOnPrimaryContainer, - this.textDisabled, - this.success, - this.onSuccess, - this.successContainer, - this.onSuccessContainer, - this.warning, - this.onWarning, - this.warningContainer, - this.onWarningContainer, - this.onSurfaceNeutral08, - this.onSurfaceNeutral012, - this.onSurfaceNeutral016, - this.onSurfacePrimaryContainer, - this.onSurfacePrimary08, - this.onSurfacePrimary012, - this.onSurfacePrimary016, - this.onSurfaceNeutralOpaqueLv0, - this.onSurfaceNeutralOpaqueLv1, - this.onSurfaceNeutralOpaqueLv2, - this.onSurfaceSecondary08, - this.onSurfaceSecondary012, - this.onSurfaceSecondary016, - this.onSurfaceError08, - this.onSurfaceError012, - this.onSurfaceError016, - this.iconsForeground, - this.iconsBackground, - this.iconsBackgroundVariant, - this.iconsOnImage, - this.iconsDisabled, - this.iconsPrimary, - this.iconsSecondary, - this.iconsSuccess, - this.iconsWarning, - this.iconsError, - this.avatarsPrimary, - this.avatarsSecondary, - this.avatarsSuccess, - this.avatarsWarning, - this.avatarsError, - this.elevationsOnSurfaceNeutralLv0, - this.elevationsOnSurfaceNeutralLv1Grey, - this.elevationsOnSurfaceNeutralLv1White, - this.elevationsOnSurfaceNeutralLv2, - this.outlineBorder, - this.outlineBorderVariant, - this.primary98, - this.primaryContainer, - this.onPrimaryContainer, - this.onErrorVariant, - this.errorContainer, - this.onErrorContainer, + this.textPrimary = Colors.black, + this.textOnPrimary = Colors.black, + this.textOnPrimaryLevel0 = Colors.black, + this.textOnPrimaryLevel1 = Colors.black, + this.textOnPrimaryWhite = Colors.black, + this.textOnPrimaryContainer = Colors.black, + this.textDisabled = Colors.black, + this.success = Colors.black, + this.onSuccess = Colors.black, + this.successContainer = Colors.black, + this.onSuccessContainer = Colors.black, + this.warning = Colors.black, + this.onWarning = Colors.black, + this.warningContainer = Colors.black, + this.onWarningContainer = Colors.black, + this.onSurfaceNeutral08 = Colors.black, + this.onSurfaceNeutral012 = Colors.black, + this.onSurfaceNeutral016 = Colors.black, + this.onSurfacePrimaryContainer = Colors.black, + this.onSurfacePrimary08 = Colors.black, + this.onSurfacePrimary012 = Colors.black, + this.onSurfacePrimary016 = Colors.black, + this.onSurfaceNeutralOpaqueLv0 = Colors.black, + this.onSurfaceNeutralOpaqueLv1 = Colors.black, + this.onSurfaceNeutralOpaqueLv2 = Colors.black, + this.onSurfaceSecondary08 = Colors.black, + this.onSurfaceSecondary012 = Colors.black, + this.onSurfaceSecondary016 = Colors.black, + this.onSurfaceError08 = Colors.black, + this.onSurfaceError012 = Colors.black, + this.onSurfaceError016 = Colors.black, + this.iconsForeground = Colors.black, + this.iconsBackground = Colors.black, + this.iconsBackgroundVariant = Colors.black, + this.iconsOnImage = Colors.black, + this.iconsDisabled = Colors.black, + this.iconsPrimary = Colors.black, + this.iconsSecondary = Colors.black, + this.iconsSuccess = Colors.black, + this.iconsWarning = Colors.black, + this.iconsError = Colors.black, + this.avatarsPrimary = Colors.black, + this.avatarsSecondary = Colors.black, + this.avatarsSuccess = Colors.black, + this.avatarsWarning = Colors.black, + this.avatarsError = Colors.black, + this.elevationsOnSurfaceNeutralLv0 = Colors.black, + this.elevationsOnSurfaceNeutralLv1Grey = Colors.black, + this.elevationsOnSurfaceNeutralLv1White = Colors.black, + this.elevationsOnSurfaceNeutralLv2 = Colors.black, + this.outlineBorder = Colors.black, + this.outlineBorderVariant = Colors.black, + this.primary98 = Colors.black, + this.primaryContainer = Colors.black, + this.onPrimaryContainer = Colors.black, + this.onErrorVariant = Colors.black, + this.errorContainer = Colors.black, + this.onErrorContainer = Colors.black, + this.overlay = Colors.black, + this.dropShadow = Colors.black, }); @override @@ -249,6 +255,8 @@ class VoicesColorScheme extends ThemeExtension { Color? onErrorVariant, Color? errorContainer, Color? onErrorContainer, + Color? overlay, + Color? dropShadow, }) { return VoicesColorScheme( textPrimary: textPrimary ?? this.textPrimary, @@ -321,6 +329,8 @@ class VoicesColorScheme extends ThemeExtension { onErrorVariant: onErrorVariant ?? this.onErrorVariant, errorContainer: errorContainer ?? this.errorContainer, onErrorContainer: onErrorContainer ?? this.onErrorContainer, + overlay: overlay ?? this.overlay, + dropShadow: dropShadow ?? this.dropShadow, ); } @@ -333,128 +343,142 @@ class VoicesColorScheme extends ThemeExtension { return this; } return VoicesColorScheme( - textPrimary: Color.lerp(textPrimary, other.textPrimary, t), - textOnPrimary: Color.lerp(textOnPrimary, other.textOnPrimary, t), + textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!, + textOnPrimary: Color.lerp(textOnPrimary, other.textOnPrimary, t)!, textOnPrimaryLevel0: Color.lerp( textOnPrimaryLevel0, other.textOnPrimaryLevel0, t, - ), + )!, textOnPrimaryLevel1: Color.lerp( textOnPrimaryLevel1, other.textOnPrimaryLevel1, t, - ), + )!, textOnPrimaryWhite: Color.lerp( textOnPrimaryWhite, other.textOnPrimaryWhite, t, - ), + )!, textOnPrimaryContainer: Color.lerp( textOnPrimaryContainer, other.textOnPrimaryContainer, t, - ), - textDisabled: Color.lerp(textDisabled, other.textDisabled, t), - success: Color.lerp(success, other.success, t), - onSuccess: Color.lerp(onSuccess, other.onSuccess, t), - successContainer: Color.lerp(successContainer, other.successContainer, t), - onSuccessContainer: - Color.lerp(onSuccessContainer, other.onSuccessContainer, t), - warning: Color.lerp(warning, other.warning, t), - onWarning: Color.lerp(onWarning, other.onWarning, t), - warningContainer: Color.lerp(warningContainer, other.warningContainer, t), + )!, + textDisabled: Color.lerp(textDisabled, other.textDisabled, t)!, + success: Color.lerp(success, other.success, t)!, + onSuccess: Color.lerp(onSuccess, other.onSuccess, t)!, + successContainer: Color.lerp( + successContainer, + other.successContainer, + t, + )!, + onSuccessContainer: Color.lerp( + onSuccessContainer, + other.onSuccessContainer, + t, + )!, + warning: Color.lerp(warning, other.warning, t)!, + onWarning: Color.lerp(onWarning, other.onWarning, t)!, + warningContainer: + Color.lerp(warningContainer, other.warningContainer, t)!, onWarningContainer: - Color.lerp(onWarningContainer, other.onWarningContainer, t), + Color.lerp(onWarningContainer, other.onWarningContainer, t)!, onSurfaceNeutral08: - Color.lerp(onSurfaceNeutral08, other.onSurfaceNeutral08, t), + Color.lerp(onSurfaceNeutral08, other.onSurfaceNeutral08, t)!, onSurfaceNeutral012: - Color.lerp(onSurfaceNeutral012, other.onSurfaceNeutral012, t), + Color.lerp(onSurfaceNeutral012, other.onSurfaceNeutral012, t)!, onSurfaceNeutral016: - Color.lerp(onSurfaceNeutral016, other.onSurfaceNeutral016, t), + Color.lerp(onSurfaceNeutral016, other.onSurfaceNeutral016, t)!, onSurfaceNeutralOpaqueLv0: Color.lerp( onSurfaceNeutralOpaqueLv0, other.onSurfaceNeutralOpaqueLv0, t, - ), + )!, onSurfaceNeutralOpaqueLv1: Color.lerp( onSurfaceNeutralOpaqueLv1, other.onSurfaceNeutralOpaqueLv1, t, - ), + )!, onSurfaceNeutralOpaqueLv2: Color.lerp( onSurfaceNeutralOpaqueLv2, other.onSurfaceNeutralOpaqueLv2, t, - ), + )!, onSurfacePrimaryContainer: Color.lerp( onSurfacePrimaryContainer, other.onSurfacePrimaryContainer, t, - ), + )!, onSurfacePrimary08: - Color.lerp(onSurfacePrimary08, other.onSurfacePrimary08, t), + Color.lerp(onSurfacePrimary08, other.onSurfacePrimary08, t)!, onSurfacePrimary012: - Color.lerp(onSurfacePrimary012, other.onSurfacePrimary012, t), + Color.lerp(onSurfacePrimary012, other.onSurfacePrimary012, t)!, onSurfacePrimary016: - Color.lerp(onSurfacePrimary016, other.onSurfacePrimary016, t), + Color.lerp(onSurfacePrimary016, other.onSurfacePrimary016, t)!, onSurfaceSecondary08: - Color.lerp(onSurfaceSecondary08, other.onSurfaceSecondary08, t), + Color.lerp(onSurfaceSecondary08, other.onSurfaceSecondary08, t)!, onSurfaceSecondary012: - Color.lerp(onSurfaceSecondary012, other.onSurfaceSecondary012, t), + Color.lerp(onSurfaceSecondary012, other.onSurfaceSecondary012, t)!, onSurfaceSecondary016: - Color.lerp(onSurfaceSecondary016, other.onSurfaceSecondary016, t), - onSurfaceError08: Color.lerp(onSurfaceError08, other.onSurfaceError08, t), + Color.lerp(onSurfaceSecondary016, other.onSurfaceSecondary016, t)!, + onSurfaceError08: + Color.lerp(onSurfaceError08, other.onSurfaceError08, t)!, onSurfaceError012: - Color.lerp(onSurfaceError012, other.onSurfaceError012, t), + Color.lerp(onSurfaceError012, other.onSurfaceError012, t)!, onSurfaceError016: - Color.lerp(onSurfaceError016, other.onSurfaceError016, t), - iconsForeground: Color.lerp(iconsForeground, other.iconsForeground, t), - iconsBackground: Color.lerp(iconsBackground, other.iconsBackground, t), + Color.lerp(onSurfaceError016, other.onSurfaceError016, t)!, + iconsForeground: Color.lerp(iconsForeground, other.iconsForeground, t)!, + iconsBackground: Color.lerp(iconsBackground, other.iconsBackground, t)!, iconsBackgroundVariant: - Color.lerp(iconsBackgroundVariant, other.iconsBackgroundVariant, t), - iconsOnImage: Color.lerp(iconsOnImage, other.iconsOnImage, t), - iconsDisabled: Color.lerp(iconsDisabled, other.iconsDisabled, t), - iconsPrimary: Color.lerp(iconsPrimary, other.iconsPrimary, t), - iconsSecondary: Color.lerp(iconsSecondary, other.iconsSecondary, t), - iconsSuccess: Color.lerp(iconsSuccess, other.iconsSuccess, t), - iconsWarning: Color.lerp(iconsWarning, other.iconsWarning, t), - iconsError: Color.lerp(iconsError, other.iconsError, t), - avatarsPrimary: Color.lerp(avatarsPrimary, other.avatarsPrimary, t), - avatarsSecondary: Color.lerp(avatarsSecondary, other.avatarsSecondary, t), - avatarsSuccess: Color.lerp(avatarsSuccess, other.avatarsSuccess, t), - avatarsWarning: Color.lerp(avatarsWarning, other.avatarsWarning, t), - avatarsError: Color.lerp(avatarsError, other.avatarsError, t), + Color.lerp(iconsBackgroundVariant, other.iconsBackgroundVariant, t)!, + iconsOnImage: Color.lerp(iconsOnImage, other.iconsOnImage, t)!, + iconsDisabled: Color.lerp(iconsDisabled, other.iconsDisabled, t)!, + iconsPrimary: Color.lerp(iconsPrimary, other.iconsPrimary, t)!, + iconsSecondary: Color.lerp(iconsSecondary, other.iconsSecondary, t)!, + iconsSuccess: Color.lerp(iconsSuccess, other.iconsSuccess, t)!, + iconsWarning: Color.lerp(iconsWarning, other.iconsWarning, t)!, + iconsError: Color.lerp(iconsError, other.iconsError, t)!, + avatarsPrimary: Color.lerp(avatarsPrimary, other.avatarsPrimary, t)!, + avatarsSecondary: + Color.lerp(avatarsSecondary, other.avatarsSecondary, t)!, + avatarsSuccess: Color.lerp(avatarsSuccess, other.avatarsSuccess, t)!, + avatarsWarning: Color.lerp(avatarsWarning, other.avatarsWarning, t)!, + avatarsError: Color.lerp(avatarsError, other.avatarsError, t)!, elevationsOnSurfaceNeutralLv0: Color.lerp( elevationsOnSurfaceNeutralLv0, other.elevationsOnSurfaceNeutralLv0, t, - ), + )!, elevationsOnSurfaceNeutralLv1Grey: Color.lerp( elevationsOnSurfaceNeutralLv1Grey, other.elevationsOnSurfaceNeutralLv1Grey, t, - ), + )!, elevationsOnSurfaceNeutralLv1White: Color.lerp( elevationsOnSurfaceNeutralLv1White, other.elevationsOnSurfaceNeutralLv1White, t, - ), + )!, elevationsOnSurfaceNeutralLv2: Color.lerp( elevationsOnSurfaceNeutralLv2, other.elevationsOnSurfaceNeutralLv2, t, - ), - outlineBorder: Color.lerp(outlineBorder, other.outlineBorder, t), + )!, + outlineBorder: Color.lerp(outlineBorder, other.outlineBorder, t)!, outlineBorderVariant: - Color.lerp(outlineBorderVariant, other.outlineBorderVariant, t), - primary98: Color.lerp(primary98, other.primary98, t), - primaryContainer: Color.lerp(primaryContainer, other.primaryContainer, t), + Color.lerp(outlineBorderVariant, other.outlineBorderVariant, t)!, + primary98: Color.lerp(primary98, other.primary98, t)!, + primaryContainer: + Color.lerp(primaryContainer, other.primaryContainer, t)!, onPrimaryContainer: - Color.lerp(onPrimaryContainer, other.onPrimaryContainer, t), - onErrorVariant: Color.lerp(onErrorVariant, other.onErrorVariant, t), - errorContainer: Color.lerp(errorContainer, other.errorContainer, t), - onErrorContainer: Color.lerp(onErrorContainer, other.onErrorContainer, t), + Color.lerp(onPrimaryContainer, other.onPrimaryContainer, t)!, + onErrorVariant: Color.lerp(onErrorVariant, other.onErrorVariant, t)!, + errorContainer: Color.lerp(errorContainer, other.errorContainer, t)!, + onErrorContainer: + Color.lerp(onErrorContainer, other.onErrorContainer, t)!, + overlay: Color.lerp(overlay, other.overlay, t)!, + dropShadow: Color.lerp(dropShadow, other.dropShadow, t)!, ); } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart index c2eebc5dbf7..3a01026e09b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/catalyst.dart @@ -4,6 +4,7 @@ import 'package:catalyst_voices_brands/src/theme_extensions/brand_assets.dart'; import 'package:catalyst_voices_brands/src/theme_extensions/voices_color_scheme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/buttons_theme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/toggles_theme.dart'; +import 'package:catalyst_voices_brands/src/themes/widgets/voices_dialog_theme.dart'; import 'package:catalyst_voices_brands/src/themes/widgets/voices_input_decoration_theme.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -85,6 +86,8 @@ const VoicesColorScheme darkVoicesColorScheme = VoicesColorScheme( onErrorVariant: VoicesColors.darkOnErrorVariant, errorContainer: VoicesColors.darkErrorContainer, onErrorContainer: VoicesColors.darkOnErrorContainer, + overlay: Color(0xA610141C), + dropShadow: Color(0xA610141C), ); const ColorScheme lightColorScheme = ColorScheme.light( @@ -167,6 +170,8 @@ const VoicesColorScheme lightVoicesColorScheme = VoicesColorScheme( onErrorVariant: VoicesColors.lightOnErrorVariant, errorContainer: VoicesColors.lightErrorContainer, onErrorContainer: VoicesColors.lightOnErrorContainer, + overlay: Color(0x9904080F), + dropShadow: Color(0x9904080F), ); /// [ThemeData] for the `catalyst` brand. @@ -315,13 +320,7 @@ ThemeData _buildThemeData( drawerTheme: DrawerThemeData( backgroundColor: voicesColorScheme.onSurfaceNeutralOpaqueLv0, ), - dialogTheme: DialogTheme( - // N10-38 - barrierColor: const Color(0x212A3D61), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - clipBehavior: Clip.hardEdge, - backgroundColor: voicesColorScheme.elevationsOnSurfaceNeutralLv1White, - ), + dialogTheme: VoicesDialogTheme(colors: voicesColorScheme), listTileTheme: ListTileThemeData( shape: const StadiumBorder(), minTileHeight: 56, diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/buttons_theme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/buttons_theme.dart index 5b6f7c2a3a3..52a03e9693a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/buttons_theme.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/buttons_theme.dart @@ -26,14 +26,14 @@ extension ButtonsThemeExt on ThemeData { side: WidgetStateProperty.resolveWith( (states) { if (states.contains(WidgetState.disabled)) { - return BorderSide(color: colors.onSurfaceNeutral012!); + return BorderSide(color: colors.onSurfaceNeutral012); } if (states.contains(WidgetState.focused)) { return BorderSide(color: colorScheme.primary); } - return BorderSide(color: colors.outlineBorder!); + return BorderSide(color: colors.outlineBorder); }, ), ).merge(_buildBaseButtonStyle(textTheme)), @@ -72,10 +72,10 @@ extension ButtonsThemeExt on ThemeData { side: WidgetStateProperty.resolveWith( (states) { if (states.contains(WidgetState.disabled)) { - return BorderSide(color: colors.iconsDisabled!); + return BorderSide(color: colors.iconsDisabled); } - return BorderSide(color: colors.outlineBorder!); + return BorderSide(color: colors.outlineBorder); }, ), iconSize: const WidgetStatePropertyAll(18), diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/toggles_theme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/toggles_theme.dart index 949b7231d5d..d5326ae4e67 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/toggles_theme.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/toggles_theme.dart @@ -12,7 +12,7 @@ extension TogglesTheme on ThemeData { fillColor: WidgetStateProperty.resolveWith( (states) { if (states.contains(WidgetState.disabled)) { - return colors.iconsDisabled?.withOpacity(0.32); + return colors.iconsDisabled.withOpacity(0.32); } if (states.contains(WidgetState.selected)) { @@ -78,7 +78,7 @@ extension TogglesTheme on ThemeData { (states) { if (states.contains(WidgetState.disabled)) { return BorderSide( - color: colors.onSurfaceNeutral012!, + color: colors.onSurfaceNeutral012, width: 2, ); } @@ -98,7 +98,7 @@ extension TogglesTheme on ThemeData { } return BorderSide( - color: colors.outlineBorder!, + color: colors.outlineBorder, width: 2, ); }, @@ -114,7 +114,7 @@ extension TogglesTheme on ThemeData { return colorScheme.primary; } - return colors.outlineBorderVariant?.withOpacity(0.38); + return colors.outlineBorderVariant.withOpacity(0.38); }, ), trackOutlineColor: WidgetStateProperty.resolveWith( diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_dialog_theme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_dialog_theme.dart new file mode 100644 index 00000000000..c62033a0e55 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_dialog_theme.dart @@ -0,0 +1,16 @@ +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:flutter/material.dart'; + +class VoicesDialogTheme extends DialogTheme { + VoicesDialogTheme({ + required VoicesColorScheme colors, + }) : super( + barrierColor: colors.overlay, + shadowColor: colors.dropShadow, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + clipBehavior: Clip.hardEdge, + backgroundColor: colors.elevationsOnSurfaceNeutralLv1White, + ); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_input_decoration_theme.dart b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_input_decoration_theme.dart index c7a76100da3..b5409d1adb0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_input_decoration_theme.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_brands/lib/src/themes/widgets/voices_input_decoration_theme.dart @@ -32,7 +32,7 @@ class _Border extends MaterialStateOutlineInputBorder { if (states.contains(WidgetState.disabled)) { return OutlineInputBorder( borderSide: BorderSide( - color: colors.outlineBorder!, + color: colors.outlineBorder, width: 1, ), borderRadius: BorderRadius.circular(4), @@ -42,7 +42,7 @@ class _Border extends MaterialStateOutlineInputBorder { if (states.contains(WidgetState.error)) { return OutlineInputBorder( borderSide: BorderSide( - color: colors.iconsError!, + color: colors.iconsError, width: 2, ), borderRadius: BorderRadius.circular(4), @@ -61,7 +61,7 @@ class _Border extends MaterialStateOutlineInputBorder { return OutlineInputBorder( borderSide: BorderSide( - color: colors.outlineBorderVariant!, + color: colors.outlineBorderVariant, width: 1, ), borderRadius: BorderRadius.circular(4), 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 f2638c42b23..76e2ec159b0 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 @@ -1293,5 +1293,6 @@ "singleGroupedTagSelectorRelevantTag": "Select the most relevant tag", "noUrlAdded": "No URL added", "addUrl": "Add URL", - "clear": "Clear" + "clear": "Clear", + "errorNoActiveCampaignFound": "Currently there is no active campaign running" } \ No newline at end of file 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 index 55570cc0cae..2bdb0c621cd 100644 --- 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 @@ -1,33 +1,17 @@ -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; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +final class Campaign extends CampaignBase { 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, + required super.id, + required super.name, + required super.description, + required super.startDate, + required super.endDate, + required super.proposalsCount, + required super.publish, }); - int get categoriesCount => sections.map((e) => e.category).toSet().length; - + @override Campaign copyWith({ String? id, String? name, @@ -35,9 +19,7 @@ final class Campaign extends Equatable { DateTime? startDate, DateTime? endDate, int? proposalsCount, - List? sections, CampaignPublish? publish, - ProposalTemplate? proposalTemplate, }) { return Campaign( id: id ?? this.id, @@ -46,22 +28,12 @@ final class Campaign extends Equatable { 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, + ...super.props, ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_base.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_base.dart new file mode 100644 index 00000000000..44c94d4f251 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_base.dart @@ -0,0 +1,86 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; + +/// 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; +} + +// Note. Most, if not all, fields will be removed from here because they come +// from document. +base class CampaignBase extends Equatable { + final String id; + final String name; + final String description; + final DateTime startDate; + final DateTime endDate; + final int proposalsCount; + final CampaignPublish publish; + + const CampaignBase({ + required this.id, + required this.name, + required this.description, + required this.startDate, + required this.endDate, + required this.proposalsCount, + required this.publish, + }); + + int get categoriesCount => 0; + + // TODO(damian-molinski): this should come from api + SignedDocumentRef get proposalTemplateRef { + return const SignedDocumentRef(id: 'schema'); + } + + Campaign toCampaign() { + return Campaign( + id: id, + name: name, + description: description, + startDate: startDate, + endDate: endDate, + proposalsCount: proposalsCount, + publish: publish, + ); + } + + CampaignBase copyWith({ + String? id, + String? name, + String? description, + DateTime? startDate, + DateTime? endDate, + int? proposalsCount, + CampaignPublish? publish, + }) { + return CampaignBase( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + proposalsCount: proposalsCount ?? this.proposalsCount, + publish: publish ?? this.publish, + ); + } + + @override + @mustCallSuper + List get props => [ + id, + name, + description, + startDate, + endDate, + proposalsCount, + publish, + ]; +} 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 deleted file mode 100644 index 51c4923aeab..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_publish.dart +++ /dev/null @@ -1,9 +0,0 @@ -/// 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 deleted file mode 100644 index 23fba579548..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_section.dart +++ /dev/null @@ -1,24 +0,0 @@ -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 ef05d87573a..6c321ff3167 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 @@ -3,9 +3,7 @@ library catalyst_voices_models; export 'app_config.dart'; export 'auth/password_strength.dart'; export 'campaign/campaign.dart'; -export 'campaign/campaign_category.dart'; -export 'campaign/campaign_publish.dart'; -export 'campaign/campaign_section.dart'; +export 'campaign/campaign_base.dart'; export 'crypto/lock_factor.dart'; export 'document/defined_property/grouped_tags.dart'; export 'document/document.dart'; @@ -15,16 +13,18 @@ export 'document/document_definitions.dart'; export 'document/document_node_id.dart'; export 'document/document_schema.dart'; export 'document/document_validator.dart'; +export 'document/signed_document_data.dart'; +export 'document/signed_document_ref.dart'; +export 'document/specialized/proposal_document.dart'; +export 'document/specialized/proposal_template.dart'; export 'errors/errors.dart'; export 'file/voices_file.dart'; export 'markdown_data.dart'; export 'money/money.dart'; export 'node_id.dart'; export 'optional.dart'; -export 'proposal/guidance.dart'; export 'proposal/proposal.dart'; -export 'proposal/proposal_section.dart'; -export 'proposal/proposal_template.dart'; +export 'proposal/proposal_base.dart'; export 'registration/registration.dart'; export 'seed_phrase.dart'; export 'space.dart'; diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document.dart index 4629247aa14..e371e4cbe68 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document.dart @@ -32,7 +32,11 @@ final class Document extends Equatable { } @override - List get props => [schemaUrl, schema, segments]; + List get props => [ + schemaUrl, + schema, + segments, + ]; } /// A segment that groups multiple [DocumentSection]'s. diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_metadata.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_metadata.dart new file mode 100644 index 00000000000..f7980969b05 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_metadata.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; + +abstract base class DocumentMetadata extends Equatable { + final String id; + final String version; + + const DocumentMetadata({ + required this.id, + required this.version, + }); + + /// When building new document version equals id. + const DocumentMetadata.newDocument( + this.id, + ) : version = id; + + @override + @mustCallSuper + List get props => [ + id, + version, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_schema.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_schema.dart index 2da5ee03039..01703d3c61b 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_schema.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_schema.dart @@ -11,7 +11,7 @@ import 'package:meta/meta.dart'; /// [segments] contain [DocumentSchemaSegment.sections] /// and sections contain [DocumentSchemaProperty]'s. final class DocumentSchema extends Equatable implements DocumentNode { - final String schema; + final String jsonSchema; final String title; final String description; final List segments; @@ -19,7 +19,7 @@ final class DocumentSchema extends Equatable implements DocumentNode { final String propertiesSchema; const DocumentSchema({ - required this.schema, + required this.jsonSchema, required this.title, required this.description, required this.segments, @@ -32,7 +32,7 @@ final class DocumentSchema extends Equatable implements DocumentNode { @override List get props => [ - schema, + jsonSchema, title, description, segments, diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/signed_document_data.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/signed_document_data.dart new file mode 100644 index 00000000000..d0816526901 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/signed_document_data.dart @@ -0,0 +1,102 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; + +// List of types and metadata fields is here +// https://input-output-hk.github.io/catalyst-libs/branch/feat_signed_object/architecture/08_concepts/signed_doc/types/ + +extension type const SignedDocumentDataPayload(Map data) + implements Object {} + +enum SignedDocumentType { + proposalDocument(uuid: '7808d2ba-d511-40af-84e8-c0d1625fdfdc'), + proposalTemplate(uuid: '0ce8ab38-9258-4fbc-a62e-7faa6e58318f'); + + final String uuid; + + const SignedDocumentType({ + required this.uuid, + }); +} + +final class SignedDocumentData extends Equatable { + final SignedDocumentMetadata metadata; + final SignedDocumentDataPayload payload; + + const SignedDocumentData({ + required this.metadata, + required this.payload, + }); + + @override + List get props => [ + metadata, + payload, + ]; +} + +final class SignedDocumentMetadata extends Equatable { + /// Type of this signed document + final SignedDocumentType type; + + /// uuid-v7 + final String id; + + /// uuid-v7 + final String version; + + /// Reference to another document. The purpose of the ref will vary depending + /// on the document type. + final SignedDocumentRef? ref; + + /// This is a cryptographically secured reference to another document. + final SecuredSignedDocumentRef? refHash; + + /// If the document was formed from a template, this is a reference to that + /// template document + final SignedDocumentRef? template; + + /// uuid-v4 + /// Represents a "brand" who is running the voting, e.g. Catalyst, Midnight. + final String? brandId; + + /// uuid-v4 + /// Defines a "campaign" of voting, e.g. "treasury campaign". + final String? campaignId; + + /// uuid-v4 + /// Defines an election, e.g. "Catalyst Fund 1", "Catalyst Fund 2". + final String? electionId; + + /// uuid-v4 + /// Defines a voting category as a collection of proposals, e.g. + /// "Development & Infrastructure", + /// "Products & Integrations". + final String? categoryId; + + const SignedDocumentMetadata({ + required this.type, + required this.id, + required this.version, + this.ref, + this.refHash, + this.template, + this.brandId, + this.campaignId, + this.electionId, + this.categoryId, + }); + + @override + List get props => [ + type, + id, + version, + ref, + refHash, + template, + brandId, + campaignId, + electionId, + categoryId, + ]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/signed_document_ref.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/signed_document_ref.dart new file mode 100644 index 00000000000..6653170caab --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/signed_document_ref.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; + +final class SignedDocumentRef extends Equatable { + final String id; + final String? version; + + const SignedDocumentRef({ + required this.id, + this.version, + }); + + @override + List get props => [id, version]; +} + +final class SecuredSignedDocumentRef extends Equatable { + final SignedDocumentRef ref; + final String hash; + + const SecuredSignedDocumentRef({ + required this.ref, + required this.hash, + }); + + @override + List get props => [ref, hash]; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart new file mode 100644 index 00000000000..73e30a036a4 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_document.dart @@ -0,0 +1,26 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_models/src/document/document_metadata.dart'; +import 'package:equatable/equatable.dart'; + +final class ProposalDocument extends Equatable { + final ProposalMetadata metadata; + final Document document; + + const ProposalDocument({ + required this.metadata, + required this.document, + }); + + @override + List get props => [ + metadata, + document, + ]; +} + +final class ProposalMetadata extends DocumentMetadata { + const ProposalMetadata({ + required super.id, + required super.version, + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_template.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_template.dart new file mode 100644 index 00000000000..7c55b182394 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/specialized/proposal_template.dart @@ -0,0 +1,26 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_models/src/document/document_metadata.dart'; +import 'package:equatable/equatable.dart'; + +final class ProposalTemplate extends Equatable { + final ProposalTemplateMetadata metadata; + final DocumentSchema schema; + + const ProposalTemplate({ + required this.metadata, + required this.schema, + }); + + @override + List get props => [ + metadata, + schema, + ]; +} + +final class ProposalTemplateMetadata extends DocumentMetadata { + const ProposalTemplateMetadata({ + required super.id, + required super.version, + }); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart deleted file mode 100644 index f51cc667d37..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/guidance.dart +++ /dev/null @@ -1,60 +0,0 @@ -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; - - /// 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, - }); - - @override - int compareTo(Guidance other) { - final typeComparison = type.priority.compareTo(other.type.priority); - if (typeComparison != 0) { - return typeComparison; - } - if (weight == null && other.weight == null) return 0; - if (weight == null) return 1; - if (other.weight == null) return -1; - return weight!.compareTo(other.weight!); - } - - @override - List get props => [ - id, - title, - description, - type, - weight, - ]; -} - -extension GuidanceExt on List { - List sortedByWeight() { - return [...this]..sort(); - } -} 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 index 68995d67524..4e99e917696 100644 --- 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 @@ -1,65 +1,25 @@ -import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; -import 'package:catalyst_voices_models/src/proposal/proposal_section.dart'; -import 'package:equatable/equatable.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.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; +final class Proposal extends ProposalBase { + final ProposalDocument document; 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, + required super.id, + required super.title, + required super.description, + required super.updateDate, + required super.fundsRequested, + required super.status, + required super.publish, + required super.access, + required super.category, + required super.commentsCount, + required this.document, }); - 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, + ...super.props, + document, ]; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart new file mode 100644 index 00000000000..30ec91c312b --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_base.dart @@ -0,0 +1,86 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.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 } + +// Note. Most, if not all, fields will be removed from here because they come +// from document. +base class ProposalBase 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; + + // This may be a reference to class + final String category; + + // Those may be getters. + final int commentsCount; + + const ProposalBase({ + 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.category, + required this.commentsCount, + }); + + Proposal toProposal({ + required ProposalDocument document, + }) { + return Proposal( + id: id, + title: title, + description: description, + updateDate: updateDate, + fundsRequested: fundsRequested, + status: status, + publish: publish, + access: access, + category: category, + commentsCount: commentsCount, + document: document, + ); + } + + int get totalSegments => 0; + + int get completedSegments => 0; + + // TODO(damian-molinski): this should come from api + SignedDocumentRef get ref => const SignedDocumentRef(id: 'document'); + + @override + @mustCallSuper + List get props => [ + id, + title, + description, + updateDate, + fundedDate, + fundsRequested.value, + status, + publish, + access, + 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 deleted file mode 100644 index 069fc44ebb9..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_section.dart +++ /dev/null @@ -1,78 +0,0 @@ -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_template.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart deleted file mode 100644 index afd4aa586be..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/proposal/proposal_template.dart +++ /dev/null @@ -1,15 +0,0 @@ -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_repositories/lib/src/campaign/campaign_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/campaign/campaign_repository.dart index 67785d512cc..b539fbd9d1c 100644 --- 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 @@ -1,27 +1,24 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -class CampaignRepository { - Future getCampaign({ +// ignore: one_member_abstracts +abstract interface class CampaignRepository { + factory CampaignRepository() = CampaignRepositoryImpl; + + Future getCampaign({ + required String id, + }); +} + +final class CampaignRepositoryImpl implements CampaignRepository { + const CampaignRepositoryImpl(); + + @override + 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( + return CampaignBase( id: id, name: 'Boost Social Entrepreneurship', description: 'We are currently only decentralizing our technology, ' @@ -30,124 +27,7 @@ class CampaignRepository { 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 c3a83f1d2c1..275c4e6557c 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,6 +1,7 @@ -export 'campaign/campaign_repository.dart'; +export 'campaign/campaign_repository.dart' show CampaignRepository; export 'config/config_repository.dart' show ConfigRepository; -export 'proposal/proposal_repository.dart'; +export 'document/document_repository.dart' show DocumentRepository; +export 'proposal/proposal_repository.dart' show ProposalRepository; 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/document/document_repository.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart new file mode 100644 index 00000000000..4911016232a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/document/document_repository.dart @@ -0,0 +1,140 @@ +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/document/document_data_dto.dart'; +import 'package:catalyst_voices_repositories/src/dto/document/document_dto.dart'; +import 'package:catalyst_voices_repositories/src/dto/document/schema/document_schema_dto.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:synchronized/synchronized.dart'; +import 'package:uuid/uuid.dart'; + +abstract interface class DocumentRepository { + factory DocumentRepository( + SignedDocumentManager signedDocumentManager, + ) = DocumentRepositoryImpl; + + Future publishDocument(SignedDocumentData document); + + Future getProposalDocument({ + required SignedDocumentRef ref, + }); + + Future getProposalTemplate({ + required SignedDocumentRef ref, + }); +} + +final class DocumentRepositoryImpl implements DocumentRepository { + // ignore: unused_field + final SignedDocumentManager _signedDocumentManager; + + final _proposalTemplateLock = Lock(); + + DocumentRepositoryImpl( + this._signedDocumentManager, + ); + + @override + Future publishDocument(SignedDocumentData document) { + throw UnimplementedError(); + } + + @override + Future getProposalDocument({ + required SignedDocumentRef ref, + }) async { + // TODO(damian-molinski): remove this override once we have API + ref = const SignedDocumentRef(id: 'proposal'); + + final signedDocumentData = await _getSignedDocumentData(ref: ref); + + assert( + signedDocumentData.metadata.type == SignedDocumentType.proposalDocument, + 'Invalid Proposal SignedDocument type', + ); + assert( + signedDocumentData.metadata.template != null, + 'Proposal metadata has no template', + ); + + final templateRef = signedDocumentData.metadata.template!; + + final template = await _proposalTemplateLock.synchronized(() { + return getProposalTemplate(ref: templateRef); + }); + + final metadata = ProposalMetadata( + id: signedDocumentData.metadata.id, + version: signedDocumentData.metadata.version, + ); + + final data = DocumentDataDto.fromJson(signedDocumentData.payload.data); + final schema = template.schema; + final document = DocumentDto.fromJsonSchema(data, schema).toModel(); + + return ProposalDocument( + metadata: metadata, + document: document, + ); + } + + @override + Future getProposalTemplate({ + required SignedDocumentRef ref, + }) async { + // TODO(damian-molinski): remove this override once we have API + ref = const SignedDocumentRef(id: 'schema'); + + final signedDocumentData = await _getSignedDocumentData(ref: ref); + + assert( + signedDocumentData.metadata.type == SignedDocumentType.proposalTemplate, + 'Invalid SignedDocument type', + ); + + final metadata = ProposalTemplateMetadata( + id: signedDocumentData.metadata.id, + version: signedDocumentData.metadata.version, + ); + + final json = signedDocumentData.payload.data; + final schema = DocumentSchemaDto.fromJson(json).toModel(); + + return ProposalTemplate( + metadata: metadata, + schema: schema, + ); + } + + // TODO(damian-molinski): should return SignedDocument. + // TODO(damian-molinski): make API call. + // TODO(damian-molinski): implement caching. + Future _getSignedDocumentData({ + required SignedDocumentRef ref, + }) async { + final isSchema = ref.id == 'schema'; + + final signedDocument = await (isSchema + ? VoicesDocumentsTemplates.proposalF14Schema + : VoicesDocumentsTemplates.proposalF14Document); + + final type = isSchema + ? SignedDocumentType.proposalTemplate + : SignedDocumentType.proposalDocument; + final ver = ref.version ?? const Uuid().v7(); + final template = !isSchema ? const SignedDocumentRef(id: 'schema') : null; + + final metadata = SignedDocumentMetadata( + type: type, + id: ref.id, + version: ver, + template: template, + ); + + final payload = SignedDocumentDataPayload(signedDocument); + + return SignedDocumentData( + metadata: metadata, + payload: payload, + ); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_properties_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_data_dto.dart similarity index 67% rename from catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_properties_dto.dart rename to catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_data_dto.dart index f6364904b37..ddeb5abf03a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_properties_dto.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_data_dto.dart @@ -1,10 +1,22 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; /// Utility structure for traversing a json map using [DocumentNodeId]'s. -final class DocumentPropertiesDto { +final class DocumentDataDto { final Map json; - const DocumentPropertiesDto.fromJson(this.json); + String get schemaUrl => json[r'$schema'] as String; + + const DocumentDataDto.fromJson(this.json); + + factory DocumentDataDto.fromDocument({ + required String schemaUrl, + required Iterable> segments, + }) { + return DocumentDataDto.fromJson({ + r'$schema': schemaUrl, + for (final segment in segments) ...segment, + }); + } /// Retrieves the value of a property located at the specified [nodeId]. /// diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_dto.dart index 6ac6e70cc1e..b51a9e55db2 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_dto.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_dto.dart @@ -1,5 +1,5 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_repositories/src/dto/document/document_properties_dto.dart'; +import 'package:catalyst_voices_repositories/src/dto/document/document_data_dto.dart'; import 'package:catalyst_voices_repositories/src/dto/document/schema/document_definitions_converter_ext.dart'; /// A data transfer object for the [Document]. @@ -18,19 +18,17 @@ final class DocumentDto { }); factory DocumentDto.fromJsonSchema( - Map json, + DocumentDataDto data, DocumentSchema schema, ) { - final properties = DocumentPropertiesDto.fromJson(json); - return DocumentDto( - schemaUrl: json[r'$schema'] as String, + schemaUrl: data.schemaUrl, schema: schema, segments: schema.segments .map( (segment) => DocumentSegmentDto.fromJsonSchema( segment, - properties: properties, + data: data, ), ) .toList(), @@ -56,11 +54,11 @@ final class DocumentDto { ).build(); } - Map toJson() { - return { - r'$schema': schemaUrl, - for (final segment in segments) ...segment.toJson(), - }; + DocumentDataDto toJson() { + return DocumentDataDto.fromDocument( + schemaUrl: schemaUrl, + segments: segments.map((e) => e.toJson()), + ); } } @@ -75,7 +73,7 @@ final class DocumentSegmentDto { factory DocumentSegmentDto.fromJsonSchema( DocumentSchemaSegment schema, { - required DocumentPropertiesDto properties, + required DocumentDataDto data, }) { return DocumentSegmentDto( schema: schema, @@ -83,7 +81,7 @@ final class DocumentSegmentDto { .map( (section) => DocumentSectionDto.fromJsonSchema( section, - properties: properties, + data: data, ), ) .toList(), @@ -124,7 +122,7 @@ final class DocumentSectionDto { factory DocumentSectionDto.fromJsonSchema( DocumentSchemaSection schema, { - required DocumentPropertiesDto properties, + required DocumentDataDto data, }) { return DocumentSectionDto( schema: schema, @@ -132,7 +130,7 @@ final class DocumentSectionDto { .map( (property) => DocumentPropertyDto.fromJsonSchema( property, - properties: properties, + data: data, ), ) .toList(), @@ -173,9 +171,9 @@ final class DocumentPropertyDto { factory DocumentPropertyDto.fromJsonSchema( DocumentSchemaProperty schema, { - required DocumentPropertiesDto properties, + required DocumentDataDto data, }) { - final property = properties.getProperty(schema.nodeId); + final property = data.getProperty(schema.nodeId); final value = schema.definition.converter.fromJson(property); return DocumentPropertyDto( schema: schema, diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_schema_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_schema_dto.dart index c1439c26cb5..c1cf33d1a6d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_schema_dto.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_schema_dto.dart @@ -9,9 +9,9 @@ part 'document_schema_dto.g.dart'; @JsonSerializable() final class DocumentSchemaDto { @JsonKey(name: r'$schema') - final String schema; + final String jsonSchema; @JsonKey(name: r'$id') - final String id; + final String jsonSchemaId; final String title; final String description; final DocumentDefinitionsDto definitions; @@ -26,8 +26,8 @@ final class DocumentSchemaDto { final String propertiesSchema; const DocumentSchemaDto({ - required this.schema, - required this.id, + required this.jsonSchema, + required this.jsonSchemaId, required this.title, required this.description, required this.definitions, @@ -37,6 +37,7 @@ final class DocumentSchemaDto { required this.order, required this.propertiesSchema, }); + factory DocumentSchemaDto.fromJson(Map json) { final segmentsMap = json['properties'] as Map; json['propertiesSchema'] = @@ -62,7 +63,7 @@ final class DocumentSchemaDto { .toList(); return DocumentSchema( - schema: schema, + jsonSchema: jsonSchema, title: title, description: description, segments: mappedSegments, 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 index 01dfd972975..976d5df8c78 100644 --- 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 @@ -2,15 +2,34 @@ import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.da import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; -class ProposalRepository { - const ProposalRepository(); +// ignore: one_member_abstracts +abstract interface class ProposalRepository { + const factory ProposalRepository() = ProposalRepositoryImpl; + + Future getProposal({ + required String id, + }); /// Fetches all proposals. - Future> getProposals({ + Future> getProposals({ + required String campaignId, + }); +} + +final class ProposalRepositoryImpl implements ProposalRepository { + const ProposalRepositoryImpl(); + + @override + Future getProposal({ + required String id, + }) async { + return _proposals.first; + } + + @override + Future> getProposals({ required String campaignId, }) async { - // simulate network delay - await Future.delayed(const Duration(seconds: 1)); // optionally filter by status. return _proposals; } @@ -25,7 +44,7 @@ and PRISM, but its potential is only barely exploited. .replaceAll('\n', ' '); final _proposals = [ - Proposal( + ProposalBase( id: 'f14/0', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', @@ -36,20 +55,8 @@ final _proposals = [ 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( + ProposalBase( id: 'f14/1', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', @@ -60,21 +67,8 @@ final _proposals = [ 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( + ProposalBase( id: 'f14/2', category: 'Cardano Use Cases / MVP', title: 'Proposal Title that rocks the world', @@ -85,18 +79,5 @@ final _proposals = [ 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/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_repositories/pubspec.yaml index 6eece78d6a7..6e4efc5599f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/pubspec.yaml @@ -9,6 +9,9 @@ environment: dependencies: catalyst_cardano_serialization: ^0.5.0+3 + # using for document json assets + catalyst_voices_assets: + path: ../catalyst_voices_assets catalyst_voices_models: path: ../catalyst_voices_models catalyst_voices_shared: @@ -21,12 +24,15 @@ dependencies: json_annotation: ^4.8.1 result_type: ^0.2.0 rxdart: ^0.27.7 + synchronized: ^3.3.0+3 + uuid: ^4.5.1 dev_dependencies: build_runner: ^2.4.12 catalyst_analysis: ^2.0.1 chopper_generator: ^8.0.3 + flutter_test: + sdk: flutter json_serializable: ^6.7.1 mockito: ^5.4.4 - swagger_dart_code_generator: ^3.0.1 - test: ^1.24.9 + swagger_dart_code_generator: ^3.0.1 \ 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 deleted file mode 100644 index 982db98a6ba..00000000000 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/helpers/read_json.dart +++ /dev/null @@ -1,10 +0,0 @@ -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/catalyst_voices_repositories_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/catalyst_voices_repositories_test.dart index e591dc15668..9a0dc884aa0 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/catalyst_voices_repositories_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/catalyst_voices_repositories_test.dart @@ -1,4 +1,4 @@ -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('CatalystVoicesRepositories', () {}); diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_dto_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_dto_test.dart index 4206d3a15c9..cc17f62c9b5 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_dto_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_dto_test.dart @@ -1,25 +1,22 @@ import 'dart:convert'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/dto/document/document_data_dto.dart'; import 'package:catalyst_voices_repositories/src/dto/document/document_dto.dart'; import 'package:catalyst_voices_repositories/src/dto/document/schema/document_schema_dto.dart'; -import 'package:test/test.dart'; - -import '../../helpers/read_json.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { - group(DocumentDto, () { - const schemaPath = - 'test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json'; - const documentPath = 'test/assets/generic_proposal.json'; + TestWidgetsFlutterBinding.ensureInitialized(); + group(DocumentDto, () { late Map schemaJson; late Map documentJson; - setUpAll(() { - schemaJson = json.decode(readJson(schemaPath)) as Map; - documentJson = - json.decode(readJson(documentPath)) as Map; + setUpAll(() async { + schemaJson = await VoicesDocumentsTemplates.proposalF14Schema; + documentJson = await VoicesDocumentsTemplates.proposalF14Document; }); test( @@ -27,12 +24,13 @@ void main() { 'should result in the same document', () { final schema = DocumentSchemaDto.fromJson(schemaJson).toModel(); + final data = DocumentDataDto.fromJson(documentJson); // original final originalJsonString = json.encode(documentJson); // serialized and deserialized - final documentDto = DocumentDto.fromJsonSchema(documentJson, schema); + final documentDto = DocumentDto.fromJsonSchema(data, schema); final documentDtoJson = documentDto.toJson(); final serializedJsonString = json.encode(documentDtoJson); @@ -49,9 +47,10 @@ void main() { 'Roundtrip from json to model and reverse ' 'should result in the same document', () { final schema = DocumentSchemaDto.fromJson(schemaJson).toModel(); + final data = DocumentDataDto.fromJson(documentJson); // original - final originalDocDto = DocumentDto.fromJsonSchema(documentJson, schema); + final originalDocDto = DocumentDto.fromJsonSchema(data, schema); final originalDoc = originalDocDto.toModel(); // serialized and deserialized @@ -69,15 +68,16 @@ void main() { final schema = schemaDto.toModel(); final document = DocumentBuilder.fromSchema( - schemaUrl: schemaPath, + schemaUrl: '', schema: schema, ).build(); final documentDto = DocumentDto.fromModel(document); - final documentJson = documentDto.toJson(); + final documentData = documentDto.toJson(); for (final segment in documentDto.segments) { - expect(documentJson[segment.schema.id], isA>()); + final actual = documentData.json[segment.schema.id]; + expect(actual, isA>()); } }); @@ -86,7 +86,7 @@ void main() { final schema = schemaDto.toModel(); final document = DocumentBuilder.fromSchema( - schemaUrl: schemaPath, + schemaUrl: '', schema: schema, ).build(); @@ -105,8 +105,9 @@ void main() { test('After serialization $DocumentPropertyDto has correct type', () { final schemaDto = DocumentSchemaDto.fromJson(schemaJson); final schema = schemaDto.toModel(); + final data = DocumentDataDto.fromJson(documentJson); - final documentDto = DocumentDto.fromJsonSchema(documentJson, schema); + final documentDto = DocumentDto.fromJsonSchema(data, schema); final agreementSegment = documentDto.segments .indexWhere((e) => e.schema.nodeId.paths.last == 'agreements'); diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_definitions_dto_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_definitions_dto_test.dart index ba13a97dac0..e9ce604cefc 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_definitions_dto_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_definitions_dto_test.dart @@ -1,20 +1,16 @@ -import 'dart:convert'; - +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/src/dto/document/schema/document_schema_dto.dart'; -import 'package:test/test.dart'; - -import '../../../helpers/read_json.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { - group('$DocumentSchemaDto definitions', () { - const schemaPath = - 'test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json'; + TestWidgetsFlutterBinding.ensureInitialized(); + group('$DocumentSchemaDto definitions', () { late Map schemaJson; - setUpAll(() { - schemaJson = json.decode(readJson(schemaPath)) as Map; + setUpAll(() async { + schemaJson = await VoicesDocumentsTemplates.proposalF14Schema; }); test( diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_schema_dto_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_schema_dto_test.dart index 6e36b2dbe3e..3acb7c2e2dc 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_schema_dto_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_schema_dto_test.dart @@ -1,20 +1,16 @@ -import 'dart:convert'; - +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/src/dto/document/schema/document_schema_dto.dart'; -import 'package:test/test.dart'; - -import '../../../helpers/read_json.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { - group(DocumentSchemaDto, () { - const schemaPath = - 'test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json'; + TestWidgetsFlutterBinding.ensureInitialized(); + group(DocumentSchemaDto, () { late Map schemaJson; - setUpAll(() { - schemaJson = json.decode(readJson(schemaPath)) as Map; + setUpAll(() async { + schemaJson = await VoicesDocumentsTemplates.proposalF14Schema; }); test('X-order of segments is kept in model class', () async { diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_schema_property_dto_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_schema_property_dto_test.dart index f82d3cb3207..898da822afa 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_schema_property_dto_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/schema/document_schema_property_dto_test.dart @@ -1,19 +1,15 @@ -import 'dart:convert'; - +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_repositories/src/dto/document/schema/document_schema_property_dto.dart'; -import 'package:test/test.dart'; - -import '../../../helpers/read_json.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { - group(DocumentSchemaPropertyDto, () { - const schemaPath = - 'test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json'; + TestWidgetsFlutterBinding.ensureInitialized(); + group(DocumentSchemaPropertyDto, () { late Map schemaJson; - setUpAll(() { - schemaJson = json.decode(readJson(schemaPath)) as Map; + setUpAll(() async { + schemaJson = await VoicesDocumentsTemplates.proposalF14Schema; }); test('includeIfNull does not add keys for values that are null', () { diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/transaction/transaction_config_repository_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/transaction/transaction_config_repository_test.dart index ad3559e384e..9688c76c704 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/transaction/transaction_config_repository_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/transaction/transaction_config_repository_test.dart @@ -1,6 +1,6 @@ import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group(TransactionConfigRepository, () { 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 index 58e5e2388bb..b59c24c7a67 100644 --- 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 @@ -2,34 +2,41 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; abstract interface class CampaignService { - factory CampaignService( + const factory CampaignService( CampaignRepository campaignRepository, - ) { - return CampaignServiceImpl( - campaignRepository, - ); - } - - Future isAnyCampaignActive(); + DocumentRepository documentRepository, + ) = CampaignServiceImpl; Future getActiveCampaign(); + + Future getCampaign({ + required String id, + }); } final class CampaignServiceImpl implements CampaignService { final CampaignRepository _campaignRepository; + // ignore: unused_field + final DocumentRepository _documentRepository; + const CampaignServiceImpl( this._campaignRepository, + this._documentRepository, ); @override - Future isAnyCampaignActive() async { - return true; - } + Future getActiveCampaign() => getCampaign(id: 'F14'); @override - Future getActiveCampaign() async { - final campaign = await _campaignRepository.getCampaign(id: 'F14'); + Future getCampaign({ + required String id, + }) async { + final campaignBase = await _campaignRepository.getCampaign(id: id); + + // TODO(damian-molinski): get proposalTemplateRef, document and map. + + final campaign = campaignBase.toCampaign(); return campaign; } 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 index 3d2b87f3fa0..9b155c0a736 100644 --- 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 @@ -5,28 +5,78 @@ import 'package:catalyst_voices_repositories/catalyst_voices_repositories.dart'; abstract interface class ProposalService { factory ProposalService( ProposalRepository proposalRepository, + DocumentRepository documentRepository, ) { return ProposalServiceImpl( proposalRepository, + documentRepository, ); } + Future getProposal({ + required String id, + }); + + Future getProposalTemplate({ + required SignedDocumentRef ref, + }); + /// Fetches proposals for the [campaignId]. - Future> getProposals({required String campaignId}); + Future> getProposals({ + required String campaignId, + }); } final class ProposalServiceImpl implements ProposalService { final ProposalRepository _proposalRepository; + final DocumentRepository _documentRepository; const ProposalServiceImpl( this._proposalRepository, + this._documentRepository, ); @override - Future> getProposals({required String campaignId}) async { - final proposals = - await _proposalRepository.getProposals(campaignId: campaignId); + Future getProposal({ + required String id, + }) async { + final proposalBase = await _proposalRepository.getProposal(id: id); + final proposal = await _buildProposal(proposalBase); + + return proposal; + } + + @override + Future getProposalTemplate({ + required SignedDocumentRef ref, + }) async { + final proposalTemplate = await _documentRepository.getProposalTemplate( + ref: ref, + ); + + return proposalTemplate; + } + + @override + Future> getProposals({ + required String campaignId, + }) async { + final proposalBases = await _proposalRepository.getProposals( + campaignId: campaignId, + ); + + final futures = proposalBases.map(_buildProposal); + + final proposals = await Future.wait(futures); return proposals; } + + Future _buildProposal(ProposalBase base) async { + final proposalDocument = await _documentRepository.getProposalDocument( + ref: base.ref, + ); + + return base.toProposal(document: proposalDocument); + } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/local_crypto_service.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/local_crypto_service.dart index c783526f667..08f2aa1c7f6 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/local_crypto_service.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/crypto/local_crypto_service.dart @@ -224,7 +224,7 @@ final class LocalCryptoService implements CryptoService { return future.whenComplete( () { stopwatch.stop(); - _logger.finer('Took[$debugLabel] ${stopwatch.elapsed}'); + _logger.finest('Took[$debugLabel] ${stopwatch.elapsed}'); }, ); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_category.dart similarity index 100% rename from catalyst_voices/packages/internal/catalyst_voices_models/lib/src/campaign/campaign_category.dart rename to catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/campaign_category.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 index 7baf375a4e6..303ac1529d4 100644 --- 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 @@ -1,4 +1,4 @@ -import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/src/campaign/campaign_category.dart'; import 'package:catalyst_voices_view_models/src/menu/menu_item.dart'; import 'package:equatable/equatable.dart'; @@ -19,14 +19,6 @@ final class CampaignCategorySection extends Equatable implements MenuItem { 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; diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/exception/active_campaign_not_found_exception.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/exception/active_campaign_not_found_exception.dart new file mode 100644 index 00000000000..4da079c108a --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/campaign/exception/active_campaign_not_found_exception.dart @@ -0,0 +1,12 @@ +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'; + +final class ActiveCampaignNotFoundException extends LocalizedException { + const ActiveCampaignNotFoundException(); + + @override + String message(BuildContext context) { + return context.l10n.errorNoActiveCampaignFound; + } +} 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 bbb572b4d24..0ff8337fa4d 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,8 +1,10 @@ export 'authentication/authentication.dart'; +export 'campaign/campaign_category.dart'; export 'campaign/campaign_category_section.dart'; export 'campaign/campaign_info.dart'; export 'campaign/campaign_list_item.dart'; export 'campaign/campaign_stage.dart'; +export 'campaign/exception/active_campaign_not_found_exception.dart'; export 'document/validation/localized_document_validation_result.dart'; export 'exception/localized_exception.dart'; export 'exception/localized_unknown_exception.dart'; 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 d15becd4aeb..ac65598eb98 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: sdk: flutter formz: ^0.7.0 intl: ^0.19.0 + uuid: ^4.5.1 dev_dependencies: catalyst_analysis: ^2.0.1 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 index 6d10f5d3da6..b28fd5d5169 100644 --- 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 @@ -13,9 +13,7 @@ void main() { startDate: date, endDate: date, proposalsCount: 0, - sections: const [], publish: CampaignPublish.draft, - proposalTemplate: const ProposalTemplate(sections: []), ); test('draft campaign resolves to draft stage', () { diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_avatar_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_avatar_example.dart index 5c1c42ae9a1..4f3ae128b4a 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_avatar_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_avatar_example.dart @@ -38,7 +38,7 @@ class VoicesAvatarExample extends StatelessWidget { icon: VoicesAssets.icons.lightBulb.buildIcon(), foregroundColor: Theme.of(context).colors.iconsSecondary, backgroundColor: - Theme.of(context).colors.iconsSecondary?.withOpacity(0.16), + Theme.of(context).colors.iconsSecondary.withOpacity(0.16), ), VoicesAvatar( icon: Image.asset(UiKitAssets.images.robotAvatar.path), diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_badge_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_badge_example.dart index 3fd662d88d1..54297a54bb2 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_badge_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_badge_example.dart @@ -10,7 +10,7 @@ class VoicesBadgeExample extends StatelessWidget { Widget build(BuildContext context) { final colors = [ Theme.of(context).colorScheme.error, - Theme.of(context).colors.success!, + Theme.of(context).colors.success, ]; return Scaffold( 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 9d6286621e8..c3bd20de3ea 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 @@ -68,7 +68,7 @@ class VoicesModalsExample extends StatelessWidget { color: Theme.of(context).colors.iconsError, ), border: Border.all( - color: Theme.of(context).colors.iconsError!, + color: Theme.of(context).colors.iconsError, width: 3, ), ),