commit 6e2f0527fcb4143eb736d423c263c8fc5a38de6d
parent 5d361b7ea4fb9c80c6e1664acfe17ce26dd8f63d
Author: triesap <tyson@radroots.org>
Date: Sun, 24 May 2026 22:17:23 +0000
app: add buyer order retry action
Diffstat:
6 files changed, 174 insertions(+), 13 deletions(-)
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -1466,6 +1466,10 @@ impl DesktopAppRuntimeState {
projection.orders.detail = Some(order_detail.clone());
changed = true;
}
+ if !projection.orders.has_recoverable_coordination {
+ projection.orders.has_recoverable_coordination = true;
+ changed = true;
+ }
changed
});
@@ -1488,8 +1492,10 @@ impl DesktopAppRuntimeState {
} else {
false
};
+ let coordination_changed =
+ self.refresh_personal_orders_coordination_retry_state(&buyer_context)?;
- Ok(personal_changed || section_changed || pending_changed)
+ Ok(personal_changed || section_changed || pending_changed || coordination_changed)
}
fn retry_pending_personal_order_coordination(&mut self) -> Result<bool, AppSqliteError> {
@@ -1500,6 +1506,9 @@ impl DesktopAppRuntimeState {
};
sqlite_store.load_recoverable_buyer_order_coordination_records(&buyer_context)?
};
+ if records.is_empty() {
+ return self.refresh_personal_orders_coordination_retry_state(&buyer_context);
+ }
let mut changed = false;
let mut refreshed_order_id = None;
@@ -1551,6 +1560,26 @@ impl DesktopAppRuntimeState {
Ok(changed)
}
+ fn refresh_personal_orders_coordination_retry_state(
+ &mut self,
+ buyer_context: &BuyerContext,
+ ) -> Result<bool, AppSqliteError> {
+ let Some(sqlite_store) = self.sqlite_store.as_ref() else {
+ return Ok(false);
+ };
+ let has_recoverable_coordination = !sqlite_store
+ .load_recoverable_buyer_order_coordination_records(buyer_context)?
+ .is_empty();
+ Ok(self.mutate_personal_projection(|projection| {
+ if projection.orders.has_recoverable_coordination == has_recoverable_coordination {
+ false
+ } else {
+ projection.orders.has_recoverable_coordination = has_recoverable_coordination;
+ true
+ }
+ }))
+ }
+
fn refresh_personal_orders_projection(
&mut self,
buyer_context: &BuyerContext,
@@ -1563,13 +1592,22 @@ impl DesktopAppRuntimeState {
.detail
.as_ref()
.map(|detail| detail.order_id);
- let (refreshed_cart, refreshed_checkout, refreshed_orders, refreshed_order_detail) = {
+ let (
+ refreshed_cart,
+ refreshed_checkout,
+ refreshed_orders,
+ refreshed_order_detail,
+ has_recoverable_coordination,
+ ) = {
let Some(sqlite_store) = self.sqlite_store.as_ref() else {
return Ok(false);
};
let refreshed_cart = sqlite_store.load_buyer_cart(buyer_context)?;
let refreshed_checkout = sqlite_store.load_buyer_checkout(buyer_context)?;
let refreshed_orders = sqlite_store.load_buyer_orders(buyer_context)?;
+ let has_recoverable_coordination = !sqlite_store
+ .load_recoverable_buyer_order_coordination_records(buyer_context)?
+ .is_empty();
let detail_order_id = current_detail_order_id
.filter(|order_id| {
refreshed_orders
@@ -1594,6 +1632,7 @@ impl DesktopAppRuntimeState {
refreshed_checkout,
refreshed_orders,
refreshed_order_detail,
+ has_recoverable_coordination,
)
};
@@ -1615,6 +1654,10 @@ impl DesktopAppRuntimeState {
projection.orders.detail = refreshed_order_detail.clone();
changed = true;
}
+ if projection.orders.has_recoverable_coordination != has_recoverable_coordination {
+ projection.orders.has_recoverable_coordination = has_recoverable_coordination;
+ changed = true;
+ }
changed
}))
@@ -4652,6 +4695,9 @@ fn load_selected_account_context_with_options(
let buyer_cart = sqlite_store.load_buyer_cart(&buyer_context)?;
let buyer_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?;
let buyer_orders = sqlite_store.load_buyer_orders(&buyer_context)?;
+ let has_recoverable_coordination = !sqlite_store
+ .load_recoverable_buyer_order_coordination_records(&buyer_context)?
+ .is_empty();
let buyer_order_detail = match continuity_state.buyer.orders_detail_order_id {
Some(order_id) => sqlite_store.load_buyer_order_detail(&buyer_context, order_id)?,
None => None,
@@ -4673,6 +4719,7 @@ fn load_selected_account_context_with_options(
orders: BuyerOrdersScreenProjection {
list: buyer_orders,
detail: buyer_order_detail,
+ has_recoverable_coordination,
},
..PersonalWorkspaceProjection::default()
};
@@ -8872,6 +8919,12 @@ mod tests {
.expect("same-session buyer order recovery retry should sync")
);
let summary_after_retry = runtime.summary();
+ assert!(
+ !summary_after_retry
+ .personal_projection
+ .orders
+ .has_recoverable_coordination
+ );
assert_eq!(
pending_order_sync_payloads(&runtime, buyer_account_id.as_str(), order_id),
vec![expected_order_sync_payload.clone()]
@@ -8955,6 +9008,12 @@ mod tests {
vec![format!("app:local_work:order_request:{order_id}")]
);
let summary = restarted_runtime.summary();
+ assert!(
+ !summary
+ .personal_projection
+ .orders
+ .has_recoverable_coordination
+ );
assert_eq!(summary.personal_projection.orders.list.rows.len(), 1);
assert_eq!(
summary.personal_projection.orders.list.rows[0].order_id,
@@ -12580,6 +12639,12 @@ mod tests {
summary.shell_projection.selected_section,
ShellSection::Personal(PersonalSection::Orders)
);
+ assert!(
+ summary
+ .personal_projection
+ .orders
+ .has_recoverable_coordination
+ );
assert!(summary.personal_projection.cart.cart.lines.is_empty());
assert!(!summary.personal_projection.cart.checkout.can_place_order);
assert_eq!(summary.personal_projection.orders.list.rows.len(), 1);
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -40,9 +40,9 @@ use radroots_app_remote_signer::{
};
use radroots_app_sqlite::{AppSqliteError, derive_farm_rules_readiness};
use radroots_app_state::{
- FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, PackDayBatchPrintRequest,
- PackDayExportProjection, PackDayHostHandoffRequest, PackDayPrintRequest,
- derive_product_publish_blockers,
+ BuyerOrdersScreenProjection, FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute,
+ PackDayBatchPrintRequest, PackDayExportProjection, PackDayHostHandoffRequest,
+ PackDayPrintRequest, derive_product_publish_blockers,
};
use radroots_app_sync::{
AppSyncRunStatus, SyncAggregateRef, SyncCheckpointState, SyncConflict, SyncConflictKind,
@@ -1570,7 +1570,35 @@ impl HomeView {
error = %runtime_error,
"failed to place buyer order"
);
- self.set_buyer_workspace_notice(notice)
+ let notice_changed = self.set_buyer_workspace_notice(notice);
+ buyer_order_coordination_notice_forces_redraw(notice) || notice_changed
+ }
+ }
+ }
+
+ fn retry_pending_personal_order_coordination(&mut self, cx: &mut Context<Self>) {
+ if self.retry_pending_personal_order_coordination_update() {
+ cx.notify();
+ }
+ }
+
+ fn retry_pending_personal_order_coordination_update(&mut self) -> bool {
+ match self.runtime.retry_pending_personal_order_coordination() {
+ Ok(true) => {
+ let _ = self.clear_buyer_workspace_notice();
+ true
+ }
+ Ok(false) => false,
+ Err(runtime_error) => {
+ error!(
+ target: "buyer",
+ event = "buyer.order_coordination_retry_failed",
+ error = %runtime_error,
+ "failed to retry buyer order coordination"
+ );
+ let notice = BuyerWorkspaceNotice::OrderCoordinationFailed;
+ let notice_changed = self.set_buyer_workspace_notice(notice);
+ buyer_order_coordination_notice_forces_redraw(notice) || notice_changed
}
}
}
@@ -3149,6 +3177,9 @@ impl HomeView {
AppTextKey::HomeNavOrders,
AppTextKey::PersonalOrdersSurfaceBody,
))
+ .when(buyer_orders_retry_action_visible(orders), |this| {
+ this.child(buyer_orders_retry_card(cx))
+ })
.child(if orders.list.rows.is_empty() {
home_empty_state_card(
AppTextKey::PersonalOrdersEmptyTitle,
@@ -8569,6 +8600,28 @@ fn buyer_orders_list_card(
.into_any_element()
}
+fn buyer_orders_retry_action_visible(orders: &BuyerOrdersScreenProjection) -> bool {
+ orders.has_recoverable_coordination
+}
+
+fn buyer_orders_retry_card(cx: &mut Context<HomeView>) -> AnyElement {
+ home_card(
+ app_shared_text(AppTextKey::PersonalOrdersCoordinationRetryTitle),
+ app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
+ .w_full()
+ .child(home_body_text(app_shared_text(
+ AppTextKey::PersonalOrdersCoordinationRetryBody,
+ )))
+ .child(action_button_primary(
+ "buyer-orders-retry-coordination",
+ app_shared_text(AppTextKey::PersonalOrdersCoordinationRetryAction),
+ cx.listener(|this, _, _, cx| this.retry_pending_personal_order_coordination(cx)),
+ cx,
+ )),
+ )
+ .into_any_element()
+}
+
fn buyer_orders_list_entry(
index: usize,
row: &BuyerOrdersListRow,
@@ -12786,6 +12839,10 @@ fn buyer_order_place_failure_notice(error: &AppSqliteError) -> BuyerWorkspaceNot
}
}
+fn buyer_order_coordination_notice_forces_redraw(notice: BuyerWorkspaceNotice) -> bool {
+ notice == BuyerWorkspaceNotice::OrderCoordinationFailed
+}
+
fn buyer_workspace_notice_card(notice: String) -> impl IntoElement {
app_surface_card(home_body_text(notice))
}
@@ -12937,9 +12994,10 @@ mod tests {
SettingsInventorySectionSpec, SettingsPanelViewKey, StartupHomeSurface,
StartupSignerConnectState, about_conflict_action_specs, about_conflict_aggregate_text,
about_conflict_detail_rows, about_conflict_review_body_key, about_manual_refresh_enabled,
- about_runtime_rows, about_status_rows, app_text, buyer_orders_status_key,
- farm_setup_onboarding_card_spec, farmer_home_farm_state, farmer_pack_day_available,
- home_auto_focus_target, home_content_scroll_id, home_saved_farm,
+ about_runtime_rows, about_status_rows, app_text,
+ buyer_order_coordination_notice_forces_redraw, buyer_orders_retry_action_visible,
+ buyer_orders_status_key, farm_setup_onboarding_card_spec, farmer_home_farm_state,
+ farmer_pack_day_available, home_auto_focus_target, home_content_scroll_id, home_saved_farm,
home_sidebar_navigation_sections, home_stage, home_window_launch_size_px,
home_window_minimum_size_px, pack_day_batch_print_action_presentation,
pack_day_batch_print_status_presentation, pack_day_export_action_enabled,
@@ -12982,10 +13040,10 @@ mod tests {
RadrootsAppRemoteSignerSessionRecord,
};
use radroots_app_state::{
- AppShellProjection, FarmWorkspaceReadinessProjection, FarmWorkspaceStatus, HomeRoute,
- PackDayBatchPrintProjection, PackDayBatchPrintRequest, PackDayExportProjection,
- PackDayHostHandoffProjection, PackDayHostHandoffRequest, PackDayPrintProjection,
- PackDayPrintRequest,
+ AppShellProjection, BuyerOrdersScreenProjection, FarmWorkspaceReadinessProjection,
+ FarmWorkspaceStatus, HomeRoute, PackDayBatchPrintProjection, PackDayBatchPrintRequest,
+ PackDayExportProjection, PackDayHostHandoffProjection, PackDayHostHandoffRequest,
+ PackDayPrintProjection, PackDayPrintRequest,
};
use radroots_app_sync::{
AppSyncProjection, AppSyncRunStatus, SyncAggregateRef, SyncCheckpointStatus, SyncConflict,
@@ -13101,6 +13159,25 @@ mod tests {
}
#[test]
+ fn buyer_order_coordination_failure_forces_redraw_when_notice_is_unchanged() {
+ assert!(buyer_order_coordination_notice_forces_redraw(
+ BuyerWorkspaceNotice::OrderCoordinationFailed
+ ));
+ assert!(!buyer_order_coordination_notice_forces_redraw(
+ BuyerWorkspaceNotice::OrderPlaceFailed
+ ));
+ }
+
+ #[test]
+ fn buyer_orders_retry_action_tracks_recoverable_coordination() {
+ let mut orders = BuyerOrdersScreenProjection::default();
+ assert!(!buyer_orders_retry_action_visible(&orders));
+
+ orders.has_recoverable_coordination = true;
+ assert!(buyer_orders_retry_action_visible(&orders));
+ }
+
+ #[test]
fn buyer_browse_refresh_failure_uses_typed_visible_notice() {
let (mut view, paths, home_dir) = test_home_view("buyer_notice");
block_shared_local_events_database(&paths);
diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs
@@ -144,6 +144,9 @@ define_app_text_keys! {
PersonalOrdersRepeatDemandNotePartialSingle => "personal.orders.repeat_demand.note.partial_single",
PersonalOrdersRepeatDemandNotePartialMultiple => "personal.orders.repeat_demand.note.partial_multiple",
PersonalOrdersRepeatDemandNoteUnavailable => "personal.orders.repeat_demand.note.unavailable",
+ PersonalOrdersCoordinationRetryTitle => "personal.orders.coordination_retry.title",
+ PersonalOrdersCoordinationRetryBody => "personal.orders.coordination_retry.body",
+ PersonalOrdersCoordinationRetryAction => "personal.orders.coordination_retry.action",
PersonalOrdersStatusPlaced => "personal.orders.status.placed",
PersonalOrdersStatusScheduled => "personal.orders.status.scheduled",
PersonalOrdersStatusReady => "personal.orders.status.ready",
diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs
@@ -440,6 +440,18 @@ mod tests {
app_text(AppTextKey::PersonalOrdersRepeatDemandNoteUnavailable),
"Items from this order are currently unavailable to reorder."
);
+ assert_eq!(
+ app_text(AppTextKey::PersonalOrdersCoordinationRetryTitle),
+ "Finish sharing saved orders"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PersonalOrdersCoordinationRetryBody),
+ "A saved order still needs to be shared with your order tools."
+ );
+ assert_eq!(
+ app_text(AppTextKey::PersonalOrdersCoordinationRetryAction),
+ "Try sharing again"
+ );
}
#[test]
diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs
@@ -132,6 +132,7 @@ pub struct BuyerCartScreenProjection {
pub struct BuyerOrdersScreenProjection {
pub list: BuyerOrdersProjection,
pub detail: Option<BuyerOrderDetailProjection>,
+ pub has_recoverable_coordination: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -124,6 +124,9 @@
"personal.orders.repeat_demand.note.partial_single": "One item from this order is currently unavailable to reorder.",
"personal.orders.repeat_demand.note.partial_multiple": "Some items from this order are currently unavailable to reorder.",
"personal.orders.repeat_demand.note.unavailable": "Items from this order are currently unavailable to reorder.",
+ "personal.orders.coordination_retry.title": "Finish sharing saved orders",
+ "personal.orders.coordination_retry.body": "A saved order still needs to be shared with your order tools.",
+ "personal.orders.coordination_retry.action": "Try sharing again",
"personal.orders.status.placed": "Placed",
"personal.orders.status.scheduled": "Scheduled",
"personal.orders.status.ready": "Ready",