# @fusion/mobile ## Native Shell Onboarding & Remote Connections Mobile uses a shell-level onboarding flow for first-run connection setup before dashboard onboarding. - **Remote-first flow:** mobile onboarding goes directly to remote server connection. - **Connection setup options:** QR scan (`startQrScan`) or manual server URL entry, with optional auth token. - **Saved profiles:** multiple remote profiles are persisted in shell-local storage and can be added via QR/manual entry, edited, switched, and deleted later from dashboard connection management. - **Active-profile fallback:** deleting the active profile automatically promotes the first remaining profile; deleting the last profile resets to an empty state (`activeProfileId: null`, `profiles: []`) so onboarding/manager recovery can reopen cleanly. - **Storage boundary:** profile/mode state is stored only in mobile shell-local storage (via native plugin wrappers), not in Fusion project settings/local dashboard project storage. - **Bridge contract:** mobile exposes `window.fusionShell` (`getState`, `listProfiles`, `saveProfile`, `deleteProfile`, `setActiveProfile`, `startQrScan`, `openConnectionManager`, `subscribe`) so shared dashboard code can run host-neutrally. - **Dashboard-safe capability contract:** shared dashboard helpers should consume the typed `MobileShellDashboardBridge` subset (`getState?`, `openConnectionManager?`). If either function is missing at runtime, treat connection-management as unsupported instead of throwing. Native wrappers are isolated under `src/plugins/native-shell.ts`, `src/plugins/connection-profiles.ts`, and `src/plugins/qr-scanner.ts` so dashboard code never calls vendor-specific APIs directly. ## Android Back: hardware button and predictive-back GESTURE both dismiss task detail `AndroidBackButtonManager` (`src/plugins/native-shell.ts`) subscribes to `@capacitor/app`'s `backButton` event and dispatches a single cancelable `fusion:native-back` browser event. The dashboard's shared nav-history stack (`useNavigationHistory`) consumes that event to dismiss the top task-detail surface (board main-panel, list-mobile, modal, and nested detail) and return to the board/previous detail — the exact same invariant the browser `popstate`/swipe-back path already uses. If nothing handles the event (`!defaultPrevented`), Capacitor's own fallback runs unchanged: `history.back()` when `canGoBack`, else `exitApp()`. **FN-7583:** the Android system back **gesture** (predictive back / edge swipe, Android 13+, default-prominent at this project's `targetSdk 35`) previously did not reach that listener, even though the hardware/legacy Back button did — the app never opted the generated manifest into Android's predictive-back framework (`android:enableOnBackInvokedCallback="true"`), so `OnBackPressedDispatcher` silently dropped the gesture-completion callback while still delivering button presses via the ordinary `Window`/`Activity` path. Because the native `android/` project is generated by `cap sync` and is git-ignored, the fix lives in tracked source: `scripts/patch-android-manifest.ts` idempotently adds that manifest attribute to the generated `AndroidManifest.xml`, and is wired into Capacitor's own `capacitor:sync:after` npm-script hook (see `package.json`) so it runs automatically on every `cap sync` — including `build:mobile`, `dev:android`, and `cap run android` (which syncs first). No dashboard-side change was needed: once the gesture reaches the native `backButton` listener, gesture and button converge on the identical `fusion:native-back` dispatch. Manual invocation (e.g. after a bare `npx cap sync` outside `build:mobile`): ```bash pnpm --filter @fusion/mobile patch:android-manifest ``` ### Mobile task-detail predictive-back transition (FN-7587) FN-7583 (Android back-gesture parity) and FN-7586 (iOS edge-swipe-back parity) made native "back" gestures **functionally** dismiss Fusion's mobile task-detail surfaces (board main-panel, list-mobile, modal, and nested detail) through the dashboard's shared nav-history invariant (`useNavigationHistory` / `popstate` / `fusion:native-back`). FN-7587 layers a **presentation-only** slide/fade transition on top of that unchanged routing: - Mobile/native-only — the transition is gated to the mobile viewport (`<= 768px`, matching the existing `isMobile`/`OVERSIGHT_MENU_MOBILE_BREAKPOINT` convention in the dashboard); desktop task-detail never receives the animation class. - Non-interactive — the transition is a short CSS `@keyframes` slide/fade (~200ms) triggered by mount/prop-state change, not by gesture progress. It does **not** intercept, delay, or reorder when the `useNavigationHistory` pop / `fusion:native-back` / empty-stack fallback fires; the animation is purely a CSS class applied to the already-real DOM node. - Honors `prefers-reduced-motion: reduce` (neutralizes to an instant, transform-free show), mirroring the dashboard's existing reduced-motion convention (`WorkflowSwitcher.css`, `TopProgressBar.css`). - **Interactive predictive-back is not implemented** and is not feasible today from a Capacitor single-page WebView on either platform: iOS's `allowsBackForwardNavigationGestures` gesture (used by FN-7586) exposes only a discrete `popstate` on commit, with no interactive-progress callback reachable from JS; Android's OS-owned predictive-back preview animates outside the single-Activity WebView and is not driveable from in-page DOM. A follow-up task is filed to revisit this if/when platform APIs expose gesture-progress callbacks to JS. Implementation lives entirely in the dashboard package (`packages/dashboard/app/styles.css`, `packages/dashboard/app/components/TaskDetailModal.css`, `MainContent.tsx`, `TaskDetailModal.tsx`) — no mobile-shell-native code changes were required for this task. ### Regression coverage locked by tests `packages/mobile/src/__tests__/connection-profiles.test.ts`, `native-shell.test.ts`, and `qr-scanner.test.ts` now lock these contracts: - first-run remote setup via QR/manual payloads (including optional auth token handling) - saved-profile edit, active-profile switching, and persisted-state restore across module reinit/relaunch - bridge reads (`getState`, `listProfiles`) plus connection-manager event dispatch - malformed/empty QR payload handling and unavailable-scanner fallback behavior ## Native Back Handling (Android Back + iOS Edge-Swipe-Back) Mobile task-detail dismissal converges on a SINGLE shared invariant regardless of how the user triggers "back": the dashboard's `useNavigationHistory` nav-history stack (`packages/dashboard/app/hooks/useNavigationHistory.ts`), which board main-panel, list-mobile, modal, and nested task-detail surfaces already consume for desktop/browser swipe-back via ordinary `popstate`. - **Android hardware Back button + gesture (FN-7583):** `AndroidBackButtonManager` in `src/plugins/native-shell.ts` listens for `@capacitor/app`'s `backButton` event and dispatches a cancelable `fusion:native-back` custom event; `useNavigationHistory` consumes it to dismiss the top task-detail surface (falling back to native browser-history `back()` or app exit when Fusion owns no nav entry). The Android 13+ predictive-back **gesture** additionally requires the generated `AndroidManifest.xml` to opt into `android:enableOnBackInvokedCallback="true"` for AndroidX's dispatcher to route the gesture-completion callback to the same `backButton` listener — since `android/` is generated by `cap sync` and git-ignored, this opt-in lives in a tracked post-`cap sync` patch (`scripts/patch-android-manifest.ts`), wired into the `capacitor:sync:after` npm hook. - **iOS edge-swipe-back (FN-7586):** iOS has **no** Capacitor `backButton` event, so it does NOT go through `fusion:native-back`. Instead, the WKWebView edge-swipe-back gesture (once enabled) drives ordinary back-forward navigation, which the browser delivers as a `popstate` — the exact same seam desktop/browser swipe-back already uses, and that `useNavigationHistory.handlePopState` already consumes correctly. The gesture itself is OFF by default: `WKWebView.allowsBackForwardNavigationGestures` defaults to `false`, Capacitor's `CAPBridgeViewController` never sets it, and no `capacitor.config.ts` `ios` toggle exists for it. Since `ios/` is also generated by `cap sync` and git-ignored, this is enabled via a tracked post-`cap sync` patch (`scripts/patch-ios-webview.ts`) that idempotently patches the generated `AppDelegate.swift` to set `webView?.allowsBackForwardNavigationGestures = true` once the storyboard's root `CAPBridgeViewController` has loaded — also wired into the same `capacitor:sync:after` npm hook. Empty-stack behavior (no Fusion nav entry) is unaffected: the gesture just performs ordinary WKWebView history navigation with no forced trap/exit. Both patch scripts are safe no-ops when their respective native platform directory (`android/` / `ios/`) hasn't been added yet, and are idempotent across repeated `cap sync` runs (see `src/__tests__/native-shell.test.ts`). ## Push Notifications `PushNotificationManager` supports two complementary notification channels: 1. **Native push notifications** via Capacitor Push Notifications (`@capacitor/push-notifications`) for FCM/APNs token registration and notification tap handling. 2. **ntfy.sh streaming subscription** via polling-driven topic management, so the app can receive in-app notifications without server-side FCM/APNs setup. ### Initialization ```ts import { PushNotificationManager } from "@fusion/mobile"; const manager = new PushNotificationManager({ settingsFetcher: fetchGlobalSettings, }); await manager.start(); ``` You can also initialize through `initializePlugins({ pushNotifications: { ... } })` if you want plugin bootstrapping from a single entrypoint. ### Event API ```ts manager.on("notification:tapped", ({ taskId }) => { if (taskId) { navigateToTask(taskId); } }); manager.on("notification:received", ({ title, body }) => { console.log("Foreground notification", title, body); }); manager.on("ntfy:message", ({ taskId, message }) => { console.log("ntfy message", taskId, message); }); ``` ### ntfy.sh Integration Behavior When `settingsFetcher()` returns: - `ntfyEnabled: true` - `ntfyTopic: ""` …the manager starts (or switches) a live subscription to `{ntfyBaseUrl}/{topic}/json`. If settings disable ntfy or clear the topic, the subscription is automatically stopped. ### Device Token Access Use `manager.getDeviceToken()` after registration to retrieve the native device token for future server-side FCM/APNs integration work. ### Out of Scope This package currently handles **receiving** push notifications and in-app routing events only. Server-side FCM/APNs delivery infrastructure (token storage, provider credentials, push sending services) is intentionally out of scope for this feature. ## Native Sharing & Deep Links ### ShareManager `ShareManager` opens platform-native sharing when available and always includes a Fusion deep link in the shared payload. ```ts import { ShareManager } from "@fusion/mobile"; const manager = new ShareManager(); await manager.initialize(); await manager.shareTask({ id: "FN-1118", title: "Mobile Plugins - Native Sharing & Deep Links", description: "Implements native share sheet support and deep link parsing.", }); ``` #### Share behavior + fallbacks - Builds a payload with: - `title`: `task.title` or fallback `Task {id}` - `text`: task description (truncated to 200 chars with `...` when needed) - `url`: `${deepLinkBaseUrl}{task.id}` (default base: `fusion://task/`) - **Native (Capacitor)**: uses `@capacitor/share` - **Web fallback**: uses `navigator.share(...)` when available - **Final fallback**: copies the deep-link URL to `navigator.clipboard.writeText(...)` #### Share events - `share:success` → `{ taskId }` - `share:cancelled` → `{ taskId }` - `share:error` → `{ taskId, error }` ### DeepLinkManager `DeepLinkManager` handles incoming links and emits parsed payloads for app-level navigation. ```ts import { DeepLinkManager } from "@fusion/mobile"; const deepLinks = new DeepLinkManager({ scheme: "fusion://", universalLinkHosts: ["app.fusion.dev"], }); await deepLinks.initialize(); deepLinks.on("deeplink:received", (payload) => { // route to screen/task/project in app UI console.log(payload); }); ``` #### Supported URL patterns - `fusion://task/{taskId}` - `fusion://project/{projectId}` - `fusion://project/{projectId}/task/{taskId}` - `fusion://settings` - `fusion://agents` - Query params are preserved in `payload.params` for custom-scheme links Universal links are supported when the host is allowed in `universalLinkHosts`, e.g.: - `https://app.fusion.dev/?task=FN-123` - `https://app.fusion.dev/?project=my-project&task=FN-123&target=task` #### Deep link events - `deeplink:received` → parsed `DeepLinkPayload` - `deeplink:error` → `{ url, error }` Use `handleUrl(url)` for programmatic handling (for example, push-notification tap flows that already provide a URL string). ### Integration flow: share -> open -> navigate A common flow is: 1. Use `ShareManager.shareTask(...)` to share a task link like `fusion://task/FN-123` 2. Recipient opens that link on mobile 3. `DeepLinkManager` receives/parses the URL 4. Your UI listens to `deeplink:received` and navigates to the matching task view ### Capacitor deep-link scheme registration The Fusion mobile app registers the custom URL scheme in `packages/dashboard/capacitor.config.ts`: - `server.iosScheme = "fusion"` - `server.androidScheme = "fusion"` ### Browser hash listener (development/testing) On non-native platforms, `DeepLinkManager` listens for hash changes in the form: - `#deeplink=` This hash-based behavior is intended for development/testing only and is not a production universal-link replacement.