From 0d4c725c04ce1fd18a236d0301359c49307ca147 Mon Sep 17 00:00:00 2001 From: danvleju-rdx <163979791+danvleju-rdx@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:28:28 +0300 Subject: [PATCH 1/6] Image service URL construction (#221) * wip * bump to 1.1.18 * split tests * address feedback * wip * bump to 1.1.18 * split tests * address feedback * Create kotlin extensions * Bump version * add test --------- Co-authored-by: micbakos-rdx --- Cargo.lock | 2 +- apple/Sources/Sargon/Util/URL+image.swift | 17 +++ .../TestCases/Prelude/ImageURLTests.swift | 37 ++++++ crates/sargon/Cargo.toml | 2 +- .../sargon/src/core/utils/image_url_utils.rs | 120 ++++++++++++++++++ .../core/utils/image_url_utils_uniffi_fn.rs | 55 ++++++++ crates/sargon/src/core/utils/mod.rs | 4 + crates/sargon/src/core/utils/string_utils.rs | 20 +++ crates/sargon/src/types/mod.rs | 4 + crates/sargon/src/types/vector_image_type.rs | 64 ++++++++++ .../src/types/vector_image_type_uniffi_fn.rs | 49 +++++++ .../com/radixdlt/sargon/extensions/Url.kt | 30 +++++ .../sargon/extensions/VectorImageType.kt | 11 ++ .../sargon/samples/VectorImageTypeSample.kt | 14 ++ .../test/java/com/radixdlt/sargon/UrlTest.kt | 117 +++++++++++++++++ .../radixdlt/sargon/VectorImageTypeTest.kt | 25 ++++ 16 files changed, 569 insertions(+), 2 deletions(-) create mode 100644 apple/Sources/Sargon/Util/URL+image.swift create mode 100644 apple/Tests/TestCases/Prelude/ImageURLTests.swift create mode 100644 crates/sargon/src/core/utils/image_url_utils.rs create mode 100644 crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs create mode 100644 crates/sargon/src/types/vector_image_type.rs create mode 100644 crates/sargon/src/types/vector_image_type_uniffi_fn.rs create mode 100644 jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/Url.kt create mode 100644 jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/VectorImageType.kt create mode 100644 jvm/sargon-android/src/main/java/com/radixdlt/sargon/samples/VectorImageTypeSample.kt create mode 100644 jvm/sargon-android/src/test/java/com/radixdlt/sargon/UrlTest.kt create mode 100644 jvm/sargon-android/src/test/java/com/radixdlt/sargon/VectorImageTypeTest.kt diff --git a/Cargo.lock b/Cargo.lock index 0c40f6cb4..e8dc1cc57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2617,7 +2617,7 @@ dependencies = [ [[package]] name = "sargon" -version = "1.1.16" +version = "1.1.19" dependencies = [ "actix-rt", "aes-gcm", diff --git a/apple/Sources/Sargon/Util/URL+image.swift b/apple/Sources/Sargon/Util/URL+image.swift new file mode 100644 index 000000000..859c91c47 --- /dev/null +++ b/apple/Sources/Sargon/Util/URL+image.swift @@ -0,0 +1,17 @@ +import Foundation +import SargonUniFFI + +extension URL { + public func isVectorImage(type: VectorImageType) -> Bool { + imageUrlUtilsIsVectorImage(url: self.absoluteString, imageType: type) + } + + public func imageURL(imageServiceURL: URL, size: CGSize) throws -> URL { + try imageUrlUtilsMakeImageUrl( + url: self.absoluteString, + imageServiceUrl: imageServiceURL.absoluteString, + width: UInt32(size.width), + height: UInt32(size.height) + ) + } +} diff --git a/apple/Tests/TestCases/Prelude/ImageURLTests.swift b/apple/Tests/TestCases/Prelude/ImageURLTests.swift new file mode 100644 index 000000000..fb81571d4 --- /dev/null +++ b/apple/Tests/TestCases/Prelude/ImageURLTests.swift @@ -0,0 +1,37 @@ +@testable import Sargon + +import CustomDump +import Foundation +import Sargon +import SargonUniFFI +import XCTest + +final class ImageURLTests: XCTestCase { + func test_is_vector_image() { + let svgURL = URL(string: "https://svgshare.com/i/U7z.svg")! + + XCTAssert(svgURL.isVectorImage(type: .svg)) + } + + func test_image_url() throws { + let size = CGSize(width: 1024, height: 1024) + let imageServiceURL = URL(string: "https://image-service-dev.extratools.works")! + let svgURL = URL(string: "https://svgshare.com/i/U7z.svg")! + + XCTAssertEqual( + try svgURL.imageURL(imageServiceURL: imageServiceURL, size: size), + URL(string: "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg&imageSize=1024x1024&format=png") + ) + } + + func test_image_url_with_data_url() throws { + let size = CGSize(width: 1024, height: 1024) + let imageServiceURL = URL(string: "https://image-service-dev.extratools.works")! + let svgDataURL = URL(string: "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg%3E")! + + XCTAssertEqual( + try svgDataURL.imageURL(imageServiceURL: imageServiceURL, size: size), + URL(string: "https://image-service-dev.extratools.works/?imageOrigin=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%2529%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%25200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%25221000%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L500%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%253E&imageSize=1024x1024&format=png") + ) + } +} \ No newline at end of file diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 2239f4833..d849618a2 100644 --- a/crates/sargon/Cargo.toml +++ b/crates/sargon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sargon" -version = "1.1.16" +version = "1.1.19" edition = "2021" build = "build.rs" diff --git a/crates/sargon/src/core/utils/image_url_utils.rs b/crates/sargon/src/core/utils/image_url_utils.rs new file mode 100644 index 000000000..48ec9a264 --- /dev/null +++ b/crates/sargon/src/core/utils/image_url_utils.rs @@ -0,0 +1,120 @@ +use crate::prelude::*; +use crate::types::*; + +pub fn is_vector_image(url: &str, image_type: VectorImageType) -> bool { + let parsed_url = match parse_url(url) { + Ok(parsed) => parsed, + Err(_) => return false, + }; + let query_parameters = parsed_url + .query_pairs() + .into_owned() + .collect::>(); + let image_url_string = query_parameters + .get("imageOrigin") + .map(|s| s.as_str()) + .unwrap_or(url); + + image_url_string + .starts_with(&format!("data:image/{}", image_type.data_url_type())) + || image_url_string + .to_lowercase() + .ends_with(image_type.url_extension()) +} + +pub fn make_image_url( + url: &str, + image_service_url: &str, + width: u32, + height: u32, +) -> Result { + const MIN_SIZE: u32 = 64; + + let image_origin = url_encode(url); + let image_size = + format!("{}x{}", width.max(MIN_SIZE), height.max(MIN_SIZE)); + let mut query = + format!("imageOrigin={}&imageSize={}", image_origin, image_size); + + if is_vector_image(url, VectorImageType::Svg) { + query.push_str("&format=png"); + } + + let mut parsed_image_service_url = parse_url(image_service_url)?; + parsed_image_service_url.set_query(Some(&query)); + + Ok(parsed_image_service_url) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_is_vector_image() { + let svg_url = "https://svgshare.com/i/U7z.svg"; + let pdf_url = "https://example.com/image.pdf"; + let svg_data_url = "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg%3E"; + let pdf_data_url = "data:image/pdf,dummydata"; + + assert!(is_vector_image(svg_url, VectorImageType::Svg)); + assert!(is_vector_image(pdf_url, VectorImageType::Pdf)); + assert!(is_vector_image(svg_data_url, VectorImageType::Svg)); + assert!(is_vector_image(pdf_data_url, VectorImageType::Pdf)); + } + + #[test] + fn test_is_vector_image_invalid_url() { + let url = "invalid"; + + assert_eq!(is_vector_image(url, VectorImageType::sample()), false); + } + + #[test] + fn test_is_vector_image_with_image_origin_url() { + let url = "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg&imageSize=1024x1024&format=png"; + + assert!(is_vector_image(url, VectorImageType::Svg)); + } + + #[test] + fn test_is_vector_image_with_image_origin_data_url() { + let data_url = "https://image-service-dev.extratools.works/?imageOrigin=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%2529%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%25200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%25221000%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L500%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%253E&imageSize=1024x1024&format=png"; + + assert!(is_vector_image(data_url, VectorImageType::Svg)); + } + + #[test] + fn test_make_image_url_svg_url() { + let image_service_url = "https://image-service-dev.extratools.works/"; + let image_origin_url = "https://svgshare.com/i/U7z.svg"; + + pretty_assertions::assert_eq!( + make_image_url(image_origin_url, image_service_url, 1024, 1024).unwrap().to_string(), + "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg&imageSize=1024x1024&format=png".to_string() + ); + } + + #[test] + fn test_make_image_url_svg_data_url() { + let image_service_url = "https://image-service-dev.extratools.works/"; + let image_origin_data_url = "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg%3E"; + + pretty_assertions::assert_eq!( + make_image_url(image_origin_data_url, image_service_url, 1024, 1024).unwrap().to_string(), + "https://image-service-dev.extratools.works/?imageOrigin=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%2529%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%25200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%25221000%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L500%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%253E&imageSize=1024x1024&format=png".to_string() + ); + } + + #[test] + fn test_make_image_url_not_svg() { + let image_service_url = "https://image-service-dev.extratools.works/"; + let image_origin_url = "https://sgo4bmuvgu4t24bvdcfbndmnxigspezdsnzoevon2jb5odru7auq.arweave.net/kZ3AspU1OT1wNRiKFo2Nug0nkyOTcuJVzdJD1w40-Ck"; + + pretty_assertions::assert_eq!( + make_image_url(image_origin_url, image_service_url, 1024, 1024).unwrap().to_string(), + "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsgo4bmuvgu4t24bvdcfbndmnxigspezdsnzoevon2jb5odru7auq.arweave.net%2FkZ3AspU1OT1wNRiKFo2Nug0nkyOTcuJVzdJD1w40-Ck&imageSize=1024x1024".to_string() + ); + } +} diff --git a/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs b/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs new file mode 100644 index 000000000..722938cd1 --- /dev/null +++ b/crates/sargon/src/core/utils/image_url_utils_uniffi_fn.rs @@ -0,0 +1,55 @@ +use crate::prelude::*; +use crate::types::*; + +#[uniffi::export] +pub fn image_url_utils_is_vector_image( + url: &str, + image_type: VectorImageType, +) -> bool { + is_vector_image(url, image_type) +} + +#[uniffi::export] +pub fn image_url_utils_make_image_url( + url: &str, + image_service_url: &str, + width: u32, + height: u32, +) -> Result { + make_image_url(url, image_service_url, width, height) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_image_url_utils_is_vector_image() { + let url = "https://svgshare.com/i/U7z.svg"; + let image_type = VectorImageType::Svg; + + assert_eq!( + is_vector_image(url, image_type), + image_url_utils_is_vector_image(&url, image_type) + ) + } + + #[test] + fn test_image_url_utils_make_image_url() { + let url = "https://svgshare.com/i/U7z.svg"; + let image_service_url = "https://image-service-dev.extratools.works"; + let width = 1024; + let height = 1024; + + assert_eq!( + make_image_url(url, image_service_url, width, height), + image_url_utils_make_image_url( + url, + image_service_url, + width, + height + ) + ) + } +} diff --git a/crates/sargon/src/core/utils/mod.rs b/crates/sargon/src/core/utils/mod.rs index 10c882b02..847fafc44 100644 --- a/crates/sargon/src/core/utils/mod.rs +++ b/crates/sargon/src/core/utils/mod.rs @@ -1,9 +1,13 @@ mod factory; +mod image_url_utils; +mod image_url_utils_uniffi_fn; mod logged_panic; mod serialization; mod string_utils; pub use factory::*; +pub use image_url_utils::*; +pub use image_url_utils_uniffi_fn::*; pub use logged_panic::*; pub use serialization::*; pub use string_utils::*; diff --git a/crates/sargon/src/core/utils/string_utils.rs b/crates/sargon/src/core/utils/string_utils.rs index 6e84a6db3..0c65dcf1b 100644 --- a/crates/sargon/src/core/utils/string_utils.rs +++ b/crates/sargon/src/core/utils/string_utils.rs @@ -1,4 +1,5 @@ use crate::CommonError; +use url::form_urlencoded; use url::Url; /// Returns the last `n` chars of the &str `s`. If `s` is shorter than `n` @@ -42,6 +43,10 @@ pub fn parse_url(s: impl AsRef) -> Result { }) } +pub fn url_encode(s: impl AsRef) -> String { + form_urlencoded::byte_serialize(s.as_ref().as_bytes()).collect() +} + #[cfg(test)] mod tests { use super::*; @@ -104,4 +109,19 @@ mod tests { fn test_parse_url_invalid() { assert!(parse_url("https/radixdlt").is_err()); } + + #[test] + fn test_url_encode() { + let url = "https://svgshare.com/i/U7z.svg"; + let data_url = "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20transform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg%3E"; + + pretty_assertions::assert_eq!( + url_encode(url), + "https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg" + ); + pretty_assertions::assert_eq!( + url_encode(data_url), + "data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520xmlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%2529%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%25200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%25221000%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%253Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L500%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%253E" + ); + } } diff --git a/crates/sargon/src/types/mod.rs b/crates/sargon/src/types/mod.rs index c136ba1f5..d6c267adc 100644 --- a/crates/sargon/src/types/mod.rs +++ b/crates/sargon/src/types/mod.rs @@ -1,3 +1,7 @@ mod ffi_url; +mod vector_image_type; +mod vector_image_type_uniffi_fn; pub use ffi_url::*; +pub use vector_image_type::*; +pub use vector_image_type_uniffi_fn::*; diff --git a/crates/sargon/src/types/vector_image_type.rs b/crates/sargon/src/types/vector_image_type.rs new file mode 100644 index 000000000..0e8f5688b --- /dev/null +++ b/crates/sargon/src/types/vector_image_type.rs @@ -0,0 +1,64 @@ +use crate::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)] +pub enum VectorImageType { + Svg, + Pdf, +} + +impl VectorImageType { + pub fn url_extension(&self) -> &str { + match self { + VectorImageType::Svg => ".svg", + VectorImageType::Pdf => ".pdf", + } + } + + pub fn data_url_type(&self) -> &str { + match self { + VectorImageType::Svg => "svg+xml", + VectorImageType::Pdf => "pdf", + } + } +} + +impl HasSampleValues for VectorImageType { + fn sample() -> Self { + Self::Svg + } + + fn sample_other() -> Self { + Self::Pdf + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = VectorImageType; + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn test_url_extension() { + assert_eq!(SUT::sample().url_extension(), ".svg"); + assert_eq!(SUT::sample_other().url_extension(), ".pdf"); + } + + #[test] + fn test_data_url_type() { + assert_eq!(SUT::sample().data_url_type(), "svg+xml"); + assert_eq!(SUT::sample_other().data_url_type(), "pdf"); + } +} diff --git a/crates/sargon/src/types/vector_image_type_uniffi_fn.rs b/crates/sargon/src/types/vector_image_type_uniffi_fn.rs new file mode 100644 index 000000000..796314517 --- /dev/null +++ b/crates/sargon/src/types/vector_image_type_uniffi_fn.rs @@ -0,0 +1,49 @@ +use crate::prelude::*; +use crate::types::*; + +#[uniffi::export] +pub fn vector_image_type_url_extension(image_type: VectorImageType) -> String { + image_type.url_extension().to_string() +} + +#[uniffi::export] +pub fn vector_image_type_data_url_type(image_type: VectorImageType) -> String { + image_type.data_url_type().to_string() +} + +#[uniffi::export] +pub fn new_vector_image_type_sample() -> VectorImageType { + VectorImageType::sample() +} + +#[uniffi::export] +pub fn new_vector_image_type_sample_other() -> VectorImageType { + VectorImageType::sample_other() +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = VectorImageType; + + #[test] + fn equality_samples() { + assert_eq!(SUT::sample(), new_vector_image_type_sample()); + assert_eq!(SUT::sample_other(), new_vector_image_type_sample_other()); + } + + #[test] + fn test_vector_image_type_url_extension() { + let sut = SUT::sample(); + assert_eq!(sut.url_extension(), vector_image_type_url_extension(sut)); + } + + #[test] + fn test_vector_image_type_data_url_type() { + let sut = SUT::sample(); + assert_eq!(sut.data_url_type(), vector_image_type_data_url_type(sut)); + } +} diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/Url.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/Url.kt new file mode 100644 index 000000000..9c1525956 --- /dev/null +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/Url.kt @@ -0,0 +1,30 @@ +package com.radixdlt.sargon.extensions + +import android.net.Uri +import android.util.Size +import com.radixdlt.sargon.CommonException +import com.radixdlt.sargon.Url +import com.radixdlt.sargon.VectorImageType +import com.radixdlt.sargon.imageUrlUtilsIsVectorImage +import com.radixdlt.sargon.imageUrlUtilsMakeImageUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +fun String.toUrl() = toHttpUrl() +fun String.toUrlOrNull() = toHttpUrlOrNull() + +fun Uri.isVectorImage(imageType: VectorImageType): Boolean = imageUrlUtilsIsVectorImage( + url = toString(), + imageType = imageType +) + +@Throws(CommonException::class) +fun Uri.intoImageUrl( + imageServiceUrl: Url, + size: Size +): Url = imageUrlUtilsMakeImageUrl( + url = toString(), + imageServiceUrl = imageServiceUrl.toString(), + width = size.width.toUInt(), + height = size.height.toUInt() +) \ No newline at end of file diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/VectorImageType.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/VectorImageType.kt new file mode 100644 index 000000000..21f0292c4 --- /dev/null +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/extensions/VectorImageType.kt @@ -0,0 +1,11 @@ +package com.radixdlt.sargon.extensions + +import com.radixdlt.sargon.VectorImageType +import com.radixdlt.sargon.vectorImageTypeDataUrlType +import com.radixdlt.sargon.vectorImageTypeUrlExtension + +val VectorImageType.urlExtension: String + get() = vectorImageTypeUrlExtension(imageType = this) + +val VectorImageType.dataUrlType: String + get() = vectorImageTypeDataUrlType(imageType = this) diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/samples/VectorImageTypeSample.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/samples/VectorImageTypeSample.kt new file mode 100644 index 000000000..fcf9bfd14 --- /dev/null +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/samples/VectorImageTypeSample.kt @@ -0,0 +1,14 @@ +package com.radixdlt.sargon.samples + +import com.radixdlt.sargon.VectorImageType +import com.radixdlt.sargon.annotation.UsesSampleValues +import com.radixdlt.sargon.newVectorImageTypeSample +import com.radixdlt.sargon.newVectorImageTypeSampleOther + +@UsesSampleValues +val VectorImageType.Companion.sample: Sample + get() = object : Sample { + override fun invoke(): VectorImageType = newVectorImageTypeSample() + + override fun other(): VectorImageType = newVectorImageTypeSampleOther() + } \ No newline at end of file diff --git a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/UrlTest.kt b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/UrlTest.kt new file mode 100644 index 000000000..51a5c0f5f --- /dev/null +++ b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/UrlTest.kt @@ -0,0 +1,117 @@ +package com.radixdlt.sargon + +import android.net.Uri +import android.util.Size +import androidx.core.net.toUri +import com.radixdlt.sargon.extensions.intoImageUrl +import com.radixdlt.sargon.extensions.isVectorImage +import com.radixdlt.sargon.extensions.toUrl +import com.radixdlt.sargon.extensions.toUrlOrNull +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.slot +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.net.URI + +class UrlTest { + + @Test + fun testToUrl() { + assertEquals( + "https://svgshare.com/i/U7z.svg", + "https://svgshare.com/i/U7z.svg".toUrl().toString(), + ) + } + + @Test + fun testToUrlOrNull() { + assertEquals( + "https://svgshare.com/i/U7z.svg", + "https://svgshare.com/i/U7z.svg".toUrlOrNull().toString(), + ) + + assertNull( + "data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20".toUrlOrNull(), + ) + } + + @Test + fun testVectorImage() { + val svgUrl = mockUri("https://svgshare.com/i/U7z.svg") + + assertTrue(svgUrl.isVectorImage(imageType = VectorImageType.SVG)) + } + + @Test + fun testImageUrl() { + val size = mockSize(width = 1024, height = 1024) + val imageServiceURL = "https://image-service-dev.extratools.works".toUrl() + val svgURL = mockUri("https://svgshare.com/i/U7z.svg") + + assertEquals( + "https://image-service-dev.extratools.works/?imageOrigin=https%3A%2F%2Fsvgshare.com%2Fi%2FU7z.svg&imageSize=1024x1024&format=png", + svgURL.intoImageUrl( + imageServiceUrl = imageServiceURL, + size = size + ).toString() + ) + } + + @Test + fun testImageUrlWithDataUrl() { + val size = mockSize(width = 1024, height = 1024) + val imageServiceURL = "https://image-service-dev.extratools.works".toUrl() + val svgDataUrl = mockUri("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%201000%201000%22%20" + + "xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpolygon%20fill%3" + + "D%22hsla%2890%2C99%25%2C52%25%2C1%29%22%20points%3D%220%2C%200%2C%201000%2C" + + "%201000%2C%200%2C%201000%22%20transform%3D%22scale%28-1%2C1%29%20translate%" + + "28-1000%29%22%2F%3E%0A%3Cpolygon%20fill%3D%22hsla%28199%2C90%25%2C64%25%2C1" + + "%29%22%20points%3D%221000%2C%201000%2C%201000%2C%200%2C%200%2C%200%22%20tra" + + "nsform%3D%22scale%28-1%2C1%29%20translate%28-1000%29%22%2F%3E%0A%3Cpath%20d" + + "%3D%22M1000%2C229%20A1000%2C1000%2C0%2C0%2C0%2C229%2C1000%20L1000%2C1000%20" + + "z%22%20fill%3D%22hsla%28140%2C98%25%2C61%25%2C1%29%22%2F%3E%0A%3Cpath%20d%3" + + "D%22M392%2C500%20L608%2C500%20M500%2C392%20L500%2C608%22%20stroke%3D%22hsla" + + "%2847%2C92%25%2C61%25%2C1%29%22%20stroke-width%3D%2272%22%2F%3E%0A%3C%2Fsvg" + + "%3E") + + assertEquals( + "https://image-service-dev.extratools.works/?imageOrigin=data%3Aimage%2Fsvg" + + "%2Bxml%2C%253Csvg%2520viewBox%253D%25220%25200%25201000%25201000%2522%2520x" + + "mlns%253D%2522http%253A%252F%252Fwww.w3.org%252F2000%252Fsvg%2522%253E%250A" + + "%253Cpolygon%2520fill%253D%2522hsla%252890%252C99%2525%252C52%2525%252C1%25" + + "29%2522%2520points%253D%25220%252C%25200%252C%25201000%252C%25201000%252C%2" + + "5200%252C%25201000%2522%2520transform%253D%2522scale%2528-1%252C1%2529%2520" + + "translate%2528-1000%2529%2522%252F%253E%250A%253Cpolygon%2520fill%253D%2522" + + "hsla%2528199%252C90%2525%252C64%2525%252C1%2529%2522%2520points%253D%252210" + + "00%252C%25201000%252C%25201000%252C%25200%252C%25200%252C%25200%2522%2520tr" + + "ansform%253D%2522scale%2528-1%252C1%2529%2520translate%2528-1000%2529%2522%" + + "252F%253E%250A%253Cpath%2520d%253D%2522M1000%252C229%2520A1000%252C1000%252" + + "C0%252C0%252C0%252C229%252C1000%2520L1000%252C1000%2520z%2522%2520fill%253D" + + "%2522hsla%2528140%252C98%2525%252C61%2525%252C1%2529%2522%252F%253E%250A%25" + + "3Cpath%2520d%253D%2522M392%252C500%2520L608%252C500%2520M500%252C392%2520L5" + + "00%252C608%2522%2520stroke%253D%2522hsla%252847%252C92%2525%252C61%2525%252" + + "C1%2529%2522%2520stroke-width%253D%252272%2522%252F%253E%250A%253C%252Fsvg%" + + "253E&imageSize=1024x1024&format=png", + svgDataUrl.intoImageUrl(imageServiceUrl = imageServiceURL, size = size).toString() + ) + } + + private fun mockUri(urlString: String): Uri { + val uri = mockk() + every { uri.toString() } returns urlString + return uri + } + + private fun mockSize(width: Int, height: Int): Size { + val size = mockk() + every { size.width } returns width + every { size.height } returns height + return size + } +} \ No newline at end of file diff --git a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/VectorImageTypeTest.kt b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/VectorImageTypeTest.kt new file mode 100644 index 000000000..3790b9de5 --- /dev/null +++ b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/VectorImageTypeTest.kt @@ -0,0 +1,25 @@ +package com.radixdlt.sargon + +import com.radixdlt.sargon.extensions.dataUrlType +import com.radixdlt.sargon.extensions.urlExtension +import com.radixdlt.sargon.samples.Sample +import com.radixdlt.sargon.samples.sample +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class VectorImageTypeTest: SampleTestable { + override val samples: List> + get() = listOf(VectorImageType.sample) + + @Test + fun testExtension() { + assertEquals(".svg", VectorImageType.sample().urlExtension) + assertEquals(".pdf", VectorImageType.sample.other().urlExtension) + } + + @Test + fun testDataUrlType() { + assertEquals("svg+xml", VectorImageType.sample().dataUrlType) + assertEquals("pdf", VectorImageType.sample.other().dataUrlType) + } +} \ No newline at end of file From 9ac67686d851b71d2d7e869765fcd3a8d3a4f51d Mon Sep 17 00:00:00 2001 From: micbakos-rdx <125959264+micbakos-rdx@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:27:35 +0300 Subject: [PATCH 2/6] Add JNA dependency to sargon-desktop-bins (#223) Add dependency to pom --- jvm/gradle/libs.versions.toml | 4 ++++ jvm/sargon-android/build.gradle.kts | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/jvm/gradle/libs.versions.toml b/jvm/gradle/libs.versions.toml index e4accbfa1..d130509ac 100644 --- a/jvm/gradle/libs.versions.toml +++ b/jvm/gradle/libs.versions.toml @@ -22,6 +22,8 @@ okhttp = "5.0.0-alpha.14" turbine = "1.1.0" hilt = "2.51.1" ksp = "1.9.22-1.0.17" +jna = "5.13.0" +serialization-json = "1.6.3" viewmodel = "2.7.0" androidx-test = "1.5.0" androidx-test-junit = "1.1.5" @@ -44,12 +46,14 @@ androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtim androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "viewmodel" } material = { module = "com.google.android.material:material", version.ref = "material" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization-json" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version.ref = "okhttp" } okhttp-mock-web-server = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } +jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } junit = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } diff --git a/jvm/sargon-android/build.gradle.kts b/jvm/sargon-android/build.gradle.kts index 5d0e17b05..7105ee300 100644 --- a/jvm/sargon-android/build.gradle.kts +++ b/jvm/sargon-android/build.gradle.kts @@ -124,9 +124,7 @@ koverReport { } dependencies { - // Cannot use version catalogues for aar. For some reason when published to Maven, - // the jna dependency cannot be resolved - implementation("net.java.dev.jna:jna:5.13.0@aar") + implementation("${libs.jna.get()}@aar") // For lifecycle callbacks implementation(libs.androidx.appcompat) @@ -138,7 +136,7 @@ dependencies { implementation(libs.coroutines.android) // For Serialization extensions - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation(libs.kotlinx.serialization.json) // For Network support implementation(libs.okhttp) @@ -151,7 +149,7 @@ dependencies { implementation(libs.timber) // Unit tests - testImplementation("net.java.dev.jna:jna:5.13.0") + testImplementation(libs.jna) testImplementation(libs.junit) testImplementation(libs.junit.params) testImplementation(libs.mockk) @@ -192,6 +190,18 @@ publishing { afterEvaluate { artifact(tasks.getByName("desktopJar")) } + + pom { + withXml { + val dependencies = asNode().appendNode("dependencies") + + val jni = dependencies.appendNode("dependency") + jni.appendNode("groupId", "net.java.dev.jna") + jni.appendNode("artifactId", "jna") + jni.appendNode("version", libs.versions.jna.get()) + jni.appendNode("scope", "runtime") + } + } } } From fcdc0a8097f2fb18f6e271662d7bc64cca9f64eb Mon Sep 17 00:00:00 2001 From: micbakos-rdx <125959264+micbakos-rdx@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:50:04 +0300 Subject: [PATCH 3/6] Android Local Development (#226) * Provide ability to publish package to maven local for local development * Update readme * Add suffix explanation --- .github/workflows/release-android.yml | 2 +- .github/workflows/release-desktop.yml | 5 +- .github/workflows/test.yml | 3 - README.md | 34 +++++++ .../com/radixdlt/cargo/toml/SargonVersion.kt | 6 +- jvm/sargon-android/build.gradle.kts | 98 ++++++++++++------- 6 files changed, 104 insertions(+), 44 deletions(-) diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml index 794564498..49f075a01 100644 --- a/.github/workflows/release-android.yml +++ b/.github/workflows/release-android.yml @@ -62,7 +62,7 @@ jobs: - name: Build and publish Android uses: RDXWorks-actions/gradle-build-action@main with: - arguments: sargon-android:publishAndroidPublicationToGitHubPackagesRepository + arguments: sargon-android:publishAndroidReleasePublicationToGitHubPackagesRepository build-root-directory: jvm env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index d52c25082..4ad0f69d8 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -46,9 +46,6 @@ jobs: target: ${{ matrix.build-target.toolchain }} default: 'true' - - name: Rustc version - run: cargo --version --verbose - - name: Set up JDK 17 uses: RDXWorks-actions/setup-java@v3 with: @@ -92,7 +89,7 @@ jobs: - name: Publish desktop binaries uses: RDXWorks-actions/gradle-build-action@main with: - arguments: sargon-android:publishDesktopPublicationToGitHubPackagesRepository + arguments: sargon-android:publishDesktopReleasePublicationToGitHubPackagesRepository build-root-directory: jvm env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe4ff0f1d..ffeb701bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -165,9 +165,6 @@ jobs: target: x86_64-unknown-linux-gnu default: 'true' - - name: Rustc version - run: cargo --version --verbose - - name: Set up JDK 17 uses: RDXWorks-actions/setup-java@v3 with: diff --git a/README.md b/README.md index 7e38e1d89..bb411ab2e 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,40 @@ See [`.github/workflows/release.yml`](.github/workflows/release.yml) ## Android +### Locally +In order to build sargon for local development we will leverage the local maven repository. Instead of publishing the package in a maven server, we can publish it locally. + +In order to publish both android and desktop binaries with a simple command run +```sh +cd jvm/ +./gradlew sargon-android:buildForLocalDev // This builds both sargon-android and sargon-desktop-bins +``` +This will produce the following message when successfully finished +```txt +> Task :sargon-android:buildForLocalDev +✅ Library is published in maven local with version: +1.1.19-c74d9cbf-SNAPSHOT +``` +Note that such local maven builds are in debug mode and have a `-SNAPSHOT` suffix. + +Copy the version name to your project but make sure that `mavenLocal()` is included in your project's `settings.gradle` +```gradle +dependencyResolutionManagement { + ... + repositories { + mavenLocal() + ... + } +} +``` +> [!TIP] +> The libraries that are published in local maven will reside in: +> ``` +> $HOME/.m2/repository/com/radixdlt/sargon +> ``` + +### CD + Two modules are published in [Github's maven](https://github.com/radixdlt/sargon/packages/). - `sargon-android` diff --git a/jvm/buildSrc/src/main/java/com/radixdlt/cargo/toml/SargonVersion.kt b/jvm/buildSrc/src/main/java/com/radixdlt/cargo/toml/SargonVersion.kt index 25bc2ed00..17b27639a 100644 --- a/jvm/buildSrc/src/main/java/com/radixdlt/cargo/toml/SargonVersion.kt +++ b/jvm/buildSrc/src/main/java/com/radixdlt/cargo/toml/SargonVersion.kt @@ -22,12 +22,14 @@ private fun Project.parseGitHash(): String { return String(out.toByteArray(), Charsets.UTF_8).trim() } -fun Project.sargonVersion(): String { +fun Project.sargonVersion(isDebug: Boolean): String { val customBuildName = System.getenv("CUSTOM_BUILD_NAME")?.takeIf { it.isNotBlank() }?.replace("\\s+".toRegex(), "-")?.let { "-${it}" }.orEmpty() - return "${parseTomlVersion()}${customBuildName}-${parseGitHash()}" + val snapshot = if (isDebug) "-SNAPSHOT" else "" + + return "${parseTomlVersion()}${customBuildName}-${parseGitHash()}$snapshot" } \ No newline at end of file diff --git a/jvm/sargon-android/build.gradle.kts b/jvm/sargon-android/build.gradle.kts index 7105ee300..898b7243e 100644 --- a/jvm/sargon-android/build.gradle.kts +++ b/jvm/sargon-android/build.gradle.kts @@ -1,6 +1,8 @@ import com.radixdlt.cargo.desktop.DesktopTargetTriple import com.radixdlt.cargo.desktop.currentTargetTriple import com.radixdlt.cargo.toml.sargonVersion +import org.gradle.configurationcache.extensions.capitalized +import org.gradle.internal.logging.text.StyledTextOutput import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.nio.file.Files @@ -70,14 +72,24 @@ android { ) } - // This task is used when publishing `sargon-desktop-bins`. - // Before generating a Jar we need all native libs to have been built for all desktop - // architectures. - // The building is handled by github. After that the `copyExternalArtifacts` needs to copy - // all the built libraries into the resources directory. - tasks.register("desktopJar") { - from("${buildDir}/generated/src/resources") - dependsOn("copyExternalArtifacts") + buildTypes.forEach { + val buildTypeVariant = it.name.capitalized() + tasks.register("desktopJar${buildTypeVariant}") { + from("${buildDir}/generated/src/resources") + + if (it.isDebuggable) { + // For debug we only need to build for the current architecture. Used by maven publication in + // debug mode. + dependsOn("buildCargoDesktopDebug") + } else { + // This task is used when publishing `sargon-desktop-bins`. + // Before generating a Jar we need all native libs to have been built for all desktop + // architectures. + // The building is handled by github. After that the `copyExternalArtifacts` needs to copy + // all the built libraries into the resources directory. + dependsOn("copyExternalArtifacts") + } + } } } @@ -169,37 +181,41 @@ dependencies { publishing { publications { - // Publishing the android library we just need to build the library from the release component - register("android") { - groupId = "com.radixdlt.sargon" - artifactId = "sargon-android" - version = project.sargonVersion() - - afterEvaluate { - from(components["release"]) - } - } + android.buildTypes.forEach { + val buildTypeVariant = it.name.capitalized() - // Publishing the desktop bins we need to run the `desktopJar` task. For more info check - // the comments of that task. - register("desktop") { - groupId = "com.radixdlt.sargon" - artifactId = "sargon-desktop-bins" - version = project.sargonVersion() + // Publishing the android library we just need to build the library from the release component + register("android$buildTypeVariant") { + groupId = "com.radixdlt.sargon" + artifactId = "sargon-android" + version = project.sargonVersion(it.isDebuggable) - afterEvaluate { - artifact(tasks.getByName("desktopJar")) + afterEvaluate { + from(components[it.name]) + } } - pom { - withXml { - val dependencies = asNode().appendNode("dependencies") + // Publishing the desktop bins we need to run the `desktopJar` task. For more info check + // the comments of that task. + register("desktop$buildTypeVariant") { + groupId = "com.radixdlt.sargon" + artifactId = "sargon-desktop-bins" + version = project.sargonVersion(it.isDebuggable) - val jni = dependencies.appendNode("dependency") - jni.appendNode("groupId", "net.java.dev.jna") - jni.appendNode("artifactId", "jna") - jni.appendNode("version", libs.versions.jna.get()) - jni.appendNode("scope", "runtime") + afterEvaluate { + artifact(tasks.getByName("desktopJar$buildTypeVariant")) + } + + pom { + withXml { + val dependencies = asNode().appendNode("dependencies") + + val jni = dependencies.appendNode("dependency") + jni.appendNode("groupId", "net.java.dev.jna") + jni.appendNode("artifactId", "jna") + jni.appendNode("version", libs.versions.jna.get()) + jni.appendNode("scope", "runtime") + } } } } @@ -294,6 +310,20 @@ afterEvaluate { tasks.getByName("testDebugUnitTest").dependsOn("buildCargoDesktopDebug") tasks.getByName("testReleaseUnitTest").dependsOn("buildCargoDesktopRelease") + + tasks.register("buildForLocalDev") { + group = "publishing" + + doLast { + println("✅ Library is published in maven local with version:") + println(project.sargonVersion(true)) + } + + dependsOn( + "publishAndroidDebugPublicationToMavenLocal", + "publishDesktopDebugPublicationToMavenLocal" + ) + } } // Task that copies externally built artifacts into resources directory From 95dc0c89cafae2d5561a24e3b8a06118ff71125e Mon Sep 17 00:00:00 2001 From: Alexander Cyon <116169792+CyonAlexRDX@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:56:38 +0200 Subject: [PATCH 4/6] Fixed Sargon iOS example app. (Xcode 16) (#224) * Fixed Sargon iOS example app. * fix tests * Revert "fix tests" This reverts commit 79ce9798de254fafdf9c0155cec3d514deb92484. --- Cargo.toml | 9 +- crates/sargon/Cargo.toml | 9 +- .../support/test/fail_unsafe_storage.rs | 28 -- .../unsafe_storage_driver/support/test/mod.rs | 6 - .../Planbok/Dependencies/ProfileClient.swift | 7 +- .../Sources/Planbok/Features/AppFeature.swift | 49 +++- .../Children/SetFactorThresholdFeature.swift | 2 +- .../Shields/New/Models/NewShield+Models.swift | 6 +- .../New/Models/NewShield+SharedState.swift | 8 +- .../CreateAccountFlowFeature.swift | 2 +- .../NewOrImportProfileFeature.swift | 16 +- .../Flows/Onboarding/OnboardingFeature.swift | 2 +- .../Planbok/Features/MainFeature.swift | 176 ++++++------ .../Planbok/Features/SplashFeature.swift | 3 +- .../SargonExtensions/Sargon+Extensions.swift | 2 +- .../Planbok/SharedState/SargonKey.swift | 255 +++++++++--------- .../SharedState+FactorSources.swift | 4 +- .../SharedState/SharedState+NetworkID.swift | 2 +- .../SharedState+SavedGateways.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- 20 files changed, 314 insertions(+), 278 deletions(-) delete mode 100644 crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/fail_unsafe_storage.rs diff --git a/Cargo.toml b/Cargo.toml index 0c6a7fc20..45c2e63d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,9 @@ [workspace] resolver = "2" -members = [ - "crates/sargon", -] +members = ["crates/sargon"] default-members = ["crates/sargon"] + +[profile.release] +incremental = false +panic = 'unwind' +codegen-units = 1 diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index d849618a2..38aaa0044 100644 --- a/crates/sargon/Cargo.toml +++ b/crates/sargon/Cargo.toml @@ -4,11 +4,6 @@ version = "1.1.19" edition = "2021" build = "build.rs" -[profile.release] -incremental = false -panic = 'unwind' -codegen-units = 1 - [[test]] name = "vectors" @@ -75,7 +70,9 @@ strum = { git = "https://github.com/Peternator7/strum/", rev = "f746c3699acf1501 ] } sbor = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "6ec9c337246b5d4cf3c142bd9af80e9e8bd5fbae" } -radix-rust = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "6ec9c337246b5d4cf3c142bd9af80e9e8bd5fbae", features = ["serde"] } +radix-rust = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "6ec9c337246b5d4cf3c142bd9af80e9e8bd5fbae", features = [ + "serde", +] } radix-engine = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "6ec9c337246b5d4cf3c142bd9af80e9e8bd5fbae" } radix-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "6ec9c337246b5d4cf3c142bd9af80e9e8bd5fbae", features = [ "serde", diff --git a/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/fail_unsafe_storage.rs b/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/fail_unsafe_storage.rs deleted file mode 100644 index 3ec74b534..000000000 --- a/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/fail_unsafe_storage.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![cfg(test)] - -use crate::prelude::*; - -#[derive(Debug)] -pub(crate) struct AlwaysFailUnsafeStorage {} - -#[async_trait::async_trait] -impl UnsafeStorageDriver for AlwaysFailUnsafeStorage { - async fn load_data( - &self, - _key: UnsafeStorageKey, - ) -> Result> { - panic!("AlwaysFailStorage does not implement `load_data"); - } - - async fn save_data( - &self, - _key: UnsafeStorageKey, - _data: BagOfBytes, - ) -> Result<()> { - Err(CommonError::Unknown) - } - - async fn delete_data_for_key(&self, _key: UnsafeStorageKey) -> Result<()> { - panic!("AlwaysFailStorage does not implement `delete_data_for_key"); - } -} diff --git a/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/mod.rs b/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/mod.rs index 0b9cdd32e..fd18ffa85 100644 --- a/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/mod.rs +++ b/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/mod.rs @@ -3,9 +3,3 @@ mod ephemeral_unsafe_storage; #[cfg(test)] pub use ephemeral_unsafe_storage::*; - -#[cfg(test)] -mod fail_unsafe_storage; - -#[cfg(test)] -pub use fail_unsafe_storage::*; diff --git a/examples/iOS/Backend/Sources/Planbok/Dependencies/ProfileClient.swift b/examples/iOS/Backend/Sources/Planbok/Dependencies/ProfileClient.swift index 40b118473..9971e52c3 100644 --- a/examples/iOS/Backend/Sources/Planbok/Dependencies/ProfileClient.swift +++ b/examples/iOS/Backend/Sources/Planbok/Dependencies/ProfileClient.swift @@ -30,10 +30,10 @@ extension ProfileClient: DependencyKey { public static func live(os: SargonOS) -> Self { return Self( activeProfile: { - os.profile() + try! os.profile() }, deleteProfileAndMnemonicsThenCreateNew: { - let _ = try await os.deleteProfileThenCreateNewWithBdfs() + try await os.deleteWallet() }, importProfile: { try await os.importProfile(profile: $0) @@ -42,7 +42,8 @@ extension ProfileClient: DependencyKey { try Profile(encrypted: $0, decryptionPassword: $1) }, emulateFreshInstallOfAppThenRestart: { - try await os.emulateFreshInstall() + log.warning("TODO Migrate `emulateFreshInstallOfAppThenRestart`, not in Sargon anymore.") + try await os.deleteWallet() } ) } diff --git a/examples/iOS/Backend/Sources/Planbok/Features/AppFeature.swift b/examples/iOS/Backend/Sources/Planbok/Features/AppFeature.swift index 6aca83890..d295a875e 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/AppFeature.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/AppFeature.swift @@ -1,6 +1,48 @@ import Sargon import ComposableArchitecture +final class ProfileStateChangeDriverClass { + init() {} + static let shared = ProfileStateChangeDriverClass() +} +extension ProfileStateChangeDriverClass: ProfileStateChangeDriver { + func handleProfileStateChange(changedProfileState: ProfileState) async { + log.warning("profileStateChangeDriver not used, ignored event") + switch changedProfileState { + case .incompatible(let error): + fatalError("incompatible profile snapshot format, error: \(error)") + case .loaded(let loadedProfile): + log.notice("Loaded profile - id: \(loadedProfile.header.id)") + case .none: + log.notice("Profle changed to `none`.") + } + } +} + +final class HostInfoDriverClass { + init() {} + static let shared = HostInfoDriverClass() +} +extension HostInfoDriverClass: HostInfoDriver { + func hostOs() async -> HostOs { + HostOs.ios(version: "read") + } + + func hostDeviceName() async -> String { + "iPhone wip" + } + + func hostAppVersion() async -> String { + "0.0.1" + } + + func hostDeviceModel() async -> String { + "iPhone wip" + } + + +} + @Reducer public struct AppFeature { @@ -11,14 +53,11 @@ public struct AppFeature { case main(MainFeature.State) public init(isEmulatingFreshInstall: Bool = false) { - let drivers = Drivers( networking: URLSession.shared, secureStorage: Keychain(service: "rdx.works.planbok"), entropyProvider: EntropyProvider.shared, - hostInfo: HostInfo( - appVersion: "0.0.1" - ), + hostInfo: HostInfoDriverClass.shared, logging: Log.shared, eventBus: EventBus.shared, fileSystem: FileSystem.shared, @@ -26,7 +65,7 @@ public struct AppFeature { userDefaults: .init( suiteName: "rdx.works" )! - ) + ), profileStateChangeDriver: ProfileStateChangeDriverClass.shared ) BIOS.creatingShared(drivers: drivers) diff --git a/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Children/SetFactorThresholdFeature.swift b/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Children/SetFactorThresholdFeature.swift index 4be73d0e6..766750914 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Children/SetFactorThresholdFeature.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Children/SetFactorThresholdFeature.swift @@ -38,7 +38,7 @@ public struct SetFactorThresholdFeature { guard numberOfFactors > 0 else { return options } - let exceeding1 = UInt16(numberOfFactors - 1) + let exceeding1 = UInt8(numberOfFactors - 1) if exceeding1 > 1 { options.append(contentsOf: (1...exceeding1).map(FactorThreshold.threshold)) } diff --git a/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Models/NewShield+Models.swift b/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Models/NewShield+Models.swift index 254ff0f2e..d7fd575f6 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Models/NewShield+Models.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Models/NewShield+Models.swift @@ -24,8 +24,8 @@ public struct Factor: Hashable, Sendable, Identifiable { public typealias Factors = IdentifiedArrayOf public enum FactorThreshold: Hashable, Sendable, CustomStringConvertible { - init(count: UInt16, thresholdFactorsCount: Int) { - let factorCount = UInt16(thresholdFactorsCount) + init(count: UInt8, thresholdFactorsCount: Int) { + let factorCount = UInt8(thresholdFactorsCount) if count == factorCount { self = .all } else if count == 1 { @@ -61,7 +61,7 @@ public enum FactorThreshold: Hashable, Sendable, CustomStringConvertible { case any case all - case threshold(UInt16) + case threshold(UInt8) public var description: String { switch self { diff --git a/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Models/NewShield+SharedState.swift b/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Models/NewShield+SharedState.swift index 94cb13f3c..c04647b66 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Models/NewShield+SharedState.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/FactorSource/Shields/New/Models/NewShield+SharedState.swift @@ -49,7 +49,7 @@ public protocol RoleFromDraft { /** * How many threshold factors that must be used to perform some function with this role. */ - var threshold: UInt16 { get } + var threshold: UInt8 { get } /** * Overriding / Super admin / "sudo" / God / factors, **ANY** * single of these factor which can perform the function of this role, @@ -58,7 +58,7 @@ public protocol RoleFromDraft { var overrideFactors: [FactorSource] { get } static var role: Role { get } - init(thresholdFactors: [FactorSource], threshold: UInt16, overrideFactors: [FactorSource]) + init(thresholdFactors: [FactorSource], threshold: UInt8, overrideFactors: [FactorSource]) init?(draft: MatrixOfFactorsForRole) } extension RoleFromDraft { @@ -76,8 +76,8 @@ extension RoleFromDraft { thresholdFactors: draft.thresholdFactorSources, threshold: { switch draft.threshold { - case .any: UInt16(min(1, draft.thresholdFactorSources.count)) - case .all: UInt16(draft.thresholdFactorSources.count) + case .any: UInt8(min(1, draft.thresholdFactorSources.count)) + case .all: UInt8(draft.thresholdFactorSources.count) case let .threshold(t): t } }(), diff --git a/examples/iOS/Backend/Sources/Planbok/Features/Flows/CreateAccount/CreateAccountFlowFeature.swift b/examples/iOS/Backend/Sources/Planbok/Features/Flows/CreateAccount/CreateAccountFlowFeature.swift index b00d46b47..e2bb04432 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/Flows/CreateAccount/CreateAccountFlowFeature.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/Flows/CreateAccount/CreateAccountFlowFeature.swift @@ -37,7 +37,7 @@ public struct CreateAccountFlowFeature { return .run { send in try await accountsClient.createAndSaveAccount(name) await send(.delegate(.createdAccount)) - } catch: { _, error in + } catch: { error, _ in fatalError("TODO error handling: \(error)") } diff --git a/examples/iOS/Backend/Sources/Planbok/Features/Flows/Onboarding/NewOrImportProfileFeature.swift b/examples/iOS/Backend/Sources/Planbok/Features/Flows/Onboarding/NewOrImportProfileFeature.swift index 8f7d1447a..d869b530a 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/Flows/Onboarding/NewOrImportProfileFeature.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/Flows/Onboarding/NewOrImportProfileFeature.swift @@ -22,7 +22,7 @@ public struct NewOrImportProfileFeature { public enum Action: ViewAction { public enum DelegateAction { - case newProfile + case createdNewEmptyProfile case importProfile } @@ -40,10 +40,16 @@ public struct NewOrImportProfileFeature { switch action { case .view(.importProfileButtonTapped): - .send(.delegate(.importProfile)) - - case .view(.newProfileButtonTapped): - .send(.delegate(.newProfile)) + .send(.delegate(.importProfile)) + + + case .view(.newProfileButtonTapped): + .run { send in + try await SargonOS.shared.newWallet() + await send(.delegate(.createdNewEmptyProfile)) + } catch: { error, _ in + fatalError("Failed to create Profile, error: \(error)") + } case .delegate: .none diff --git a/examples/iOS/Backend/Sources/Planbok/Features/Flows/Onboarding/OnboardingFeature.swift b/examples/iOS/Backend/Sources/Planbok/Features/Flows/Onboarding/OnboardingFeature.swift index 552a7f6c0..ecb74bfc6 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/Flows/Onboarding/OnboardingFeature.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/Flows/Onboarding/OnboardingFeature.swift @@ -61,7 +61,7 @@ public struct OnboardingFeature { state.destination = .importProfile(.init()) return .none - case .element(id: _, action: .newOrImportProfile(.delegate(.newProfile))): + case .element(id: _, action: .newOrImportProfile(.delegate(.createdNewEmptyProfile))): state.destination = .createAccount(CreateAccountFlowFeature.State(index: 0)) return .none diff --git a/examples/iOS/Backend/Sources/Planbok/Features/MainFeature.swift b/examples/iOS/Backend/Sources/Planbok/Features/MainFeature.swift index 50b968a58..6267e06b4 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/MainFeature.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/MainFeature.swift @@ -268,88 +268,102 @@ extension MainFeature { onMainnet: store.network == .mainnet ) - NavigationStack(path: $store.scope(state: \.path, action: \.path)) { - VStack { - VStack { - Text("ProfileID:") - Text("\(SargonOS.shared.profile.id)") - } - - AccountsFeature.View( - store: store.scope(state: \.accounts, action: \.accounts) - ) - - Button("Delete Wallet", role: .destructive) { - send(.deleteWalletButtonTapped) - } - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button("Settings") { - send(.settingsButtonTapped) - } - } - } - } destination: { store in - switch store.case { - - case let .settings(store): - SettingsFeature.View(store: store) - - case let .manageSecurityShields(store): - ManageSecurityShieldsFeature.View(store: store) - - case let .manageFactorSources(store): - ManageFactorSourcesFeature.View(store: store) - - case let .manageSpecificFactorSources(store): - ManageSpecificFactorSourcesFeature.View(store: store) - - case let .accountDetails(store): - AccountDetailsFeature.View(store: store) - - case let .shieldDetails(store): - ShieldDetailsFeature.View(store: store) - - case let .profileView(store): - DebugProfileFeature.View(store: store) - } - } - .sheet( - item: $store.scope( - state: \.destination?.createAccount, - action: \.destination.createAccount - ) - ) { store in - CreateAccountFlowFeature.View(store: store) - } - .sheet( - item: $store.scope( - state: \.destination?.newHWFactorSource, - action: \.destination.newHWFactorSource - ) - ) { store in - NewHWFactorSourceFeature.View(store: store) - } - .sheet( - item: $store.scope( - state: \.destination?.newTrustedContact, - action: \.destination.newTrustedContact - ) - ) { store in - NewTrustedContactFactorSourceFeature.View(store: store) - } - .sheet( - item: $store.scope( - state: \.destination?.newSecurityQuestions, - action: \.destination.newSecurityQuestions - ) - ) { store in - NewSecurityQuestionsFeatureCoordinator.View(store: store) - } - .alert($store.scope(state: \.destination?.deleteProfileAlert, action: \.destination.deleteProfileAlert)) - + mainbody } } + + + var mainbody: some SwiftUI.View { + NavigationStack(path: $store.scope(state: \.path, action: \.path)) { + VStack { + if let profile = try? SargonOS.shared.profile() { + VStack { + Text("ProfileID:") + Text("\(profile.id )") + } + } else { + Text("NO PROFILE") + } + + AccountsFeature.View( + store: store.scope(state: \.accounts, action: \.accounts) + ) + + Button("Delete Wallet", role: .destructive) { + send(.deleteWalletButtonTapped) + } + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Settings") { + send(.settingsButtonTapped) + } + } + } + } destination: { store in + destinationView(store: store) + } + .sheet( + item: $store.scope( + state: \.destination?.createAccount, + action: \.destination.createAccount + ) + ) { store in + CreateAccountFlowFeature.View(store: store) + } + .sheet( + item: $store.scope( + state: \.destination?.newHWFactorSource, + action: \.destination.newHWFactorSource + ) + ) { store in + NewHWFactorSourceFeature.View(store: store) + } + .sheet( + item: $store.scope( + state: \.destination?.newTrustedContact, + action: \.destination.newTrustedContact + ) + ) { store in + NewTrustedContactFactorSourceFeature.View(store: store) + } + .sheet( + item: $store.scope( + state: \.destination?.newSecurityQuestions, + action: \.destination.newSecurityQuestions + ) + ) { store in + NewSecurityQuestionsFeatureCoordinator.View(store: store) + } + .alert($store.scope(state: \.destination?.deleteProfileAlert, action: \.destination.deleteProfileAlert)) + + } + + @ViewBuilder + func destinationView(store: StoreOf) -> some SwiftUI.View { + switch store.case { + + case let .settings(store): + SettingsFeature.View(store: store) + + case let .manageSecurityShields(store): + ManageSecurityShieldsFeature.View(store: store) + + case let .manageFactorSources(store): + ManageFactorSourcesFeature.View(store: store) + + case let .manageSpecificFactorSources(store): + ManageSpecificFactorSourcesFeature.View(store: store) + + case let .accountDetails(store): + AccountDetailsFeature.View(store: store) + + case let .shieldDetails(store): + ShieldDetailsFeature.View(store: store) + + case let .profileView(store): + DebugProfileFeature.View(store: store) + } + } } } diff --git a/examples/iOS/Backend/Sources/Planbok/Features/SplashFeature.swift b/examples/iOS/Backend/Sources/Planbok/Features/SplashFeature.swift index 3a7d81931..9e590ed77 100644 --- a/examples/iOS/Backend/Sources/Planbok/Features/SplashFeature.swift +++ b/examples/iOS/Backend/Sources/Planbok/Features/SplashFeature.swift @@ -58,9 +58,10 @@ public struct SplashFeature { bootingWith: BIOS.shared, isEmulatingFreshInstall: isEmulatingFreshInstall ) + let hasAnyAccountOnAnyNetwork = (try? os.hasAnyAccountOnAnyNetwork()) ?? false await send( .delegate(.booted( - hasAnyAccountOnAnyNetwork: os.hasAnyAccountOnAnyNetwork() + hasAnyAccountOnAnyNetwork: hasAnyAccountOnAnyNetwork )) ) } diff --git a/examples/iOS/Backend/Sources/Planbok/SargonExtensions/Sargon+Extensions.swift b/examples/iOS/Backend/Sources/Planbok/SargonExtensions/Sargon+Extensions.swift index 59f3b6354..7c64b46b0 100644 --- a/examples/iOS/Backend/Sources/Planbok/SargonExtensions/Sargon+Extensions.swift +++ b/examples/iOS/Backend/Sources/Planbok/SargonExtensions/Sargon+Extensions.swift @@ -21,7 +21,7 @@ extension NetworkID { extension SargonOS { public var accountsForDisplayOnCurrentNetworkIdentified: AccountsForDisplay { - accountsForDisplayOnCurrentNetwork.asIdentified() + try! accountsForDisplayOnCurrentNetwork.asIdentified() } } diff --git a/examples/iOS/Backend/Sources/Planbok/SharedState/SargonKey.swift b/examples/iOS/Backend/Sources/Planbok/SharedState/SargonKey.swift index 7025da5b8..dbf61ac8b 100644 --- a/examples/iOS/Backend/Sources/Planbok/SharedState/SargonKey.swift +++ b/examples/iOS/Backend/Sources/Planbok/SharedState/SargonKey.swift @@ -27,146 +27,155 @@ extension KeyPath: @unchecked Sendable where Root: Sendable, Value: Sendable {} /// ) /// ``` public struct SargonKey: Hashable, PersistenceReaderKey, Sendable { - public typealias FetchValueFromSargonOS = @Sendable () -> Value? - public typealias ShouldFetch = @Sendable (EventKind) -> Bool - - /// A closure which we invoke if `shouldFetch` returns `true` for a received `EventNotification`, - /// which fetches a new value from SargonOS. The closure has already been translated from a `(SargonOS) -> Value?` - /// closure in the initializer of `SargonKey` into a `() -> Value?`. - private let fetchValueFromSargonOS: FetchValueFromSargonOS - - /// A predicate which returns `true` given an received `EventNotification` of a certain kind - /// (`EventKind`) if we should call `fetchValueFromSargonOS` else `false`, if the - /// event kind is not relevant for the `Value` of this `SargonKey`. E.g. the EventKind `.addedAccount`, - /// does not affect the current network, so a `SargonKey` should return `false` for - /// a received `EventNotification` of kind `.addedAccount`. - /// - /// However, we SHOULD not make our own decisions here on the Swift side if a certain event is - /// relevant or not, better to use the functions `affectsX` which we have wrapped as computed - /// properties on `EventKind`, e.g. `eventKind.affectsCurrentAccounts`. - private let shouldFetch: ShouldFetch - - /// Owned (retained) by SargonOS - private unowned let eventBus: EventBus - - public init( - sargonOS: SargonOS = .shared, - eventBus: EventBus = .shared, - fetchValueFromSargonOS: @escaping @Sendable (SargonOS) -> Value?, - shouldFetch: @escaping ShouldFetch - ) { - self.eventBus = eventBus - self.fetchValueFromSargonOS = { [weak sargonOS] in - guard let sargonOS else { - return nil - } - return fetchValueFromSargonOS(sargonOS) - } - self.shouldFetch = shouldFetch - } + public typealias FetchValueFromSargonOS = @Sendable () -> Value? + public typealias ShouldFetch = @Sendable (EventKind) -> Bool + + /// A closure which we invoke if `shouldFetch` returns `true` for a received `EventNotification`, + /// which fetches a new value from SargonOS. The closure has already been translated from a `(SargonOS) -> Value?` + /// closure in the initializer of `SargonKey` into a `() -> Value?`. + private let fetchValueFromSargonOS: FetchValueFromSargonOS + + /// A predicate which returns `true` given an received `EventNotification` of a certain kind + /// (`EventKind`) if we should call `fetchValueFromSargonOS` else `false`, if the + /// event kind is not relevant for the `Value` of this `SargonKey`. E.g. the EventKind `.addedAccount`, + /// does not affect the current network, so a `SargonKey` should return `false` for + /// a received `EventNotification` of kind `.addedAccount`. + /// + /// However, we SHOULD not make our own decisions here on the Swift side if a certain event is + /// relevant or not, better to use the functions `affectsX` which we have wrapped as computed + /// properties on `EventKind`, e.g. `eventKind.affectsCurrentAccounts`. + private let shouldFetch: ShouldFetch + + /// Owned (retained) by SargonOS + private unowned let eventBus: EventBus + + public init( + sargonOS: SargonOS = .shared, + eventBus: EventBus = .shared, + fetchValueFromSargonOS: @escaping @Sendable (SargonOS) throws -> Value?, + shouldFetch: @escaping ShouldFetch + ) { + self.eventBus = eventBus + self.fetchValueFromSargonOS = { [weak sargonOS] in + guard let sargonOS else { + return nil + } + return try? fetchValueFromSargonOS(sargonOS) + } + self.shouldFetch = shouldFetch + } } extension SargonKey { - - /// Create a new `SargonKey` with `KeyPath` based API, instead of closure based. - /// - /// This allows use to write: - /// - /// ``` - /// SargonKey( - /// accessing: \.accountsForDisplayOnCurrentNetworkIdentified, - /// fetchIf: \.affectsCurrentAccounts - /// ) - /// ``` - /// - /// Instead of more verbose: - /// - /// ``` - /// SargonKey( - /// fetchValueFromSargonOS: { os in os[keyPath: \.accountsForDisplayOnCurrentNetworkIdentified] }, - /// shouldFetch: { eventKind in eventKind[keyPath: \.affectsCurrentAccounts] } - /// ) - /// ``` - public init( - accessing lastValueWithOSKeyPath: KeyPath, - fetchIf fetchIfKeyPath: KeyPath - ) { - self.init( - fetchValueFromSargonOS: { $0[keyPath: lastValueWithOSKeyPath] }, - shouldFetch: { $0[keyPath: fetchIfKeyPath] } - ) - - } + + /// Create a new `SargonKey` with `KeyPath` based API, instead of closure based. + /// + /// This allows use to write: + /// + /// ``` + /// SargonKey( + /// accessing: \.accountsForDisplayOnCurrentNetworkIdentified, + /// fetchIf: \.affectsCurrentAccounts + /// ) + /// ``` + /// + /// Instead of more verbose: + /// + /// ``` + /// SargonKey( + /// fetchValueFromSargonOS: { os in os[keyPath: \.accountsForDisplayOnCurrentNetworkIdentified] }, + /// shouldFetch: { eventKind in eventKind[keyPath: \.affectsCurrentAccounts] } + /// ) + /// ``` + public init( + accessing lastValueWithOSKeyPath: KeyPath, + fetchIf fetchIfKeyPath: KeyPath + ) { + self.init( + fetchValueFromSargonOS: { $0[keyPath: lastValueWithOSKeyPath] }, + shouldFetch: { $0[keyPath: fetchIfKeyPath] } + ) + } + + public init( + mapping fetchValueFromSargonOS: @escaping @Sendable (SargonOS) throws -> Value?, + fetchIf fetchIfKeyPath: KeyPath + ) { + self.init( + fetchValueFromSargonOS: fetchValueFromSargonOS, + shouldFetch: { $0[keyPath: fetchIfKeyPath] } + ) + } } // MARK: PersistenceReaderKey extension SargonKey { - - /// Loads the freshest value from storage (SargonOS). Returns `nil` if there is no value in storage. - /// - /// - Parameter initialValue: An initial value assigned to the `@Shared` property. - /// - Returns: An initial value provided by an external system, or `nil`. - public func load(initialValue: Value?) -> Value? { - fetchValueFromSargonOS() ?? initialValue - } - - - /// Subscribes to external updates, we do it by subscribing to `EventBus.notifications()`. - /// - /// - Parameters: - /// - initialValue: An initial value assigned to the `@Shared` property. - /// - didSet: A closure that is invoked with new values from an external system, or `nil` if the - /// external system no longer holds a value. - /// - Returns: A subscription to updates from an external system. If it is cancelled or - /// deinitialized, the `didSet` closure will no longer be invoked. - public func subscribe( - initialValue: Value?, - didSet: @Sendable @escaping (_ newValue: Value?) -> Void - ) -> Shared.Subscription { - let task = Task { [shouldFetch = self.shouldFetch] in - for await _ in await eventBus.notifications().map(\.event.kind).filter({ - shouldFetch($0) - }) { - guard !Task.isCancelled else { return } - - // The call `fetchValueFromSargonOS` might be costly - // we SHOULD try to use as fast and cheap calls as possible - // i.e. it is best to call `os.currentNetwork()` which is near instant - // compared to `os.profile().gateways.current.network.id` which is - // costly, since the whole of Profile has to pass across the UniFFI - // boundary - let newValue = fetchValueFromSargonOS() - - didSet(newValue) - } - } - return .init { - task.cancel() - } - } + + /// Loads the freshest value from storage (SargonOS). Returns `nil` if there is no value in storage. + /// + /// - Parameter initialValue: An initial value assigned to the `@Shared` property. + /// - Returns: An initial value provided by an external system, or `nil`. + public func load(initialValue: Value?) -> Value? { + fetchValueFromSargonOS() ?? initialValue + } + + + /// Subscribes to external updates, we do it by subscribing to `EventBus.notifications()`. + /// + /// - Parameters: + /// - initialValue: An initial value assigned to the `@Shared` property. + /// - didSet: A closure that is invoked with new values from an external system, or `nil` if the + /// external system no longer holds a value. + /// - Returns: A subscription to updates from an external system. If it is cancelled or + /// deinitialized, the `didSet` closure will no longer be invoked. + public func subscribe( + initialValue: Value?, + didSet: @Sendable @escaping (_ newValue: Value?) -> Void + ) -> Shared.Subscription { + let task = Task { [shouldFetch = self.shouldFetch] in + for await _ in await eventBus.notifications().map(\.event.kind).filter({ + shouldFetch($0) + }) { + guard !Task.isCancelled else { return } + + // The call `fetchValueFromSargonOS` might be costly + // we SHOULD try to use as fast and cheap calls as possible + // i.e. it is best to call `os.currentNetwork()` which is near instant + // compared to `os.profile().gateways.current.network.id` which is + // costly, since the whole of Profile has to pass across the UniFFI + // boundary + let newValue = fetchValueFromSargonOS() + + didSet(newValue) + } + } + return .init { + task.cancel() + } + } } extension SargonKey { - /// A String representation of `Self.Value`, used for `Equatable` and `Hashable` - /// conformance. - private var valueKind: String { - String(describing: Value.self) - } + /// A String representation of `Self.Value`, used for `Equatable` and `Hashable` + /// conformance. + private var valueKind: String { + String(describing: Value.self) + } } // MARK: Equatable extension SargonKey { - public static func == (lhs: SargonKey, rhs: SargonKey) -> Bool { - lhs.valueKind == rhs.valueKind && /* this aint pretty, but I guess it works */ EventKind.allCases.map(lhs.shouldFetch) == EventKind.allCases.map(rhs.shouldFetch) - } + public static func == (lhs: SargonKey, rhs: SargonKey) -> Bool { + lhs.valueKind == rhs.valueKind && /* this aint pretty, but I guess it works */ EventKind.allCases.map(lhs.shouldFetch) == EventKind.allCases.map(rhs.shouldFetch) + } } // MARK: Hashable extension SargonKey { - public func hash(into hasher: inout Hasher) { - hasher.combine(valueKind) - /* this aint pretty, but I guess it works */ - hasher.combine(EventKind.allCases.map(shouldFetch)) - } + public func hash(into hasher: inout Hasher) { + hasher.combine(valueKind) + /* this aint pretty, but I guess it works */ + hasher.combine(EventKind.allCases.map(shouldFetch)) + } } diff --git a/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+FactorSources.swift b/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+FactorSources.swift index 60408c5d8..1e6fab8f7 100644 --- a/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+FactorSources.swift +++ b/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+FactorSources.swift @@ -65,11 +65,11 @@ extension PersistenceKeyDefault> { extension SargonOS { public var factorSources: FactorSources { - factorSources().asIdentified() + try! factorSources().asIdentified() } public var shieldReferences: ShieldReferences { - securityStructuresOfFactorSourceIds().asIdentified() + try! securityStructuresOfFactorSourceIds().asIdentified() } public var shields: Shields { diff --git a/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+NetworkID.swift b/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+NetworkID.swift index d0066ea3d..08843e154 100644 --- a/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+NetworkID.swift +++ b/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+NetworkID.swift @@ -12,7 +12,7 @@ extension PersistenceReaderKey where Self == PersistenceKeyDefault> { public static let sharedNetwork = Self( SargonKey( - accessing: \.currentNetworkID, + mapping: { try $0.currentNetworkID }, fetchIf: \.affectsCurrentNetwork ), .default diff --git a/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+SavedGateways.swift b/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+SavedGateways.swift index 5ae973df7..63a99f13c 100644 --- a/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+SavedGateways.swift +++ b/examples/iOS/Backend/Sources/Planbok/SharedState/SharedState+SavedGateways.swift @@ -18,7 +18,7 @@ extension PersistenceReaderKey where Self == PersistenceKeyDefault> { public static let sharedSavedGateways = Self( SargonKey( - accessing: \.gateways, + mapping: { try $0.gateways }, fetchIf: \.affectsSavedGateways ), .default diff --git a/examples/iOS/Planbok.xcworkspace/xcshareddata/swiftpm/Package.resolved b/examples/iOS/Planbok.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3659c8b61..31c4b19aa 100644 --- a/examples/iOS/Planbok.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/examples/iOS/Planbok.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sideeffect-io/AsyncExtensions", "state" : { - "revision" : "1f0729e4f1f6c7166acfac3cec43b3cbe83be0e6", - "version" : "0.5.2" + "revision" : "3442d3d046800f1974bda096faaf0ac510b21154", + "version" : "0.5.3" } }, { From d6d9f88f97322bf570c585530f1c608aef898ecd Mon Sep 17 00:00:00 2001 From: matiasbzurovski <164921079+matiasbzurovski@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:56:54 +0200 Subject: [PATCH 5/6] Update workflows to use Xcode 16 (#222) update workflows to use Xcode 16 --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 ++-- crates/sargon/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9976851c3..fdd733906 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: - uses: RDXWorks-actions/setup-xcode@master with: - xcode-version: "15.3.0" + xcode-version: "16.0.0" - name: Install Rust Toolchain for aarch64-apple-darwin uses: RDXWorks-actions/toolchain@master diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffeb701bf..1d0c58196 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -127,7 +127,7 @@ jobs: - uses: RDXWorks-actions/setup-xcode@master with: - xcode-version: "15.3.0" + xcode-version: "16.0.0" - name: Install Rust Toolchain for aarch64-apple-darwin uses: RDXWorks-actions/toolchain@master @@ -198,7 +198,7 @@ jobs: - uses: RDXWorks-actions/setup-xcode@master with: # trying to fix https://github.com/rust-lang/rust/issues/113783 - xcode-version: "15.3.0" + xcode-version: "16.0.0" - name: Install Rust Toolchain uses: RDXWorks-actions/toolchain@master diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 38aaa0044..423f7418f 100644 --- a/crates/sargon/Cargo.toml +++ b/crates/sargon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sargon" -version = "1.1.19" +version = "1.1.20" edition = "2021" build = "build.rs" From a12fdc6b4ab9f3dc4d6fb26cc863778ad2999840 Mon Sep 17 00:00:00 2001 From: Alexander Cyon <116169792+CyonAlexRDX@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:42:13 +0200 Subject: [PATCH 6/6] bump (#227) --- crates/sargon/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 423f7418f..fe60d8260 100644 --- a/crates/sargon/Cargo.toml +++ b/crates/sargon/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sargon" -version = "1.1.20" +version = "1.1.21" edition = "2021" build = "build.rs"