From de4a70009d862085bd0d052d1f4c5c16dd8878a9 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 30 Aug 2025 19:01:25 -0400 Subject: [PATCH] notification hook --- ...maybe_notify_frontend_of_billing_errors.rs | 57 +++++++++++++++++++ .../src/core/commands/enqueue/common/mod.rs | 2 + .../common/notify_frontend_of_errors.rs | 10 ++++ .../enqueue_image_bg_removal_command.rs | 3 + .../enqueue_contextual_edit_image_command.rs | 3 + .../enqueue_image_inpaint_command.rs | 3 + .../enqueue_image_to_3d_object_command.rs | 3 + .../enqueue_image_to_video_command.rs | 5 +- .../enqueue_text_to_image_command.rs | 3 + .../core/events/basic_sendable_event_trait.rs | 9 ++- .../src/core/events/functional_events/mod.rs | 1 + .../show_provider_billing_modal_event.rs | 25 ++++++++ .../show_provider_login_modal_event.rs | 6 +- .../enums/src/tauri/ux/tauri_event_name.rs | 14 ++++- 14 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 crates/desktop/artcraft/src/core/commands/enqueue/common/maybe_notify_frontend_of_billing_errors.rs create mode 100644 crates/desktop/artcraft/src/core/commands/enqueue/common/notify_frontend_of_errors.rs create mode 100644 crates/desktop/artcraft/src/core/events/functional_events/show_provider_billing_modal_event.rs diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/common/maybe_notify_frontend_of_billing_errors.rs b/crates/desktop/artcraft/src/core/commands/enqueue/common/maybe_notify_frontend_of_billing_errors.rs new file mode 100644 index 0000000000..7cc52f6d75 --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/common/maybe_notify_frontend_of_billing_errors.rs @@ -0,0 +1,57 @@ +use crate::core::commands::enqueue::generate_error::{BillingIssueReason, BillingProvider, GenerateError, ProviderFailureReason}; +use crate::core::events::functional_events::show_provider_billing_modal_event::ShowProviderBillingModalEvent; +use enums::common::generation_provider::GenerationProvider; +use log::warn; +use storyteller_client::error::api_error::ApiError; +use storyteller_client::error::storyteller_error::StorytellerError; +use tauri::AppHandle; + +pub async fn maybe_notify_frontend_of_billing_errors( + app: &AppHandle, + error: &GenerateError, +) { + match error { + GenerateError::BillingIssue(reason) => { + billing_error(app, reason); + } + GenerateError::ProviderFailure(reason) => { + provider_billing_error(app, reason); + } + _ => { + // Do nothing for other types of errors + } + } +} + +fn billing_error( + app: &AppHandle, + reason: &BillingIssueReason, +) { + let provider = match reason.provider { + BillingProvider::Fal => GenerationProvider::Fal, + BillingProvider::Midjourney => GenerationProvider::Midjourney, + BillingProvider::Sora => GenerationProvider::Sora, + BillingProvider::Storyteller => GenerationProvider::Artcraft, + }; + warn!("Billing issue with: {:?}", provider); + ShowProviderBillingModalEvent::send_for_provider(provider, app); +} + +fn provider_billing_error( + app: &AppHandle, + error: &ProviderFailureReason, +) { + let provider; + + match error { + ProviderFailureReason::StorytellerError(StorytellerError::Api(ApiError::PaymentRequired(reason))) => { + warn!("Billing issue with Artcraft/Storyteller: {}", reason); + provider = GenerationProvider::Artcraft; + } + _ => { + return; + } + } + + ShowProviderBillingModalEvent::send_for_provider(provider, app); +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/common/mod.rs b/crates/desktop/artcraft/src/core/commands/enqueue/common/mod.rs index e69de29bb2..dcf51ca12a 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/common/mod.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/common/mod.rs @@ -0,0 +1,2 @@ +pub mod notify_frontend_of_errors; +mod maybe_notify_frontend_of_billing_errors; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/common/notify_frontend_of_errors.rs b/crates/desktop/artcraft/src/core/commands/enqueue/common/notify_frontend_of_errors.rs new file mode 100644 index 0000000000..30c9bdbdcb --- /dev/null +++ b/crates/desktop/artcraft/src/core/commands/enqueue/common/notify_frontend_of_errors.rs @@ -0,0 +1,10 @@ +use crate::core::commands::enqueue::common::maybe_notify_frontend_of_billing_errors::maybe_notify_frontend_of_billing_errors; +use crate::core::commands::enqueue::generate_error::GenerateError; +use tauri::AppHandle; + +pub async fn notify_frontend_of_errors( + app: &AppHandle, + errors: &GenerateError, +) { + maybe_notify_frontend_of_billing_errors(app, errors).await; +} diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs index b29455c342..3607cd6625 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_bg_removal/enqueue_image_bg_removal_command.rs @@ -1,3 +1,4 @@ +use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError}; use crate::core::commands::enqueue::image_bg_removal::generic::handle_generic_bg_removal::handle_generic_bg_removal; use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit; @@ -123,6 +124,8 @@ pub async fn enqueue_image_bg_removal_command( match result { Err(err) => { error!("Error removing background: {:?}", err); + + notify_frontend_of_errors(&app, &err).await; // TODO: Derive from err. Make service provider optional. let event = GenerationEnqueueFailureEvent { diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs index d00efe4600..eab2495577 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_edit/enqueue_contextual_edit_image_command.rs @@ -1,3 +1,4 @@ +use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError}; use crate::core::commands::enqueue::image_edit::flux_kontext::handle_flux_kontext_edit::handle_flux_kontext_edit; use crate::core::commands::enqueue::image_edit::gemini_25_flash::handle_gemini_25_flash_edit::handle_gemini_25_flash_edit; @@ -194,6 +195,8 @@ pub async fn enqueue_contextual_edit_image_command( match result { Err(err) => { error!("Error enqueuing contextual edit image: {:?}", err); + + notify_frontend_of_errors(&app, &err).await; // TODO: Derive from err. Make service provider optional. let event = GenerationEnqueueFailureEvent { diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_inpaint/enqueue_image_inpaint_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_inpaint/enqueue_image_inpaint_command.rs index cbd415b2ea..b12716d369 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_inpaint/enqueue_image_inpaint_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_inpaint/enqueue_image_inpaint_command.rs @@ -1,3 +1,4 @@ +use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError}; use crate::core::commands::enqueue::image_edit::gpt_image_1::handle_gpt_image_1_edit::handle_gpt_image_1_edit; use crate::core::commands::enqueue::image_inpaint::flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint::handle_flux_dev_juggernaut_inpaint; @@ -183,6 +184,8 @@ pub async fn enqueue_image_inpaint_command( match result { Err(err) => { error!("Error enqueuing inpaint image: {:?}", err); + + notify_frontend_of_errors(&app, &err).await; // TODO: Derive from err. Make service provider optional. let event = GenerationEnqueueFailureEvent { diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/enqueue_image_to_3d_object_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/enqueue_image_to_3d_object_command.rs index a2d12cb69b..d4057b8f3a 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/enqueue_image_to_3d_object_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/enqueue_image_to_3d_object_command.rs @@ -1,3 +1,4 @@ +use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason}; use crate::core::commands::enqueue::image_to_object::generic::handle_object::handle_object; use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; @@ -96,6 +97,8 @@ pub async fn enqueue_image_to_3d_object_command( match result { Err(err) => { error!("error: {:?}", err); + + notify_frontend_of_errors(&app, &err).await; let mut status = CommandErrorStatus::ServerError; let mut error_type = EnqueueImageTo3dObjectErrorType::ServerError; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs index 7f55d40e19..fd92dce748 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs @@ -1,3 +1,4 @@ +use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason, ProviderFailureReason}; use crate::core::commands::enqueue::image_to_video::generic::handle_video::handle_video; use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; @@ -20,7 +21,7 @@ use serde_derive::{Deserialize, Serialize}; use tauri::{AppHandle, State}; use tokens::tokens::media_files::MediaFileToken; -/// This is used in the Tauri command bridge. +/// This is used in the Tauri command bridge. /// Don't change the serializations without coordinating with the frontend. #[derive(Deserialize, Debug, Copy, Clone)] #[serde(rename_all = "snake_case")] @@ -116,6 +117,8 @@ pub async fn enqueue_image_to_video_command( match result { Err(err) => { error!("error: {:?}", err); + + notify_frontend_of_errors(&app, &err).await; let mut status = CommandErrorStatus::ServerError; let mut error_type = EnqueueImageToVideoErrorType::ServerError; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs index 4c86458008..61a1f43923 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/enqueue_text_to_image_command.rs @@ -1,3 +1,4 @@ +use crate::core::commands::enqueue::common::notify_frontend_of_errors::notify_frontend_of_errors; use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError, MissingCredentialsReason}; use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; use crate::core::commands::enqueue::text_to_image::gemini_25_flash::handle_gemini_25_flash::handle_gemini_25_flash; @@ -173,6 +174,8 @@ pub async fn enqueue_text_to_image_command( Err(err) => { error!("error: {:?}", err); + notify_frontend_of_errors(&app, &err).await; + let mut status = CommandErrorStatus::ServerError; let mut error_type = EnqueueTextToImageErrorType::ServerError; let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem."; diff --git a/crates/desktop/artcraft/src/core/events/basic_sendable_event_trait.rs b/crates/desktop/artcraft/src/core/events/basic_sendable_event_trait.rs index 012c43e6f2..8eae34f14b 100644 --- a/crates/desktop/artcraft/src/core/events/basic_sendable_event_trait.rs +++ b/crates/desktop/artcraft/src/core/events/basic_sendable_event_trait.rs @@ -1,6 +1,6 @@ use crate::core::events::sendable_event_error::SendableEventError; use enums::tauri::ux::tauri_event_name::TauriEventName; -use log::info; +use log::{error, info}; use serde::Serialize; use std::fmt::Debug; use tauri::{AppHandle, Emitter}; @@ -49,4 +49,11 @@ pub trait BasicSendableEvent : Clone + Debug + Serialize { } Ok(()) } + + fn send_infallible(&self, app: &AppHandle) { + if let Err(err) = self.send(app) { + // Fail open + error!("Failed to send event `{}`: {:?}", Self::FRONTEND_EVENT_NAME.to_str(), err); + } + } } diff --git a/crates/desktop/artcraft/src/core/events/functional_events/mod.rs b/crates/desktop/artcraft/src/core/events/functional_events/mod.rs index 723590a391..46418dd6ed 100644 --- a/crates/desktop/artcraft/src/core/events/functional_events/mod.rs +++ b/crates/desktop/artcraft/src/core/events/functional_events/mod.rs @@ -1,5 +1,6 @@ pub mod canvas_background_removal_complete_event; pub mod image_edit_complete_event; pub mod refresh_account_state_event; +pub mod show_provider_billing_modal_event; pub mod show_provider_login_modal_event; pub mod text_to_image_generation_complete_event; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/events/functional_events/show_provider_billing_modal_event.rs b/crates/desktop/artcraft/src/core/events/functional_events/show_provider_billing_modal_event.rs new file mode 100644 index 0000000000..1aaa4b5102 --- /dev/null +++ b/crates/desktop/artcraft/src/core/events/functional_events/show_provider_billing_modal_event.rs @@ -0,0 +1,25 @@ +use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent}; +use enums::common::generation_provider::GenerationProvider; +use enums::tauri::ux::tauri_event_name::TauriEventName; +use serde_derive::Serialize; +use tauri::AppHandle; + +/// Send a signal to the frontend to show a modal that suggests fixing or setting up +/// service billing requirements. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct ShowProviderBillingModalEvent { + pub provider: GenerationProvider, +} + +impl ShowProviderBillingModalEvent { + pub fn send_for_provider(provider: GenerationProvider, app: &AppHandle) { + let event = Self { provider }; + event.send_infallible(&app); + } +} + +impl BasicSendableEvent for ShowProviderBillingModalEvent { + const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::ShowProviderBillingModalEvent; + const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Failure; +} diff --git a/crates/desktop/artcraft/src/core/events/functional_events/show_provider_login_modal_event.rs b/crates/desktop/artcraft/src/core/events/functional_events/show_provider_login_modal_event.rs index 2deb3af957..9e4726c367 100644 --- a/crates/desktop/artcraft/src/core/events/functional_events/show_provider_login_modal_event.rs +++ b/crates/desktop/artcraft/src/core/events/functional_events/show_provider_login_modal_event.rs @@ -1,8 +1,6 @@ use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent}; -use crate::core::events::sendable_event_error::SendableEventError; use enums::common::generation_provider::GenerationProvider; use enums::tauri::ux::tauri_event_name::TauriEventName; -use log::error; use serde_derive::Serialize; use tauri::AppHandle; @@ -16,9 +14,7 @@ pub struct ShowProviderLoginModalEvent { impl ShowProviderLoginModalEvent { pub fn send_for_provider(provider: GenerationProvider, app: &AppHandle) { let event = Self { provider }; - if let Err(err) = event.send(&app) { - error!("Failed to send ShowProviderLoginModalEvent: {:?}", err); // Fail open - } + event.send_infallible(&app); } } diff --git a/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs b/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs index dc2877b90d..d3a675d695 100644 --- a/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs +++ b/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs @@ -58,6 +58,11 @@ pub enum TauriEventName { #[serde(rename = "refresh_account_state_event")] RefreshAccountStateEvent, + /// Special event: + /// Show a billing modal (a suggestion to pay, etc. for a specific provider) + #[serde(rename = "show_provider_billing_modal_event")] + ShowProviderBillingModalEvent, + /// Special event: /// Show a login modal (or a suggestion to login) #[serde(rename = "show_provider_login_modal_event")] @@ -86,6 +91,7 @@ impl TauriEventName { Self::TextToImageGenerationCompleteEvent => "text_to_image_generation_complete_event", Self::ImageEditCompleteEvent => "image_edit_complete_event", Self::RefreshAccountStateEvent => "refresh_account_state_event", + Self::ShowProviderBillingModalEvent => "show_provider_billing_modal_event", Self::ShowProviderLoginModalEvent => "show_provider_login_modal_event", Self::FlashUserInputErrorEvent => "flash_user_input_error_event", } @@ -101,6 +107,7 @@ impl TauriEventName { "text_to_image_generation_complete_event" => Ok(Self::TextToImageGenerationCompleteEvent), "image_edit_complete_event" => Ok(Self::ImageEditCompleteEvent), "refresh_account_state_event" => Ok(Self::RefreshAccountStateEvent), + "show_provider_billing_modal_event" => Ok(Self::ShowProviderBillingModalEvent), "show_provider_login_modal_event" => Ok(Self::ShowProviderLoginModalEvent), "flash_user_input_error_event" => Ok(Self::FlashUserInputErrorEvent), _ => Err(format!("invalid tauri_event_name: {:?}", job_status)), @@ -119,6 +126,7 @@ impl TauriEventName { Self::TextToImageGenerationCompleteEvent, Self::ImageEditCompleteEvent, Self::RefreshAccountStateEvent, + Self::ShowProviderBillingModalEvent, Self::ShowProviderLoginModalEvent, Self::FlashUserInputErrorEvent, ]) @@ -143,6 +151,7 @@ mod tests { assert_serialization(TauriEventName::TextToImageGenerationCompleteEvent, "text_to_image_generation_complete_event"); assert_serialization(TauriEventName::ImageEditCompleteEvent, "image_edit_complete_event"); assert_serialization(TauriEventName::RefreshAccountStateEvent, "refresh_account_state_event"); + assert_serialization(TauriEventName::ShowProviderBillingModalEvent, "show_provider_billing_modal_event"); assert_serialization(TauriEventName::ShowProviderLoginModalEvent, "show_provider_login_modal_event"); assert_serialization(TauriEventName::FlashUserInputErrorEvent, "flash_user_input_error_event"); } @@ -157,6 +166,7 @@ mod tests { assert_eq!(TauriEventName::TextToImageGenerationCompleteEvent.to_str(), "text_to_image_generation_complete_event"); assert_eq!(TauriEventName::ImageEditCompleteEvent.to_str(), "image_edit_complete_event"); assert_eq!(TauriEventName::RefreshAccountStateEvent.to_str(), "refresh_account_state_event"); + assert_eq!(TauriEventName::ShowProviderBillingModalEvent.to_str(), "show_provider_billing_modal_event"); assert_eq!(TauriEventName::ShowProviderLoginModalEvent.to_str(), "show_provider_login_modal_event"); assert_eq!(TauriEventName::FlashUserInputErrorEvent.to_str(), "flash_user_input_error_event"); } @@ -171,6 +181,7 @@ mod tests { assert_eq!(TauriEventName::from_str("text_to_image_generation_complete_event").unwrap(), TauriEventName::TextToImageGenerationCompleteEvent); assert_eq!(TauriEventName::from_str("image_edit_complete_event").unwrap(), TauriEventName::ImageEditCompleteEvent); assert_eq!(TauriEventName::from_str("refresh_account_state_event").unwrap(), TauriEventName::RefreshAccountStateEvent); + assert_eq!(TauriEventName::from_str("show_provider_billing_modal_event").unwrap(), TauriEventName::ShowProviderBillingModalEvent); assert_eq!(TauriEventName::from_str("show_provider_login_modal_event").unwrap(), TauriEventName::ShowProviderLoginModalEvent); assert_eq!(TauriEventName::from_str("flash_user_input_error_event").unwrap(), TauriEventName::FlashUserInputErrorEvent); } @@ -178,7 +189,7 @@ mod tests { #[test] fn all_variants() { let mut variants = TauriEventName::all_variants(); - assert_eq!(variants.len(), 10); + assert_eq!(variants.len(), 11); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueSuccessEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueFailureEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationCompleteEvent)); @@ -187,6 +198,7 @@ mod tests { assert_eq!(variants.pop_first(), Some(TauriEventName::TextToImageGenerationCompleteEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::ImageEditCompleteEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::RefreshAccountStateEvent)); + assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderBillingModalEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderLoginModalEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::FlashUserInputErrorEvent)); assert_eq!(variants.pop_first(), None);