Files
fusion/packages/mobile
dependabot[bot] bcf353de48 chore(deps): bump @capacitor/core from 7.6.1 to 8.5.0 (#3467)
Bumps [@capacitor/core](https://github.com/ionic-team/capacitor) from
7.6.1 to 8.5.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/capacitor/releases">@​capacitor/core's
releases</a>.</em></p>
<blockquote>
<h2>8.5.0</h2>
<h1><a
href="https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0">8.5.0</a>
(2026-07-31)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> support TypeScript 7 when loading
capacitor.config.ts (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8534">#8534</a>)
(<a
href="4c1c870941">4c1c870</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li><strong>cli:</strong> add migrator functionality for adopting
UIScene (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8544">#8544</a>)
(<a
href="984fa85ba0">984fa85</a>)</li>
<li><strong>ios:</strong> UIScene Support (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8536">#8536</a>)
(<a
href="3fa04a357c">3fa04a3</a>)</li>
</ul>
<h2>8.4.2</h2>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2">8.4.2</a>
(2026-07-14)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> explicitly grant URI permissions for image
capture intent (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8526">#8526</a>)
(<a
href="6f2d328389">6f2d328</a>)</li>
</ul>
<h2>8.4.1</h2>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1">8.4.1</a>
(2026-06-19)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> make SPM dependency patch work on prereleases
(<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8508">#8508</a>)
(<a
href="6048e90171">6048e90</a>)</li>
<li><strong>cli:</strong> patch Capacitor SPM dependency version in
plugins (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8492">#8492</a>)
(<a
href="28bb2c6870">28bb2c6</a>)</li>
</ul>
<h2>8.4.0</h2>
<h1><a
href="https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0">8.4.0</a>
(2026-06-02)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> show only the requested system bar (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8480">#8480</a>)
(<a
href="4c6c3219af">4c6c321</a>)</li>
<li><strong>cli:</strong> revert live reload config on failure (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8485">#8485</a>)
(<a
href="1d031a4abe">1d031a4</a>)</li>
<li><strong>SystemBars:</strong> make <code>safe-area-inset-x</code>
available on API &lt;= 34 (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8424">#8424</a>)
(<a
href="e456de083e">e456de0</a>)</li>
<li><strong>SystemBars:</strong> respect <code>insetsHandling</code>
disable (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8481">#8481</a>)
(<a
href="d4ad7ffe39">d4ad7ff</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>add method getDouble to plugin config (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/7638">#7638</a>)
(<a
href="93c72de40a">93c72de</a>)</li>
<li><strong>cli:</strong> add experimental packageOptions (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8471">#8471</a>)
(<a
href="258867b7bf">258867b</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/capacitor/blob/main/CHANGELOG.md">@​capacitor/core's
changelog</a>.</em></p>
<blockquote>
<h1><a
href="https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0">8.5.0</a>
(2026-07-31)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> support TypeScript 7 when loading
capacitor.config.ts (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8534">#8534</a>)
(<a
href="4c1c870941">4c1c870</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li><strong>cli:</strong> add migrator functionality for adopting
UIScene (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8544">#8544</a>)
(<a
href="984fa85ba0">984fa85</a>)</li>
<li><strong>ios:</strong> UIScene Support (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8536">#8536</a>)
(<a
href="3fa04a357c">3fa04a3</a>)</li>
</ul>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.4.1...8.4.2">8.4.2</a>
(2026-07-14)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> explicitly grant URI permissions for image
capture intent (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8526">#8526</a>)
(<a
href="6f2d328389">6f2d328</a>)</li>
</ul>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.4.0...8.4.1">8.4.1</a>
(2026-06-19)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> make SPM dependency patch work on prereleases
(<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8508">#8508</a>)
(<a
href="6048e90171">6048e90</a>)</li>
<li><strong>cli:</strong> patch Capacitor SPM dependency version in
plugins (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8492">#8492</a>)
(<a
href="28bb2c6870">28bb2c6</a>)</li>
</ul>
<h1><a
href="https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0">8.4.0</a>
(2026-06-02)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>android:</strong> show only the requested system bar (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8480">#8480</a>)
(<a
href="4c6c3219af">4c6c321</a>)</li>
<li><strong>cli:</strong> revert live reload config on failure (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8485">#8485</a>)
(<a
href="1d031a4abe">1d031a4</a>)</li>
<li><strong>SystemBars:</strong> make <code>safe-area-inset-x</code>
available on API &lt;= 34 (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8424">#8424</a>)
(<a
href="e456de083e">e456de0</a>)</li>
<li><strong>SystemBars:</strong> respect <code>insetsHandling</code>
disable (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8481">#8481</a>)
(<a
href="d4ad7ffe39">d4ad7ff</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>add method getDouble to plugin config (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/7638">#7638</a>)
(<a
href="93c72de40a">93c72de</a>)</li>
<li><strong>cli:</strong> add experimental packageOptions (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8471">#8471</a>)
(<a
href="258867b7bf">258867b</a>)</li>
<li><strong>cli:</strong> capture ios_package_manager in telemetry (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8482">#8482</a>)
(<a
href="b4b297a52f">b4b297a</a>)</li>
</ul>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.3.3...8.3.4">8.3.4</a>
(2026-05-12)</h2>
<p><strong>Note:</strong> Version bump only for package capacitor</p>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.3.2...8.3.3">8.3.3</a>
(2026-05-08)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>cli:</strong> copy plugin files in CocoaPods projects (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8467">#8467</a>)
(<a
href="b2d771926a">b2d7719</a>)</li>
</ul>
<h2><a
href="https://github.com/ionic-team/capacitor/compare/8.3.1...8.3.2">8.3.2</a>
(2026-05-07)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3ab4139bd0"><code>3ab4139</code></a>
Release 8.5.0</li>
<li><a
href="984fa85ba0"><code>984fa85</code></a>
feat(cli): add migrator functionality for adopting UIScene (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8544">#8544</a>)</li>
<li><a
href="3fa04a357c"><code>3fa04a3</code></a>
feat(ios): UIScene Support (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8536">#8536</a>)</li>
<li><a
href="4c1c870941"><code>4c1c870</code></a>
fix(cli): support TypeScript 7 when loading capacitor.config.ts (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8534">#8534</a>)</li>
<li><a
href="f368a1b1ec"><code>f368a1b</code></a>
chore(android): fix lint issues (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8542">#8542</a>)</li>
<li><a
href="1834b9740e"><code>1834b97</code></a>
Release 8.4.2</li>
<li><a
href="6f2d328389"><code>6f2d328</code></a>
fix(android): explicitly grant URI permissions for image capture intent
(<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8526">#8526</a>)</li>
<li><a
href="b789b683f0"><code>b789b68</code></a>
chore: run <code>npm run fmt</code> to fix lint errors (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8516">#8516</a>)</li>
<li><a
href="7217b5215a"><code>7217b52</code></a>
Release 8.4.1</li>
<li><a
href="6048e90171"><code>6048e90</code></a>
fix(cli): make SPM dependency patch work on prereleases (<a
href="https://redirect.github.com/ionic-team/capacitor/issues/8508">#8508</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/ionic-team/capacitor/compare/7.6.1...8.5.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-23 19:00:31 -07:00
..
…
2026-08-23 14:49:26 -07:00

@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):

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

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

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: "<topic>"

…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.

ShareManager

ShareManager opens platform-native sharing when available and always includes a Fusion deep link in the shared payload.

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.

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
  • 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

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=<encoded-url>

This hash-based behavior is intended for development/testing only and is not a production universal-link replacement.