field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 069280fcf16c68d4f33e75d8de78c02e5d0ccad1
parent 6acea88b9f807038bafe0b948b43ce2eb6f8c8c9
Author: triesap <tyson@radroots.org>
Date:   Sat,  8 Aug 2026 07:31:28 +0000

mobile: prove local MVP protocol flows

Diffstat:
Mcore/crates/tera_core/src/runtime/product_surface/model.rs | 6++++++
Mcore/crates/tera_core/src/runtime/product_surface/outbox.rs | 7++++++-
Mcore/crates/tera_core/src/runtime/product_surface/projection.rs | 15+++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/today.rs | 10++++++++++
4 files changed, 37 insertions(+), 1 deletion(-)

diff --git a/core/crates/tera_core/src/runtime/product_surface/model.rs b/core/crates/tera_core/src/runtime/product_surface/model.rs @@ -197,6 +197,12 @@ pub struct ClassifiedCard { pub price_unit: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub quantity: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub food_summary: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub food_published_at: Option<u64>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub food_status: Option<String>, pub context_rank: ContextRank, pub inclusion_reason: String, pub media: Vec<MediaReference>, diff --git a/core/crates/tera_core/src/runtime/product_surface/outbox.rs b/core/crates/tera_core/src/runtime/product_surface/outbox.rs @@ -100,6 +100,8 @@ pub struct Phase1DraftFormSnapshot { pub currency: Option<String>, pub unit: Option<String>, pub quantity: Option<String>, + #[serde(default)] + pub food_published_at_unix_s: Option<u64>, pub food_status: Option<String>, pub media: Vec<Phase1DraftMediaSnapshot>, } @@ -1730,7 +1732,9 @@ fn aggregate_state(draft: &AuthoredDraft, push: Option<&PushStatus>) -> Phase1Ou return Phase1OutboxState::Complete; } if push.settlement().has_failures() { - return if push.settlement().retryable() != 0 || push.settlement().delivery_retryable() != 0 + return if has_delivery_success(push) { + Phase1OutboxState::PartiallyDelivered + } else if push.settlement().retryable() != 0 || push.settlement().delivery_retryable() != 0 { Phase1OutboxState::Retryable } else if push.settlement().cancelled() != 0 || push.settlement().delivery_cancelled() != 0 @@ -1919,6 +1923,7 @@ mod tests { currency: None, unit: None, quantity: None, + food_published_at_unix_s: None, food_status: None, media: Vec::new(), } diff --git a/core/crates/tera_core/src/runtime/product_surface/projection.rs b/core/crates/tera_core/src/runtime/product_surface/projection.rs @@ -165,6 +165,15 @@ fn card( .then(|| tag_values(&tags, "radroots:quantity")) .flatten() .and_then(|values| values.first().cloned()); + let food_summary = matches!(card_type, TodayCardType::FoodAvailability) + .then(|| tag_value(&tags, &["summary"])) + .flatten(); + let food_published_at = matches!(card_type, TodayCardType::FoodAvailability) + .then(|| tag_time(&tags, "published_at")) + .flatten(); + let food_status = matches!(card_type, TodayCardType::FoodAvailability) + .then(|| tag_value(&tags, &["status"])) + .flatten(); let (effective_at, event_start, event_end) = match card_type { TodayCardType::Event => { let start = tag_time(&tags, "start").unwrap_or_else(|| event.created_at_u64()); @@ -198,6 +207,9 @@ fn card( price_currency, price_unit, quantity, + food_summary, + food_published_at, + food_status, context_rank: context.rank, inclusion_reason: context.reason.to_owned(), media, @@ -474,6 +486,9 @@ mod tests { assert_eq!(food.price_currency.as_deref(), Some("CAD")); assert_eq!(food.price_unit.as_deref(), Some("lb")); assert_eq!(food.quantity.as_deref(), Some("12")); + assert_eq!(food.food_summary.as_deref(), Some("Fresh bunches")); + assert_eq!(food.food_published_at, Some(1_999_999_999)); + assert_eq!(food.food_status.as_deref(), Some("active")); } #[test] diff --git a/core/crates/tera_core/src/runtime/product_surface/today.rs b/core/crates/tera_core/src/runtime/product_surface/today.rs @@ -659,6 +659,16 @@ impl AdmissionPolicy for TodayAdmissionPolicy { "radroots.mobile.today.v1" } + fn select_contract( + &self, + event: &radroots_event::admission::SignatureVerifiedEvent, + ) -> Option<&'static str> { + verify_nip01_event(event.event().clone()) + .ok() + .and_then(|event| admit_verified_event(event).ok()) + .map(|event| event.contract().id) + } + fn decide(&self, event: &ContractValidatedEvent) -> AdmissionDecision { let admitted = verify_nip01_event(event.event().clone()) .ok()