diff --git a/.changeset/fn-7257-android-back-task-detail.md b/.changeset/fn-7257-android-back-task-detail.md
new file mode 100644
index 0000000000..6063a1bb61
--- /dev/null
+++ b/.changeset/fn-7257-android-back-task-detail.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Dismiss mobile task details when Android Back is pressed before falling back to app exit.
+category: fix
+dev: Routes Capacitor Android Back through the dashboard navigation-history stack via a cancelable native event.
diff --git a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx
index c42551a7f5..53b47a0e0d 100644
--- a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx
@@ -186,12 +186,19 @@ vi.mock("../../components/TaskDetailModal", () => ({
TaskDetailContent: ({
task,
onBackToBoard,
+ onOpenDetail,
}: {
task: { id: string; title?: string };
onBackToBoard?: () => void;
+ onOpenDetail?: (task: { id: string; title: string }) => void;
}) => (
{onBackToBoard ? : null}
+ {task.id === "FN-1" && onOpenDetail ? (
+
+ ) : null}
{task.title ?? task.id}
),
@@ -307,6 +314,16 @@ function dispatchPopState(state: Record | null) {
});
}
+function dispatchNativeAndroidBack(): boolean {
+ let handled = false;
+ act(() => {
+ const event = new CustomEvent("fusion:native-back", { cancelable: true, detail: { source: "android-back" } });
+ window.dispatchEvent(event);
+ handled = event.defaultPrevented;
+ });
+ return handled;
+}
+
async function renderAppAndWait(expectedTestId: string = "board-view") {
const result = render();
await waitFor(() => {
@@ -318,6 +335,7 @@ async function renderAppAndWait(expectedTestId: string = "board-view") {
describe("Task detail mobile swipe-back", () => {
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
+ const originalBack = window.history.back;
beforeEach(() => {
vi.clearAllMocks();
@@ -345,11 +363,87 @@ describe("Task detail mobile swipe-back", () => {
localStorage.clear();
window.history.pushState = vi.fn();
window.history.replaceState = vi.fn();
+ window.history.back = vi.fn();
});
afterEach(() => {
window.history.pushState = originalPushState;
window.history.replaceState = originalReplaceState;
+ window.history.back = originalBack;
+ });
+
+ it("dismisses the board main-panel task detail on native Android Back", async () => {
+ const task = makeTask("FN-1", "Board Detail");
+ mockUseTasks.mockImplementation(() => ({
+ tasks: [task],
+ createTask: mockCreateTask,
+ moveTask: vi.fn(),
+ deleteTask: vi.fn(),
+ mergeTask: vi.fn(),
+ retryTask: vi.fn(),
+ updateTask: vi.fn(),
+ duplicateTask: vi.fn(),
+ archiveTask: vi.fn(),
+ unarchiveTask: vi.fn(),
+ archiveAllDone: vi.fn(),
+ refreshTasks: vi.fn(),
+ }));
+
+ await renderAppAndWait("board-view");
+ fireEvent.click(screen.getByTestId("open-task-FN-1"));
+
+ await waitFor(() => {
+ expect(screen.getByTestId("task-detail-main-panel-content")).toBeInTheDocument();
+ });
+
+ expect(dispatchNativeAndroidBack()).toBe(true);
+ expect(window.history.back).toHaveBeenCalledTimes(1);
+ expect(screen.getByTestId("task-detail-main-panel-content")).toBeInTheDocument();
+ dispatchPopState({ navIndex: 0 });
+
+ await waitFor(() => {
+ expect(screen.queryByTestId("task-detail-main-panel-content")).toBeNull();
+ expect(screen.getByTestId("board-view")).toBeInTheDocument();
+ });
+ });
+
+ it("restores the previous board main-panel detail on native Android Back", async () => {
+ const task = makeTask("FN-1", "Parent Main Panel Task");
+ mockUseTasks.mockImplementation(() => ({
+ tasks: [task, makeTask("FN-2", "Nested Main Panel Task")],
+ createTask: mockCreateTask,
+ moveTask: vi.fn(),
+ deleteTask: vi.fn(),
+ mergeTask: vi.fn(),
+ retryTask: vi.fn(),
+ updateTask: vi.fn(),
+ duplicateTask: vi.fn(),
+ archiveTask: vi.fn(),
+ unarchiveTask: vi.fn(),
+ archiveAllDone: vi.fn(),
+ refreshTasks: vi.fn(),
+ }));
+
+ await renderAppAndWait("board-view");
+ fireEvent.click(screen.getByTestId("open-task-FN-1"));
+ await waitFor(() => {
+ expect(screen.getByRole("heading", { name: "Parent Main Panel Task" })).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByTestId("task-detail-open-nested"));
+ await waitFor(() => {
+ expect(screen.getByRole("heading", { name: "Nested Main Panel Task" })).toBeInTheDocument();
+ });
+ expect(window.history.pushState).toHaveBeenCalledTimes(2);
+
+ expect(dispatchNativeAndroidBack()).toBe(true);
+ expect(screen.getByRole("heading", { name: "Nested Main Panel Task" })).toBeInTheDocument();
+ dispatchPopState({ navIndex: 1 });
+
+ await waitFor(() => {
+ expect(screen.getByRole("heading", { name: "Parent Main Panel Task" })).toBeInTheDocument();
+ });
+ expect(screen.queryByRole("heading", { name: "Nested Main Panel Task" })).toBeNull();
});
it("dismisses the board main-panel task detail on mobile popstate", async () => {
@@ -384,6 +478,80 @@ describe("Task detail mobile swipe-back", () => {
});
});
+ it("dismisses the list-mobile task detail on native Android Back", async () => {
+ const task = makeTask("FN-1", "Mobile List Detail");
+ mockUseTasks.mockImplementation(() => ({
+ tasks: [task],
+ createTask: mockCreateTask,
+ moveTask: vi.fn(),
+ deleteTask: vi.fn(),
+ mergeTask: vi.fn(),
+ retryTask: vi.fn(),
+ updateTask: vi.fn(),
+ duplicateTask: vi.fn(),
+ archiveTask: vi.fn(),
+ unarchiveTask: vi.fn(),
+ archiveAllDone: vi.fn(),
+ refreshTasks: vi.fn(),
+ }));
+ localStorage.setItem("kb-dashboard-view-mode", "project");
+ localStorage.setItem(scopedKey("kb-dashboard-task-view", DEFAULT_PROJECT_ID), "list");
+
+ await renderAppAndWait("list-view");
+ fireEvent.click(screen.getByTestId("list-open-FN-1"));
+
+ await waitFor(() => {
+ expect(screen.getByTestId("task-detail-modal")).toBeInTheDocument();
+ expect(screen.getByTestId("task-detail-mobile-header-mode")).toHaveTextContent("back");
+ });
+
+ expect(dispatchNativeAndroidBack()).toBe(true);
+ expect(window.history.back).toHaveBeenCalledTimes(1);
+ expect(screen.getByTestId("task-detail-modal")).toBeInTheDocument();
+ dispatchPopState({ navIndex: 0 });
+
+ await waitFor(() => {
+ expect(screen.queryByTestId("task-detail-modal")).toBeNull();
+ expect(screen.getByTestId("list-view")).toBeInTheDocument();
+ });
+ });
+
+ it("does not swallow native Android Back when no Fusion nav entry exists", async () => {
+ await renderAppAndWait("board-view");
+
+ expect(dispatchNativeAndroidBack()).toBe(false);
+ expect(window.history.back).not.toHaveBeenCalled();
+ });
+
+ it("leaves desktop browser behavior unchanged for native Back events without task-detail history", async () => {
+ mockUseViewportMode.mockReturnValue("desktop");
+ const task = makeTask("FN-1", "Desktop List Detail");
+ mockUseTasks.mockImplementation(() => ({
+ tasks: [task],
+ createTask: mockCreateTask,
+ moveTask: vi.fn(),
+ deleteTask: vi.fn(),
+ mergeTask: vi.fn(),
+ retryTask: vi.fn(),
+ updateTask: vi.fn(),
+ duplicateTask: vi.fn(),
+ archiveTask: vi.fn(),
+ unarchiveTask: vi.fn(),
+ archiveAllDone: vi.fn(),
+ refreshTasks: vi.fn(),
+ }));
+ localStorage.setItem("kb-dashboard-view-mode", "project");
+ localStorage.setItem(scopedKey("kb-dashboard-task-view", DEFAULT_PROJECT_ID), "list");
+
+ await renderAppAndWait("list-view");
+ fireEvent.click(screen.getByTestId("list-open-FN-1"));
+
+ expect(dispatchNativeAndroidBack()).toBe(false);
+ expect(window.history.pushState).not.toHaveBeenCalled();
+ expect(window.history.back).not.toHaveBeenCalled();
+ expect(screen.queryByTestId("task-detail-modal")).toBeNull();
+ });
+
it("dismisses the list-mobile task detail on mobile popstate", async () => {
const task = makeTask("FN-1", "Mobile List Detail");
mockUseTasks.mockImplementation(() => ({
@@ -505,6 +673,52 @@ describe("Task detail mobile swipe-back", () => {
expect(screen.queryByRole("dialog", { name: "Nested Task" })).toBeNull();
});
+ it("consumes repeated native Android Back events while nested mobile detail entries exist", async () => {
+ const task = makeTask("FN-1", "Parent Task");
+ mockUseTasks.mockImplementation(() => ({
+ tasks: [task, makeTask("FN-2", "Nested Task")],
+ createTask: mockCreateTask,
+ moveTask: vi.fn(),
+ deleteTask: vi.fn(),
+ mergeTask: vi.fn(),
+ retryTask: vi.fn(),
+ updateTask: vi.fn(),
+ duplicateTask: vi.fn(),
+ archiveTask: vi.fn(),
+ unarchiveTask: vi.fn(),
+ archiveAllDone: vi.fn(),
+ refreshTasks: vi.fn(),
+ }));
+ localStorage.setItem("kb-dashboard-view-mode", "project");
+ localStorage.setItem(scopedKey("kb-dashboard-task-view", DEFAULT_PROJECT_ID), "list");
+
+ await renderAppAndWait("list-view");
+ fireEvent.click(screen.getByTestId("list-open-FN-1"));
+ await waitFor(() => {
+ expect(screen.getByRole("dialog", { name: "Parent Task" })).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByTestId("task-detail-open-nested"));
+ await waitFor(() => {
+ expect(screen.getByRole("dialog", { name: "Nested Task" })).toBeInTheDocument();
+ });
+
+ expect(dispatchNativeAndroidBack()).toBe(true);
+ expect(screen.getByRole("dialog", { name: "Nested Task" })).toBeInTheDocument();
+ dispatchPopState({ navIndex: 1 });
+ await waitFor(() => {
+ expect(screen.getByRole("dialog", { name: "Parent Task" })).toBeInTheDocument();
+ });
+
+ expect(dispatchNativeAndroidBack()).toBe(true);
+ dispatchPopState({ navIndex: 0 });
+ await waitFor(() => {
+ expect(screen.queryByTestId("task-detail-modal")).toBeNull();
+ expect(screen.getByTestId("list-view")).toBeInTheDocument();
+ });
+ expect(window.history.back).toHaveBeenCalledTimes(2);
+ });
+
it("pops multiple mobile detail entries back to the list target on a rapid Android-style pop", async () => {
const task = makeTask("FN-1", "Parent Task");
mockUseTasks.mockImplementation(() => ({
diff --git a/packages/dashboard/app/hooks/useNavigationHistory.ts b/packages/dashboard/app/hooks/useNavigationHistory.ts
index 0524aff946..371d995eb5 100644
--- a/packages/dashboard/app/hooks/useNavigationHistory.ts
+++ b/packages/dashboard/app/hooks/useNavigationHistory.ts
@@ -41,6 +41,7 @@ export interface UseNavigationHistoryResult {
}
const SELF_POP_FALLBACK_CLEAR_MS = 1_000;
+const FUSION_NATIVE_BACK_EVENT = "fusion:native-back";
export const NavigationHistoryContext = createContext(null);
@@ -173,6 +174,18 @@ export function useNavigationHistory(
useEffect(() => {
if (typeof window === "undefined") return;
+ /*
+ FNXC:TaskDetailAndroidBack 2026-06-29-20:40:
+ The mobile shell dispatches a cancelable native Back event before applying its fallback. Prevent it only when Fusion owns at least one nav entry, then route through history.back() so modal, nested, snapshot-only, hydrated, and main-panel task-detail dismissals reuse the same popstate stack semantics as browser Back and swipe-back.
+ */
+ const handleNativeBack = (event: Event) => {
+ if (!enabledRef.current) return;
+ if (stackRef.current.length === 0) return;
+
+ event.preventDefault();
+ window.history.back();
+ };
+
const handlePopState = (event: PopStateEvent) => {
if (!enabledRef.current) return;
@@ -214,8 +227,10 @@ export function useNavigationHistory(
}
};
+ window.addEventListener(FUSION_NATIVE_BACK_EVENT, handleNativeBack);
window.addEventListener("popstate", handlePopState);
return () => {
+ window.removeEventListener(FUSION_NATIVE_BACK_EVENT, handleNativeBack);
window.removeEventListener("popstate", handlePopState);
if (selfPopClearTimerRef.current !== null) {
window.clearTimeout(selfPopClearTimerRef.current);
diff --git a/packages/mobile/src/__tests__/native-shell.test.ts b/packages/mobile/src/__tests__/native-shell.test.ts
index af362cf9cf..999ca392eb 100644
--- a/packages/mobile/src/__tests__/native-shell.test.ts
+++ b/packages/mobile/src/__tests__/native-shell.test.ts
@@ -1,6 +1,46 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { buildMobileShellHandoff } from "../plugins/shell-handoff.js";
+type BackButtonListener = (event: { canGoBack: boolean }) => void;
+
+const capacitorState = vi.hoisted(() => {
+ const state: {
+ isNativePlatform: ReturnType;
+ addListener: ReturnType;
+ backButtonRemove: ReturnType;
+ exitApp: ReturnType;
+ backButtonListener?: BackButtonListener;
+ } = {
+ isNativePlatform: vi.fn(() => false),
+ addListener: vi.fn(),
+ backButtonRemove: vi.fn(async () => {}),
+ exitApp: vi.fn(async () => {}),
+ backButtonListener: undefined,
+ };
+
+ state.addListener.mockImplementation(async (eventName: string, callback: BackButtonListener) => {
+ if (eventName === "backButton") {
+ state.backButtonListener = callback;
+ }
+ return { remove: state.backButtonRemove };
+ });
+
+ return state;
+});
+
+vi.mock("@capacitor/core", () => ({
+ Capacitor: {
+ isNativePlatform: capacitorState.isNativePlatform,
+ },
+}));
+
+vi.mock("@capacitor/app", () => ({
+ App: {
+ addListener: capacitorState.addListener,
+ exitApp: capacitorState.exitApp,
+ },
+}));
+
const state = {
activeProfileId: null as string | null,
profiles: [] as Array<{ id: string; name: string; serverUrl: string; authToken?: string | null; createdAt: string; updatedAt: string; lastUsedAt?: string | null }>,
@@ -39,9 +79,16 @@ describe("MobileNativeShellBridge", () => {
state.activeProfileId = null;
state.profiles = [];
scanner.scanConnection.mockClear();
+ capacitorState.isNativePlatform.mockReturnValue(false);
+ capacitorState.backButtonListener = undefined;
+ vi.clearAllMocks();
vi.resetModules();
});
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
it("emits state updates to subscribers", async () => {
const { MobileNativeShellBridge } = await import("../plugins/native-shell.js");
const bridge = new MobileNativeShellBridge(scanner as never);
@@ -110,4 +157,106 @@ describe("MobileNativeShellBridge", () => {
await expect(bridge.setDesktopMode("local")).rejects.toThrow("Desktop mode is not supported");
});
+
+ it("Android Back: skips Capacitor backButton registration on non-native platforms", async () => {
+ const { AndroidBackButtonManager } = await import("../plugins/native-shell.js");
+ const manager = new AndroidBackButtonManager();
+
+ await manager.initialize();
+
+ expect(capacitorState.isNativePlatform).toHaveBeenCalledTimes(1);
+ expect(capacitorState.addListener).not.toHaveBeenCalled();
+ expect(capacitorState.backButtonListener).toBeUndefined();
+
+ await manager.destroy();
+
+ expect(capacitorState.backButtonRemove).not.toHaveBeenCalled();
+ });
+
+ it("Android Back: registers and removes the native backButton listener on native platforms", async () => {
+ capacitorState.isNativePlatform.mockReturnValue(true);
+ const { AndroidBackButtonManager } = await import("../plugins/native-shell.js");
+ const manager = new AndroidBackButtonManager();
+
+ await manager.initialize();
+
+ expect(capacitorState.addListener).toHaveBeenCalledWith("backButton", expect.any(Function));
+
+ await manager.destroy();
+
+ expect(capacitorState.backButtonRemove).toHaveBeenCalledTimes(1);
+ });
+
+ it("Android Back: does not duplicate native listener registration across repeated initialization", async () => {
+ capacitorState.isNativePlatform.mockReturnValue(true);
+ const { AndroidBackButtonManager } = await import("../plugins/native-shell.js");
+ const manager = new AndroidBackButtonManager();
+
+ await manager.initialize();
+ await manager.initialize();
+
+ expect(capacitorState.addListener).toHaveBeenCalledTimes(1);
+
+ await manager.destroy();
+
+ expect(capacitorState.backButtonRemove).toHaveBeenCalledTimes(1);
+ });
+
+ it("Android Back: dispatches a cancelable browser event before native fallback", async () => {
+ capacitorState.isNativePlatform.mockReturnValue(true);
+ const mockWindow = new EventTarget() as Window & typeof globalThis;
+ const back = vi.fn();
+ Object.defineProperty(mockWindow, "history", {
+ value: { back },
+ configurable: true,
+ });
+ vi.stubGlobal("window", mockWindow);
+ const nativeBackListener = vi.fn((event: Event) => event.preventDefault());
+ mockWindow.addEventListener("fusion:native-back", nativeBackListener as EventListener);
+ const { AndroidBackButtonManager } = await import("../plugins/native-shell.js");
+ const manager = new AndroidBackButtonManager();
+
+ await manager.initialize();
+ capacitorState.backButtonListener?.({ canGoBack: false });
+
+ expect(nativeBackListener).toHaveBeenCalledTimes(1);
+ expect(back).not.toHaveBeenCalled();
+ expect(capacitorState.exitApp).not.toHaveBeenCalled();
+ });
+
+ it("Android Back: preserves browser-history fallback when Fusion does not handle it", async () => {
+ capacitorState.isNativePlatform.mockReturnValue(true);
+ const mockWindow = new EventTarget() as Window & typeof globalThis;
+ const back = vi.fn();
+ Object.defineProperty(mockWindow, "history", {
+ value: { back },
+ configurable: true,
+ });
+ vi.stubGlobal("window", mockWindow);
+ const { AndroidBackButtonManager } = await import("../plugins/native-shell.js");
+ const manager = new AndroidBackButtonManager();
+
+ await manager.initialize();
+ capacitorState.backButtonListener?.({ canGoBack: true });
+
+ expect(back).toHaveBeenCalledTimes(1);
+ expect(capacitorState.exitApp).not.toHaveBeenCalled();
+ });
+
+ it("Android Back: preserves native exit fallback when no history can go back", async () => {
+ capacitorState.isNativePlatform.mockReturnValue(true);
+ const mockWindow = new EventTarget() as Window & typeof globalThis;
+ Object.defineProperty(mockWindow, "history", {
+ value: { back: vi.fn() },
+ configurable: true,
+ });
+ vi.stubGlobal("window", mockWindow);
+ const { AndroidBackButtonManager } = await import("../plugins/native-shell.js");
+ const manager = new AndroidBackButtonManager();
+
+ await manager.initialize();
+ capacitorState.backButtonListener?.({ canGoBack: false });
+
+ expect(capacitorState.exitApp).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/packages/mobile/src/index.ts b/packages/mobile/src/index.ts
index 13c471885d..1b4bd4d35b 100644
--- a/packages/mobile/src/index.ts
+++ b/packages/mobile/src/index.ts
@@ -21,7 +21,11 @@ export type {
PushNotificationManagerOptions,
} from "./plugins/push-notifications.js";
export { ShareManager } from "./plugins/share.js";
-export { MobileNativeShellBridge } from "./plugins/native-shell.js";
+export {
+ AndroidBackButtonManager,
+ FUSION_NATIVE_BACK_EVENT,
+ MobileNativeShellBridge,
+} from "./plugins/native-shell.js";
export { buildMobileShellHandoff } from "./plugins/shell-handoff.js";
export { QrScanner, parseQrConnectionPayload } from "./plugins/qr-scanner.js";
export {
@@ -83,6 +87,7 @@ export function installMobileShellBridge(
): MobileNativeShellBridge {
const bridge = new MobileNativeShellBridge();
(target as Window & { fusionShell?: MobileNativeShellBridge }).fusionShell = bridge;
+ void bridge.initializeNativeBackButton();
return bridge;
}
diff --git a/packages/mobile/src/plugins/native-shell.ts b/packages/mobile/src/plugins/native-shell.ts
index e12a26ab6e..b15e3b8c93 100644
--- a/packages/mobile/src/plugins/native-shell.ts
+++ b/packages/mobile/src/plugins/native-shell.ts
@@ -1,3 +1,4 @@
+import { Capacitor } from "@capacitor/core";
import type {
FusionShellApi,
ShellConnectionProfile,
@@ -14,11 +15,111 @@ import {
import { QrScanner, type QrScanResult } from "./qr-scanner.js";
type Listener = (state: ShellConnectionState) => void;
+type AppModule = typeof import("@capacitor/app");
+type AppBackButtonListenerEvent = { canGoBack: boolean };
+type AppListenerHandle = { remove: () => Promise };
+
+export const FUSION_NATIVE_BACK_EVENT = "fusion:native-back";
+
+export class AndroidBackButtonManager {
+ private initialized = false;
+ private listenerHandle?: AppListenerHandle;
+ private appPlugin: AppModule["App"] | null = null;
+
+ /*
+ FNXC:TaskDetailAndroidBack 2026-06-29-20:40:
+ Native Android Back must first offer the dashboard's shared navigation-history stack a cancelable browser event so every task-detail surface dismisses through the same invariant as swipe/browser popstate. If the dashboard does not prevent the event, preserve Capacitor's native fallback by going back in ordinary browser history or exiting the app.
+ */
+ async initialize(): Promise {
+ if (this.initialized) {
+ return;
+ }
+
+ this.initialized = true;
+
+ if (!Capacitor.isNativePlatform()) {
+ return;
+ }
+
+ const app = await this.loadAppPlugin();
+ if (!app) {
+ return;
+ }
+
+ this.listenerHandle = await app.addListener("backButton", (event) => {
+ this.handleBackButton(event);
+ });
+ }
+
+ async destroy(): Promise {
+ if (this.listenerHandle) {
+ try {
+ await this.listenerHandle.remove();
+ } catch (error) {
+ console.warn("Failed to remove backButton listener", error);
+ }
+ this.listenerHandle = undefined;
+ }
+
+ this.initialized = false;
+ }
+
+ private handleBackButton(event: AppBackButtonListenerEvent): void {
+ if (this.dispatchNativeBackEvent()) {
+ return;
+ }
+
+ const win = globalThis.window;
+ if (event.canGoBack && win?.history && typeof win.history.back === "function") {
+ win.history.back();
+ return;
+ }
+
+ void this.appPlugin?.exitApp();
+ }
+
+ private dispatchNativeBackEvent(): boolean {
+ const win = globalThis.window;
+ if (!win || typeof win.dispatchEvent !== "function") {
+ return false;
+ }
+
+ const event = new CustomEvent(FUSION_NATIVE_BACK_EVENT, {
+ cancelable: true,
+ detail: { source: "android-back" },
+ });
+ win.dispatchEvent(event);
+ return event.defaultPrevented;
+ }
+
+ private async loadAppPlugin(): Promise {
+ try {
+ if (!this.appPlugin) {
+ this.appPlugin = (await import("@capacitor/app")).App;
+ }
+ return this.appPlugin;
+ } catch (error) {
+ console.warn("Failed to load Capacitor App plugin for Android Back", error);
+ return null;
+ }
+ }
+}
export class MobileNativeShellBridge implements FusionShellApi {
private listeners = new Set();
- constructor(private readonly qrScanner: QrScanner = new QrScanner()) {}
+ constructor(
+ private readonly qrScanner: QrScanner = new QrScanner(),
+ private readonly androidBackButtonManager: AndroidBackButtonManager = new AndroidBackButtonManager(),
+ ) {}
+
+ initializeNativeBackButton(): Promise {
+ return this.androidBackButtonManager.initialize();
+ }
+
+ destroy(): Promise {
+ return this.androidBackButtonManager.destroy();
+ }
private async buildState(): Promise {
const persisted = await loadShellProfiles();