commit 57662d9fdb8ae4a5e31d1526bc79b7db3daeef55
parent aa89d5ba882454dad2131c1b7399385b63671e78
Author: triesap <tyson@radroots.org>
Date: Mon, 20 Apr 2026 19:16:42 +0000
marketplace: add buyer orders confirmation handoff
- add the buyer orders history surface with selectable local order rows and detail view
- route checkout completion into buyer orders with the newly placed order selected
- map shared seller order states into buyer-facing localized status copy and indicators
- extend runtime, source guard, and i18n coverage for buyer order detail behavior
Diffstat:
6 files changed, 472 insertions(+), 51 deletions(-)
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -6,13 +6,14 @@ use radroots_app_core::{AppDesktopRuntimePaths, AppRuntimePathsError, AppSharedA
use radroots_app_models::{
ActiveSurface, AppActivityContext, AppActivityKind, AppIdentityProjection, AppStartupGate,
BuyerCartLineProjection, BuyerCartProjection, BuyerCartReplaceConfirmationProjection,
- BuyerCheckoutDraft, BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmProfileRecord,
- FarmReadiness, FarmRulesProjection, FarmSetupDraft, FarmSetupProjection, FarmSummary,
- FarmerSection, FulfillmentWindowId, LoggedOutStartupProjection, OrderDetailProjection, OrderId,
- OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayProjection,
- PackDayScreenQueryState, PersonalSection, PickupLocationRecord, ProductEditorDraft, ProductId,
- ProductsFilter, ProductsListProjection, ProductsSort, SettingsAccountProjection,
- SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection,
+ BuyerCheckoutDraft, BuyerOrderDetailProjection, BuyerProductDetailProjection, FarmId,
+ FarmOrderMethod, FarmProfileRecord, FarmReadiness, FarmRulesProjection, FarmSetupDraft,
+ FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
+ LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrdersFilter, OrdersListProjection,
+ OrdersScreenQueryState, PackDayProjection, PackDayScreenQueryState, PersonalSection,
+ PickupLocationRecord, ProductEditorDraft, ProductId, ProductsFilter, ProductsListProjection,
+ ProductsSort, SettingsAccountProjection, SettingsPreference, SettingsSection, ShellSection,
+ TodayAgendaProjection,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession,
@@ -241,6 +242,10 @@ impl DesktopAppRuntime {
self.lock_state_mut().place_personal_order()
}
+ pub fn open_personal_order_detail(&self, order_id: OrderId) -> Result<bool, AppSqliteError> {
+ self.lock_state_mut().open_personal_order_detail(order_id)
+ }
+
pub fn set_personal_search_query(&self, search_query: &str) -> Result<bool, AppSqliteError> {
self.lock_state_mut()
.set_personal_search_query(search_query)
@@ -985,6 +990,12 @@ impl DesktopAppRuntimeState {
reason: "buyer order write did not surface in buyer order history",
});
}
+ let Some(order_detail) = sqlite_store.load_buyer_order_detail(&buyer_context, order_id)?
+ else {
+ return Err(AppSqliteError::InvalidProjection {
+ reason: "buyer order write did not surface in buyer order detail",
+ });
+ };
let personal_changed = self.mutate_personal_projection(|projection| {
let mut changed = false;
@@ -1000,8 +1011,8 @@ impl DesktopAppRuntimeState {
projection.orders.list = refreshed_orders.clone();
changed = true;
}
- if projection.orders.detail.is_some() {
- projection.orders.detail = None;
+ if projection.orders.detail.as_ref() != Some(&order_detail) {
+ projection.orders.detail = Some(order_detail.clone());
changed = true;
}
@@ -1012,6 +1023,22 @@ impl DesktopAppRuntimeState {
Ok(personal_changed || section_changed)
}
+ fn open_personal_order_detail(&mut self, order_id: OrderId) -> Result<bool, AppSqliteError> {
+ let Some(sqlite_store) = self.sqlite_store.as_ref() else {
+ return Ok(false);
+ };
+ let buyer_context = self.state_store.identity_projection().buyer_context();
+ let Some(order_detail) = sqlite_store.load_buyer_order_detail(&buyer_context, order_id)?
+ else {
+ return Ok(false);
+ };
+
+ let detail_changed = self.set_personal_order_detail(Some(order_detail));
+ let section_changed = self.select_personal_section(PersonalSection::Orders);
+
+ Ok(detail_changed || section_changed)
+ }
+
fn set_personal_search_query(&mut self, search_query: &str) -> Result<bool, AppSqliteError> {
let query = self.state_store.personal_projection().search.query.clone();
if query.search_query == search_query {
@@ -1698,6 +1725,17 @@ impl DesktopAppRuntimeState {
})
}
+ fn set_personal_order_detail(&mut self, detail: Option<BuyerOrderDetailProjection>) -> bool {
+ self.mutate_personal_projection(|projection| {
+ if projection.orders.detail == detail {
+ return false;
+ }
+
+ projection.orders.detail = detail;
+ true
+ })
+ }
+
fn replace_personal_search_query(
&mut self,
query: BuyerSearchScreenQueryState,
@@ -3601,6 +3639,116 @@ mod tests {
.storage_key(),
"placed"
);
+ assert_eq!(
+ summary
+ .personal_projection
+ .orders
+ .detail
+ .as_ref()
+ .expect("buyer order detail should be selected")
+ .order_id,
+ summary.personal_projection.orders.list.rows[0].order_id
+ );
+ assert_eq!(
+ summary
+ .personal_projection
+ .orders
+ .detail
+ .as_ref()
+ .expect("buyer order detail")
+ .order_note
+ .as_deref(),
+ Some("Leave by the cooler")
+ );
+ }
+
+ #[test]
+ fn runtime_opens_buyer_order_detail_from_personal_orders() {
+ let runtime = memory_runtime();
+ let (account_id, farm_id) = provision_ready_farmer_account(&runtime);
+ assert!(
+ runtime
+ .select_active_surface(ActiveSurface::Personal)
+ .expect("surface should switch into marketplace")
+ );
+ let fulfillment_window_id = seed_buyer_marketplace_support(
+ &runtime,
+ account_id.as_str(),
+ farm_id,
+ "North field farm",
+ "Friday pickup",
+ );
+ let product_id = seed_product(
+ &runtime,
+ farm_id,
+ "Salad mix",
+ "Spring blend",
+ "published",
+ Some(8),
+ "2026-04-20T09:00:00Z",
+ );
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .connection()
+ .execute_batch(&format!(
+ "update products
+ set availability_window_id = '{fulfillment_window_id}'
+ where id = '{product_id}'"
+ ))
+ .expect("buyer detail product should attach a fulfillment window");
+ assert!(
+ runtime
+ .open_personal_product_detail(PersonalSection::Browse, product_id)
+ .expect("buyer detail should open")
+ );
+ assert!(
+ runtime
+ .add_personal_product_to_cart(PersonalSection::Browse, false)
+ .expect("buyer product should add to cart")
+ );
+ assert!(
+ runtime
+ .save_personal_checkout_draft(BuyerCheckoutDraft {
+ name: "Casey Buyer".to_owned(),
+ email: "casey@example.com".to_owned(),
+ phone: String::new(),
+ order_note: String::new(),
+ })
+ .expect("buyer checkout draft should save")
+ );
+ assert!(
+ runtime
+ .place_personal_order()
+ .expect("buyer order should place")
+ );
+ let order_id = runtime.summary().personal_projection.orders.list.rows[0].order_id;
+ assert!(runtime.select_personal_section(PersonalSection::Browse));
+ assert!(runtime.lock_state_mut().set_personal_order_detail(None));
+
+ assert!(
+ runtime
+ .open_personal_order_detail(order_id)
+ .expect("buyer order detail should open")
+ );
+
+ let summary = runtime.summary();
+ assert_eq!(
+ summary.shell_projection.selected_section,
+ ShellSection::Personal(PersonalSection::Orders)
+ );
+ assert_eq!(
+ summary
+ .personal_projection
+ .orders
+ .detail
+ .as_ref()
+ .expect("buyer order detail")
+ .order_id,
+ order_id
+ );
}
#[test]
diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs
@@ -51,11 +51,13 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"buyer.checkout_place_failed",
"buyer.checkout_save_failed",
"buyer.detail_open_failed",
+ "buyer.order_open_failed",
"bunker uri",
"bunker://466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27?relay=wss%3A%2F%2Frelay.radroots.example",
"buyer.fulfillment_filter_update_failed",
"buyer.search_query_update_failed",
"failed to add buyer product to cart",
+ "failed to open buyer order detail",
"failed to place buyer order",
"failed to remove buyer cart line",
"failed to save buyer checkout draft",
@@ -104,6 +106,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"buyer-nav-cart",
"buyer-nav-orders",
"buyer-nav-search",
+ "buyer-order-open",
"buyer-orders-scroll",
"personal-search-delivery",
"personal-search-pickup",
@@ -214,7 +217,6 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"wss://relay.radroots.example",
"{currency_code} {dollars}.{cents:02}",
"{} {} {}.",
- "{} local orders are already available on this device.",
"{quantity} {unit_label}",
"{} {}",
];
@@ -276,7 +278,22 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::PersonalBrowsePlaceholderBody",
"AppTextKey::PersonalSearchPlaceholderBody",
"AppTextKey::PersonalCartPlaceholderBody",
- "AppTextKey::PersonalOrdersPlaceholderBody",
+ "AppTextKey::PersonalOrdersSurfaceBody",
+ "AppTextKey::PersonalOrdersEmptyTitle",
+ "AppTextKey::PersonalOrdersEmptyBody",
+ "AppTextKey::PersonalOrdersListTitle",
+ "AppTextKey::PersonalOrdersDetailTitle",
+ "AppTextKey::PersonalOrdersDetailEmptyBody",
+ "AppTextKey::PersonalOrdersDetailFarmLabel",
+ "AppTextKey::PersonalOrdersDetailStatusLabel",
+ "AppTextKey::PersonalOrdersDetailFulfillmentLabel",
+ "AppTextKey::PersonalOrdersDetailNoteLabel",
+ "AppTextKey::PersonalOrdersDetailItemsTitle",
+ "AppTextKey::PersonalOrdersStatusPlaced",
+ "AppTextKey::PersonalOrdersStatusScheduled",
+ "AppTextKey::PersonalOrdersStatusReady",
+ "AppTextKey::PersonalOrdersStatusCompleted",
+ "AppTextKey::PersonalOrdersStatusRefunded",
"AppTextKey::PersonalCartSurfaceBody",
"AppTextKey::PersonalOrderSummaryTitle",
"AppTextKey::PersonalFulfillmentTitle",
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -13,16 +13,17 @@ pub use radroots_app_models::SettingsSection as SettingsPanelViewKey;
use radroots_app_models::{
AppStartupGate, BlackoutPeriodId, BlackoutPeriodRecord, BuyerCartProjection,
BuyerCartReplaceConfirmationProjection, BuyerCheckoutDraft, BuyerCheckoutSummaryProjection,
- BuyerListingRow, BuyerProductDetailProjection, FarmId, FarmOperatingRulesRecord,
- FarmOrderMethod, FarmProfileRecord, FarmReadinessBlocker, FarmRulesProjection,
- FarmRulesReadiness, FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind,
- FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary,
- LoggedOutStartupPhase, OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow,
- OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersListRow, PackDayPackListRow,
- PackDayProductTotalRow, PackDayRosterRow, PersonalEntryState, PersonalSection,
- PickupLocationId, PickupLocationRecord, ProductAttentionState, ProductEditorDraft, ProductId,
- ProductListRow, ProductPricePresentation, ProductPublishBlocker, ProductStatus, ProductsFilter,
- ProductsListRow, ProductsSort, ShellSection, TodayAgendaProjection, TodaySetupTaskKind,
+ BuyerListingRow, BuyerOrderDetailProjection, BuyerOrderStatus, BuyerOrdersListRow,
+ BuyerProductDetailProjection, FarmId, FarmOperatingRulesRecord, FarmOrderMethod,
+ FarmProfileRecord, FarmReadinessBlocker, FarmRulesProjection, FarmRulesReadiness,
+ FarmSetupBlocker, FarmSetupDraft, FarmSummary, FarmTimingConflictKind, FarmerSection,
+ FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, LoggedOutStartupPhase,
+ OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction,
+ OrderStatus, OrdersFilter, OrdersListRow, PackDayPackListRow, PackDayProductTotalRow,
+ PackDayRosterRow, PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord,
+ ProductAttentionState, ProductEditorDraft, ProductId, ProductListRow, ProductPricePresentation,
+ ProductPublishBlocker, ProductStatus, ProductsFilter, ProductsListRow, ProductsSort,
+ ShellSection, TodayAgendaProjection, TodaySetupTaskKind,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome,
@@ -1259,6 +1260,22 @@ impl HomeView {
}
}
+ fn open_personal_order_detail(&mut self, order_id: OrderId, cx: &mut Context<Self>) {
+ match self.runtime.open_personal_order_detail(order_id) {
+ Ok(true) => cx.notify(),
+ Ok(false) => {}
+ Err(runtime_error) => {
+ error!(
+ target: "buyer",
+ event = "buyer.order_open_failed",
+ error = %runtime_error,
+ order_id = %order_id,
+ "failed to open buyer order detail"
+ );
+ }
+ }
+ }
+
fn select_products_filter(&mut self, filter: ProductsFilter, cx: &mut Context<Self>) {
match self.runtime.select_products_filter(filter) {
Ok(true) => {
@@ -2067,7 +2084,9 @@ impl HomeView {
PersonalSection::Cart => self
.render_buyer_cart_content(runtime, cx)
.into_any_element(),
- PersonalSection::Orders => buyer_orders_placeholder(runtime).into_any_element(),
+ PersonalSection::Orders => self
+ .render_buyer_orders_content(runtime, cx)
+ .into_any_element(),
};
app_split_shell(
@@ -2420,6 +2439,48 @@ impl HomeView {
.into_any_element()
}
+ fn render_buyer_orders_content(
+ &mut self,
+ runtime: &DesktopAppRuntimeSummary,
+ cx: &mut Context<Self>,
+ ) -> AnyElement {
+ let orders = &runtime.personal_projection.orders;
+ let selected_order_id = orders.detail.as_ref().map(|detail| detail.order_id);
+
+ app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
+ .w_full()
+ .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
+ .mx_auto()
+ .child(buyer_workspace_title_block(
+ AppTextKey::HomeNavOrders,
+ AppTextKey::PersonalOrdersSurfaceBody,
+ ))
+ .child(if orders.list.rows.is_empty() {
+ home_empty_state_card(
+ AppTextKey::PersonalOrdersEmptyTitle,
+ AppTextKey::PersonalOrdersEmptyBody,
+ )
+ .into_any_element()
+ } else {
+ app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
+ .w_full()
+ .child(buyer_orders_list_card(
+ &orders.list.rows,
+ selected_order_id,
+ cx,
+ ))
+ .child(
+ orders
+ .detail
+ .as_ref()
+ .map(buyer_order_detail_card)
+ .unwrap_or_else(|| buyer_order_detail_empty_card().into_any_element()),
+ )
+ .into_any_element()
+ })
+ .into_any_element()
+ }
+
fn render_farmer_workspace(
&mut self,
runtime: &DesktopAppRuntimeSummary,
@@ -6355,38 +6416,167 @@ fn buyer_money_text(amount_minor_units: u32, currency_code: &str) -> String {
}
}
-fn buyer_surface_placeholder(
- title_key: AppTextKey,
- body_key: AppTextKey,
- detail: Option<String>,
+fn buyer_orders_list_card(
+ rows: &[BuyerOrdersListRow],
+ selected_order_id: Option<OrderId>,
+ cx: &mut Context<HomeView>,
) -> AnyElement {
- app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
- .w_full()
- .max_w(px(APP_UI_THEME.shells.home_card_max_width_px))
- .mx_auto()
- .child(app_text_value(app_shared_text(title_key)))
- .child(app_surface_card(
- app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
- .w_full()
- .child(home_body_text(app_shared_text(body_key)))
- .when_some(detail, |this, detail| this.child(home_body_text(detail))),
- ))
- .into_any_element()
+ home_card(
+ app_shared_text(AppTextKey::PersonalOrdersListTitle),
+ app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
+ .w_full()
+ .children(
+ rows.iter()
+ .enumerate()
+ .map(|(index, row)| {
+ buyer_orders_list_entry(
+ index,
+ row,
+ selected_order_id == Some(row.order_id),
+ cx,
+ )
+ })
+ .collect::<Vec<_>>(),
+ ),
+ )
+ .into_any_element()
}
-fn buyer_orders_placeholder(runtime: &DesktopAppRuntimeSummary) -> AnyElement {
- let detail = (!runtime.personal_projection.orders.list.rows.is_empty()).then_some(format!(
- "{} local orders are already available on this device.",
- runtime.personal_projection.orders.list.rows.len()
- ));
+fn buyer_orders_list_entry(
+ index: usize,
+ row: &BuyerOrdersListRow,
+ is_selected: bool,
+ cx: &mut Context<HomeView>,
+) -> AnyElement {
+ app_button_card(
+ ("buyer-order-open", index),
+ is_selected,
+ cx.listener({
+ let order_id = row.order_id;
+ move |this, _, _, cx| this.open_personal_order_detail(order_id, cx)
+ }),
+ cx,
+ div()
+ .w_full()
+ .min_w_0()
+ .p(px(APP_UI_THEME.shells.home_card_padding_px))
+ .flex()
+ .flex_col()
+ .gap(px(APP_UI_THEME.foundation.spacing.small_px))
+ .child(
+ div()
+ .w_full()
+ .min_w_0()
+ .flex()
+ .items_start()
+ .justify_between()
+ .gap(px(APP_UI_THEME.shells.home_stack_gap_px))
+ .child(
+ app_stack_v(4.0)
+ .flex_1()
+ .min_w_0()
+ .child(app_text_label(row.order_number.clone()))
+ .child(settings_badge_text(row.farm_display_name.clone())),
+ )
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .gap(px(6.0))
+ .child(status_indicator(buyer_orders_status_color(row.status)))
+ .child(
+ div()
+ .text_size(px(APP_UI_THEME
+ .foundation
+ .typography
+ .utility_title_text_px))
+ .font_weight(gpui::FontWeight::MEDIUM)
+ .text_color(rgb(APP_UI_THEME.foundation.text.primary))
+ .child(app_shared_text(buyer_orders_status_key(row.status))),
+ ),
+ ),
+ )
+ .child(buyer_listing_chip(row.fulfillment_summary.clone())),
+ )
+ .into_any_element()
+}
+
+fn buyer_order_detail_card(detail: &BuyerOrderDetailProjection) -> AnyElement {
+ home_card(
+ app_shared_text(AppTextKey::PersonalOrdersDetailTitle),
+ app_stack_v(APP_UI_THEME.shells.home_stack_gap_px)
+ .w_full()
+ .child(app_heading_section(detail.order_number.clone()))
+ .child(settings_badge_text(detail.farm_display_name.clone()))
+ .child(label_value_list([
+ LabelValueRow::new(
+ app_shared_text(AppTextKey::PersonalOrdersDetailFarmLabel),
+ detail.farm_display_name.clone(),
+ ),
+ LabelValueRow::new(
+ app_shared_text(AppTextKey::PersonalOrdersDetailStatusLabel),
+ app_shared_text(buyer_orders_status_key(detail.status)),
+ ),
+ LabelValueRow::new(
+ app_shared_text(AppTextKey::PersonalOrdersDetailFulfillmentLabel),
+ detail.fulfillment_summary.clone(),
+ ),
+ LabelValueRow::new(
+ app_shared_text(AppTextKey::PersonalOrdersDetailNoteLabel),
+ order_optional_text(detail.order_note.as_deref()),
+ ),
+ ]))
+ .child(app_form_section(
+ app_shared_text(AppTextKey::PersonalOrdersDetailItemsTitle),
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .gap(px(APP_UI_THEME.foundation.spacing.tight_px))
+ .children(
+ detail
+ .items
+ .iter()
+ .map(order_detail_item_row)
+ .collect::<Vec<_>>(),
+ )
+ .when(detail.items.is_empty(), |this| {
+ this.child(home_body_text(app_shared_text(AppTextKey::ValueNone)))
+ }),
+ )),
+ )
+ .into_any_element()
+}
- buyer_surface_placeholder(
- AppTextKey::HomeNavOrders,
- AppTextKey::PersonalOrdersPlaceholderBody,
- detail,
+fn buyer_order_detail_empty_card() -> impl IntoElement {
+ home_card(
+ app_shared_text(AppTextKey::PersonalOrdersDetailTitle),
+ home_body_text(app_shared_text(AppTextKey::PersonalOrdersDetailEmptyBody)),
)
}
+fn buyer_orders_status_key(status: BuyerOrderStatus) -> AppTextKey {
+ match status {
+ BuyerOrderStatus::Placed => AppTextKey::PersonalOrdersStatusPlaced,
+ BuyerOrderStatus::Scheduled => AppTextKey::PersonalOrdersStatusScheduled,
+ BuyerOrderStatus::Ready => AppTextKey::PersonalOrdersStatusReady,
+ BuyerOrderStatus::Completed => AppTextKey::PersonalOrdersStatusCompleted,
+ BuyerOrderStatus::Refunded => AppTextKey::PersonalOrdersStatusRefunded,
+ }
+}
+
+fn buyer_orders_status_color(status: BuyerOrderStatus) -> u32 {
+ match status {
+ BuyerOrderStatus::Placed => APP_UI_THEME.components.app_status_indicator.attention,
+ BuyerOrderStatus::Scheduled | BuyerOrderStatus::Ready => {
+ APP_UI_THEME.components.app_status_indicator.online
+ }
+ BuyerOrderStatus::Completed | BuyerOrderStatus::Refunded => {
+ APP_UI_THEME.components.app_status_indicator.offline
+ }
+ }
+}
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StartupHomeSurface {
IssueCard,
@@ -9347,8 +9537,8 @@ mod tests {
AppTextKey, FarmerHomeFarmState, HomeStage, SETTINGS_FARM_PANEL_SECTIONS,
SETTINGS_NAVIGATION_ORDER, SETTINGS_OPERATIONS_PANEL_SECTIONS,
SettingsInventorySectionSpec, SettingsPanelViewKey, StartupHomeSurface,
- StartupSignerConnectState, farm_setup_onboarding_card_spec, farmer_home_farm_state,
- farmer_pack_day_available, home_content_scroll_id, home_saved_farm,
+ StartupSignerConnectState, buyer_orders_status_key, farm_setup_onboarding_card_spec,
+ farmer_home_farm_state, farmer_pack_day_available, home_content_scroll_id, home_saved_farm,
home_sidebar_navigation_sections, home_stage, home_window_launch_size_px,
home_window_minimum_size_px, parse_optional_product_editor_stock_input,
parse_product_editor_price_input, product_display_title, startup_home_surface,
@@ -9359,8 +9549,8 @@ mod tests {
use crate::runtime::DesktopAppRuntimeSummary;
use radroots_app_models::SettingsAccountProjection;
use radroots_app_models::{
- ActiveSurface, AppStartupGate, FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft,
- FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
+ ActiveSurface, AppStartupGate, BuyerOrderStatus, FarmId, FarmOrderMethod, FarmReadiness,
+ FarmSetupDraft, FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection,
PackDayProjection, PersonalSection, ShellSection, TodayAgendaProjection, TodaySetupTask,
TodaySetupTaskKind,
@@ -9551,6 +9741,18 @@ mod tests {
}
#[test]
+ fn buyer_orders_status_keys_use_buyer_facing_copy() {
+ assert_eq!(
+ buyer_orders_status_key(BuyerOrderStatus::Placed),
+ AppTextKey::PersonalOrdersStatusPlaced
+ );
+ assert_eq!(
+ buyer_orders_status_key(BuyerOrderStatus::Ready),
+ AppTextKey::PersonalOrdersStatusReady
+ );
+ }
+
+ #[test]
fn farmer_home_farm_state_distinguishes_no_farm_incomplete_and_configured() {
let farm_id = FarmId::new();
let incomplete_farm = FarmSummary {
diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs
@@ -110,6 +110,22 @@ define_app_text_keys! {
PersonalSearchPlaceholderBody => "personal.search.placeholder.body",
PersonalCartPlaceholderBody => "personal.cart.placeholder.body",
PersonalOrdersPlaceholderBody => "personal.orders.placeholder.body",
+ PersonalOrdersSurfaceBody => "personal.orders.surface.body",
+ PersonalOrdersEmptyTitle => "personal.orders.empty.title",
+ PersonalOrdersEmptyBody => "personal.orders.empty.body",
+ PersonalOrdersListTitle => "personal.orders.list.title",
+ PersonalOrdersDetailTitle => "personal.orders.detail.title",
+ PersonalOrdersDetailEmptyBody => "personal.orders.detail.empty.body",
+ PersonalOrdersDetailFarmLabel => "personal.orders.detail.farm.label",
+ PersonalOrdersDetailStatusLabel => "personal.orders.detail.status.label",
+ PersonalOrdersDetailFulfillmentLabel => "personal.orders.detail.fulfillment.label",
+ PersonalOrdersDetailNoteLabel => "personal.orders.detail.note.label",
+ PersonalOrdersDetailItemsTitle => "personal.orders.detail.items.title",
+ PersonalOrdersStatusPlaced => "personal.orders.status.placed",
+ PersonalOrdersStatusScheduled => "personal.orders.status.scheduled",
+ PersonalOrdersStatusReady => "personal.orders.status.ready",
+ PersonalOrdersStatusCompleted => "personal.orders.status.completed",
+ PersonalOrdersStatusRefunded => "personal.orders.status.refunded",
PersonalCartSurfaceBody => "personal.cart.surface.body",
PersonalOrderSummaryTitle => "personal.order_summary.title",
PersonalFulfillmentTitle => "personal.fulfillment.title",
diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs
@@ -226,6 +226,28 @@ mod tests {
}
#[test]
+ fn english_marketplace_orders_copy_matches_the_buyer_history_contract() {
+ assert_eq!(
+ app_text(AppTextKey::PersonalOrdersSurfaceBody),
+ "Review orders placed on this device."
+ );
+ assert_eq!(
+ app_text(AppTextKey::PersonalOrdersEmptyTitle),
+ "No orders yet"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PersonalOrdersListTitle),
+ "Order history"
+ );
+ assert_eq!(app_text(AppTextKey::PersonalOrdersStatusPlaced), "Placed");
+ assert_eq!(app_text(AppTextKey::PersonalOrdersStatusReady), "Ready");
+ assert_eq!(
+ app_text(AppTextKey::PersonalOrdersDetailTitle),
+ "Order detail"
+ );
+ }
+
+ #[test]
fn english_pack_day_copy_matches_the_contextual_execution_contract() {
assert_eq!(app_text(AppTextKey::PackDayTitle), "Pack day");
assert_eq!(
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -89,6 +89,22 @@
"personal.search.placeholder.body": "Search will use the same marketplace listings and stay focused on products, farms, and pickup options.",
"personal.cart.placeholder.body": "Add items from one farm to start an order.",
"personal.orders.placeholder.body": "Placed orders will appear here on this device.",
+ "personal.orders.surface.body": "Review orders placed on this device.",
+ "personal.orders.empty.title": "No orders yet",
+ "personal.orders.empty.body": "Orders you place on this device will appear here.",
+ "personal.orders.list.title": "Order history",
+ "personal.orders.detail.title": "Order detail",
+ "personal.orders.detail.empty.body": "Select an order to review the details.",
+ "personal.orders.detail.farm.label": "Farm",
+ "personal.orders.detail.status.label": "Status",
+ "personal.orders.detail.fulfillment.label": "Fulfillment",
+ "personal.orders.detail.note.label": "Order note",
+ "personal.orders.detail.items.title": "Items",
+ "personal.orders.status.placed": "Placed",
+ "personal.orders.status.scheduled": "Scheduled",
+ "personal.orders.status.ready": "Ready",
+ "personal.orders.status.completed": "Completed",
+ "personal.orders.status.refunded": "Refunded",
"personal.cart.surface.body": "Review items from one farm and continue to checkout when you're ready.",
"personal.order_summary.title": "Order summary",
"personal.fulfillment.title": "Fulfillment",