diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index 9ffbdd2..e65d82e 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -4573,6 +4573,18 @@ 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-barcode-scanner" version = "2.4.4" @@ -4834,6 +4846,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build 2.5.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tauri-plugin-app-events", "tauri-plugin-barcode-scanner", "tauri-plugin-deep-link", "tauri-plugin-opener", diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 7c2b00d..90b9504 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -50,6 +50,7 @@ default-features = false [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] tauri-plugin-barcode-scanner = "2" +tauri-plugin-app-events = "0.2" [patch.crates-io.tauri] git = "https://github.com/tauri-apps/tauri" diff --git a/apps/tauri/src-tauri/capabilities/mobile.json b/apps/tauri/src-tauri/capabilities/mobile.json index 580818d..83f76e0 100644 --- a/apps/tauri/src-tauri/capabilities/mobile.json +++ b/apps/tauri/src-tauri/capabilities/mobile.json @@ -9,6 +9,7 @@ ], "permissions": [ "deep-link:default", + "app-events:default", "barcode-scanner:default", "barcode-scanner:allow-scan", "barcode-scanner:allow-cancel" diff --git a/apps/tauri/src-tauri/gen/android/build.gradle.kts b/apps/tauri/src-tauri/gen/android/build.gradle.kts index 607240b..dddb93c 100644 --- a/apps/tauri/src-tauri/gen/android/build.gradle.kts +++ b/apps/tauri/src-tauri/gen/android/build.gradle.kts @@ -1,3 +1,5 @@ +import com.android.build.gradle.LibraryExtension + buildscript { repositories { google() @@ -16,7 +18,16 @@ allprojects { } } +subprojects { + afterEvaluate { + plugins.withId("com.android.library") { + extensions.configure("android") { + compileSdk = 36 + } + } + } +} + tasks.register("clean").configure { delete("build") } - diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index a17852d..9eef4be 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -206,6 +206,9 @@ pub fn run() { #[cfg(any(target_os = "ios", target_os = "android"))] let builder = builder.plugin(tauri_plugin_barcode_scanner::init()); + #[cfg(any(target_os = "ios", target_os = "android"))] + let builder = builder.plugin(tauri_plugin_app_events::init()); + if let Err(error) = builder .setup(|_app| { #[cfg(any(target_os = "linux", windows))] diff --git a/apps/web/package.json b/apps/web/package.json index b4c1c90..07bfd07 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,6 +42,7 @@ "tailwind-merge": "^3.5.0", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.2.4", + "tauri-plugin-app-events-api": "^0.2.0", "tw-animate-css": "^1.4.0", "zod": "^4.3.6" }, diff --git a/bun.lock b/bun.lock index 655de6c..5b4b4cd 100644 --- a/bun.lock +++ b/bun.lock @@ -76,6 +76,7 @@ "tailwind-merge": "^3.5.0", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.2.4", + "tauri-plugin-app-events-api": "^0.2.0", "tw-animate-css": "^1.4.0", "zod": "^4.3.6", }, @@ -228,6 +229,7 @@ "version": "0.0.0", "dependencies": { "@tanstack/react-router": "^1.0.0", + "@tauri-apps/api": "^2", "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", @@ -235,6 +237,7 @@ "@tensamin/ui": "*", "react": "^19.2.0", "react-dom": "^19.2.0", + "tauri-plugin-app-events-api": "^0.2.0", "zod": "^4.3.6", }, "devDependencies": { @@ -1497,6 +1500,8 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "tauri-plugin-app-events-api": ["tauri-plugin-app-events-api@0.2.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.3" } }, "sha512-CnlAeucWhT+Rgx6CCsqUpq2D3pqgx0fhjSPKomH7O2t1rGVu5kGLMkYvaCX6gqVTUYpIWX5BLV7LGY2DRri0QA=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], diff --git a/package.json b/package.json index 3b9f235..40e19a8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "dev:mobile": "cd apps/tauri && bun dev:mobile", "build:mobile": "cd apps/tauri && bun run build:mobile", "dev:desktop": "cd apps/tauri && bun dev:desktop", - "build:desktop": "cd apps/tauri && bun run build:desktop" + "build:desktop": "cd apps/tauri && bun run build:desktop", + "delete:mobile": "cd apps/tauri && nix develop --command adb uninstall net.tensamin.client" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/packages/chat/src/screen.tsx b/packages/chat/src/screen.tsx index 37119b8..a23760d 100644 --- a/packages/chat/src/screen.tsx +++ b/packages/chat/src/screen.tsx @@ -431,7 +431,7 @@ export default function Screen() { } return ( -
+
; subscribePush: (handler: PushHandler) => () => void; @@ -186,7 +192,6 @@ export function Provider(props: { typeof createTransportClient > | null>(null); const identificationStartedRef = useRef(false); - const reconnectExhaustedRef = useRef(false); /** * Sends typed protocol messages through the active transport client. @@ -266,6 +271,8 @@ export function Provider(props: { let reconnectResetTimer: ReturnType | null = null; let reconnectScheduled = false; let disposed = false; + let resumeListenerRegistered = false; + let currentReadyState: number = READY_STATE.CLOSED; /** * Clears any scheduled reconnect timeout and resets scheduling flags. @@ -316,14 +323,12 @@ export function Provider(props: { return; } - if (reconnectExhaustedRef.current || attempts >= RECONNECT_TRIES) { - reconnectExhaustedRef.current = true; + if (attempts >= RECONNECT_TRIES) { setError("Connection Failed"); setErrorDescription( "Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.", ); log(0, "ttp", "red", "Reconnection attempts exhausted", reason); - void transportClient?.close("reconnect-exhausted"); return; } @@ -340,12 +345,12 @@ export function Provider(props: { ? createTransportClient(schemas, { url: TRANSPORT_URL, onReadyStateChange: (state) => { + currentReadyState = state; setReadyState(state); if (state === READY_STATE.OPEN) { clearReconnectTimer(); scheduleReconnectReset(); - reconnectExhaustedRef.current = false; identificationStartedRef.current = false; setConnected(true); setIdentified(false); @@ -383,7 +388,7 @@ export function Provider(props: { * @returns Promise that resolves after one connection attempt. */ async function connect() { - if (disposed || reconnectExhaustedRef.current) { + if (disposed) { return; } @@ -399,18 +404,56 @@ export function Provider(props: { } } + /** + * Starts a reconnect after resume only when the transport is disconnected. + * @returns Promise that resolves after any needed resume reconnect starts. + */ + async function reconnectAfterResume() { + if (disposed || !transportClient) { + return; + } + + if ( + currentReadyState === READY_STATE.OPEN || + currentReadyState === READY_STATE.CONNECTING + ) { + return; + } + + clearReconnectTimer(); + clearReconnectResetTimer(); + attempts = 0; + reconnectScheduled = false; + setError(""); + setErrorDescription(""); + + await connect(); + } + void connect(); + if (!props.blockConnection && isTauriMobile()) { + resumeListenerRegistered = true; + onResume(() => { + void reconnectAfterResume(); + }); + } + return () => { disposed = true; clearReconnectTimer(); clearReconnectResetTimer(); + if (resumeListenerRegistered) { + onResume(); + } + if (clientRef.current === transportClient) { clientRef.current = null; } void transportClient?.close("context-dispose"); + currentReadyState = READY_STATE.CLOSED; setReadyState(READY_STATE.CLOSED); setConnected(false); setIdentified(false);