From 88b1305462e0f814b1b599858f7106d532cc3029 Mon Sep 17 00:00:00 2001 From: Peter Chinman Date: Fri, 30 Jan 2026 16:29:54 -0500 Subject: [PATCH 1/4] fix[iOS]: app crash on open --- js/app/packages/app/component/Soup.tsx | 3 +- js/app/tauri/Cargo.lock | 13 --- js/app/tauri/src-tauri/Cargo.toml | 1 - .../tauri/src-tauri/capabilities/mobile.json | 3 +- .../apple/Sources/app/AppLifecycleObserver.mm | 26 +++++ js/app/tauri/src-tauri/src/lib.rs | 98 ++++++++++--------- 6 files changed, 83 insertions(+), 61 deletions(-) create mode 100644 js/app/tauri/src-tauri/gen/apple/Sources/app/AppLifecycleObserver.mm diff --git a/js/app/packages/app/component/Soup.tsx b/js/app/packages/app/component/Soup.tsx index 78bbea4e21e..8c5131ceb7b 100644 --- a/js/app/packages/app/component/Soup.tsx +++ b/js/app/packages/app/component/Soup.tsx @@ -81,6 +81,7 @@ import { ShortcutLabel, } from './Soup/components/FilterButton'; import { SortDropdown } from './Soup/components/SortDropdown'; +import { isMobile } from '@core/mobile/isMobile'; false && fileFolderDrop; @@ -778,7 +779,7 @@ export function Soup() { /> - + diff --git a/js/app/tauri/Cargo.lock b/js/app/tauri/Cargo.lock index 5c296bee64e..a35302a8c52 100644 --- a/js/app/tauri/Cargo.lock +++ b/js/app/tauri/Cargo.lock @@ -90,7 +90,6 @@ dependencies = [ "serde_qs", "tauri", "tauri-build", - "tauri-plugin-app-events", "tauri-plugin-deep-link", "tauri-plugin-haptics", "tauri-plugin-http", @@ -4527,18 +4526,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "tauri-plugin-app-events" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e625f35abcfc422c7c4af8b348d2069cdf387a39c12429fdce570137d02f069" -dependencies = [ - "serde", - "tauri", - "tauri-plugin", - "thiserror 1.0.69", -] - [[package]] name = "tauri-plugin-deep-link" version = "2.4.5" diff --git a/js/app/tauri/src-tauri/Cargo.toml b/js/app/tauri/src-tauri/Cargo.toml index 0f4c29543ff..2f29b1046b1 100644 --- a/js/app/tauri/src-tauri/Cargo.toml +++ b/js/app/tauri/src-tauri/Cargo.toml @@ -59,5 +59,4 @@ tauri-plugin-virtual-keyboard = { git = "https://github.com/voxelbee/tauri-plugi tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -tauri-plugin-app-events = "0.2" tauri-plugin-haptics = "2" diff --git a/js/app/tauri/src-tauri/capabilities/mobile.json b/js/app/tauri/src-tauri/capabilities/mobile.json index c7bbb912f69..91e227a8f4a 100644 --- a/js/app/tauri/src-tauri/capabilities/mobile.json +++ b/js/app/tauri/src-tauri/capabilities/mobile.json @@ -12,7 +12,6 @@ "haptics:allow-notification-feedback", "haptics:allow-selection-feedback", "haptics:allow-vibrate", - "log:default", - "app-events:default" + "log:default" ] } \ No newline at end of file diff --git a/js/app/tauri/src-tauri/gen/apple/Sources/app/AppLifecycleObserver.mm b/js/app/tauri/src-tauri/gen/apple/Sources/app/AppLifecycleObserver.mm new file mode 100644 index 00000000000..a4f638bd9a6 --- /dev/null +++ b/js/app/tauri/src-tauri/gen/apple/Sources/app/AppLifecycleObserver.mm @@ -0,0 +1,26 @@ +#import + +extern "C" void on_app_resumed(void); + +@interface AppLifecycleObserver : NSObject +@end + +@implementation AppLifecycleObserver + ++ (void)load { + __block BOOL isFirstActivation = YES; + + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification * _Nonnull note) { + if (isFirstActivation) { + isFirstActivation = NO; + return; + } + on_app_resumed(); + }]; +} + +@end diff --git a/js/app/tauri/src-tauri/src/lib.rs b/js/app/tauri/src-tauri/src/lib.rs index b9544aca846..9579b6e46cb 100644 --- a/js/app/tauri/src-tauri/src/lib.rs +++ b/js/app/tauri/src-tauri/src/lib.rs @@ -6,6 +6,8 @@ use reqwest::header::COOKIE; use rootcause::{Report, report}; use serde::Serialize; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +#[cfg(target_os = "ios")] +use std::sync::OnceLock; use tauri::http::{HeaderMap, HeaderValue}; use tauri::{AppHandle, Emitter}; use tauri::{Manager, Runtime}; @@ -29,6 +31,56 @@ fn heartbeat_response(state: tauri::State<'_, HeartbeatState>) { #[cfg(debug_assertions)] // do not remove this mod debug; +#[cfg(target_os = "ios")] +static GLOBAL_APP_HANDLE: OnceLock = OnceLock::new(); + +/// Send a heartbeat ping to the JS layer and check for a response after 1 second. +/// If no response is received, reload the webview (the content process is likely dead). +#[cfg(target_os = "ios")] +fn send_heartbeat(handle: &AppHandle) { + tracing::info!("app resumed, sending heartbeat ping"); + + let state = handle.state::(); + let current_gen = state.generation.fetch_add(1, Ordering::SeqCst) + 1; + state.alive.store(false, Ordering::SeqCst); + + let _ = handle.emit("heartbeat_ping", ()); + + let handle = handle.clone(); + tauri::async_runtime::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + let state = handle.state::(); + + if state.generation.load(Ordering::SeqCst) != current_gen { + tracing::debug!("heartbeat: stale generation {current_gen}, skipping"); + return; + } + + if !state.alive.load(Ordering::SeqCst) { + tracing::warn!( + "heartbeat: no response from JS — content process likely dead, reloading webview" + ); + if let Some(webview) = handle.webview_windows().values().next() { + let _ = webview.reload(); + } + } else { + tracing::info!("heartbeat: JS responded, content process alive"); + } + }); +} + +/// Called from native Objective-C when the iOS app resumes from background. +/// See `AppLifecycleObserver.mm` for the notification observer. +#[cfg(target_os = "ios")] +#[unsafe(no_mangle)] +extern "C" fn on_app_resumed() { + let Some(handle) = GLOBAL_APP_HANDLE.get() else { + tracing::warn!("on_app_resumed: app handle not yet initialized"); + return; + }; + send_heartbeat(handle); +} + /// domains which the tauri webview can render. /// This should be as restrictive as possible. /// If the webview attempts to naviate to other domains, @@ -145,51 +197,9 @@ pub fn run() { app.chain(attach_deep_link_handler); - #[cfg(mobile)] + #[cfg(target_os = "ios")] { - use tauri::ipc::Channel; - use tauri_plugin_app_events::AppEventsExt; - - let app_handle = app.handle().clone(); - app_handle.plugin(tauri_plugin_app_events::init())?; - - let handle = app_handle.clone(); - app_handle - .app_events() - .set_resume_handler(Channel::new(move |_| { - tracing::info!("app resumed, sending heartbeat ping"); - - let state = handle.state::(); - let current_gen = state.generation.fetch_add(1, Ordering::SeqCst) + 1; - state.alive.store(false, Ordering::SeqCst); - - let _ = handle.emit("heartbeat_ping", ()); - - let handle = handle.clone(); - tokio::spawn(async move { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - let state = handle.state::(); - - // A newer resume has superseded this one; bail out - if state.generation.load(Ordering::SeqCst) != current_gen { - tracing::debug!("heartbeat: stale generation {current_gen}, skipping"); - return; - } - - if !state.alive.load(Ordering::SeqCst) { - tracing::warn!( - "heartbeat: no response from JS — content process likely dead, reloading webview" - ); - if let Some(webview) = handle.webview_windows().values().next() { - let _ = webview.reload(); - } - } else { - tracing::info!("heartbeat: JS responded, content process alive"); - } - }); - - Ok(()) - }))?; + let _ = GLOBAL_APP_HANDLE.set(app.handle().clone()); } Ok(()) From b133bd2c7f0ec3d362fcb7aadf0d6797b11d9fe3 Mon Sep 17 00:00:00 2001 From: Peter Chinman Date: Fri, 30 Jan 2026 17:30:38 -0500 Subject: [PATCH 2/4] needs to be in main.mm to compile --- .../src-tauri/gen/apple/Sources/app/main.mm | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/js/app/tauri/src-tauri/gen/apple/Sources/app/main.mm b/js/app/tauri/src-tauri/gen/apple/Sources/app/main.mm index 7793a9d5cfa..025a959103b 100644 --- a/js/app/tauri/src-tauri/gen/apple/Sources/app/main.mm +++ b/js/app/tauri/src-tauri/gen/apple/Sources/app/main.mm @@ -1,4 +1,30 @@ #include "bindings/bindings.h" +#import + +extern "C" void on_app_resumed(void); + +@interface AppLifecycleObserver : NSObject +@end + +@implementation AppLifecycleObserver + ++ (void)load { + __block BOOL isFirstActivation = YES; + + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationDidBecomeActiveNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification * _Nonnull note) { + if (isFirstActivation) { + isFirstActivation = NO; + return; + } + on_app_resumed(); + }]; +} + +@end int main(int argc, char * argv[]) { ffi::start_app(); From db941fa7f66ffdc4b46710795b857ae18ddf726b Mon Sep 17 00:00:00 2001 From: Peter Chinman Date: Fri, 30 Jan 2026 17:34:46 -0500 Subject: [PATCH 3/4] remove duplicate code --- .../apple/Sources/app/AppLifecycleObserver.mm | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 js/app/tauri/src-tauri/gen/apple/Sources/app/AppLifecycleObserver.mm diff --git a/js/app/tauri/src-tauri/gen/apple/Sources/app/AppLifecycleObserver.mm b/js/app/tauri/src-tauri/gen/apple/Sources/app/AppLifecycleObserver.mm deleted file mode 100644 index a4f638bd9a6..00000000000 --- a/js/app/tauri/src-tauri/gen/apple/Sources/app/AppLifecycleObserver.mm +++ /dev/null @@ -1,26 +0,0 @@ -#import - -extern "C" void on_app_resumed(void); - -@interface AppLifecycleObserver : NSObject -@end - -@implementation AppLifecycleObserver - -+ (void)load { - __block BOOL isFirstActivation = YES; - - [[NSNotificationCenter defaultCenter] - addObserverForName:UIApplicationDidBecomeActiveNotification - object:nil - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification * _Nonnull note) { - if (isFirstActivation) { - isFirstActivation = NO; - return; - } - on_app_resumed(); - }]; -} - -@end From 43da491c05c888f2606acb2bb83439787d8b1603 Mon Sep 17 00:00:00 2001 From: Peter Chinman Date: Fri, 30 Jan 2026 17:39:49 -0500 Subject: [PATCH 4/4] update documentation --- js/app/tauri/src-tauri/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/app/tauri/src-tauri/src/lib.rs b/js/app/tauri/src-tauri/src/lib.rs index 9579b6e46cb..39b380f0500 100644 --- a/js/app/tauri/src-tauri/src/lib.rs +++ b/js/app/tauri/src-tauri/src/lib.rs @@ -70,7 +70,7 @@ fn send_heartbeat(handle: &AppHandle) { } /// Called from native Objective-C when the iOS app resumes from background. -/// See `AppLifecycleObserver.mm` for the notification observer. +/// See `main.mm` for the notification observer. #[cfg(target_os = "ios")] #[unsafe(no_mangle)] extern "C" fn on_app_resumed() {