notification hook

This commit is contained in:
Brandon Thomas committed 2025-08-30 19:01:25 -04:00
1 parent 77ec1fb43a
commit de4a70009d
14 files changed
+136 -8

No files matched your search

@@ -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);
}
@@ -0,0 +1,2 @@
pub mod notify_frontend_of_errors;
mod maybe_notify_frontend_of_billing_errors;
@@ -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;
}
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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;
@@ -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;
@@ -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.";
@@ -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);
}
}
}
@@ -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;
@@ -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;
}
@@ -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);
}
}
@@ -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);