/** * FNXC:TaskDetailIOSSwipeBack 2026-07-05-12:10: * FN-7586: the iOS edge-swipe-back gesture (WKWebView interactive pop / back-forward * navigation gesture) is a guaranteed no-op on a stock Capacitor 7 iOS build of this app. * `WKWebView.allowsBackForwardNavigationGestures` defaults to `false` * (https://developer.apple.com/documentation/webkit/wkwebview/1414995-allowsbackforwardnavigationgestu), * `CAPBridgeViewController.prepareWebView` (in `@capacitor/ios`) sets several WKWebView * properties from `capacitor.config.ts` (`allowsLinkPreview`, `scrollView.isScrollEnabled`, * background color, content-inset behavior) but never touches this one, and the installed * `@capacitor/cli`'s `ios` config block (see `declarations.d.ts`) exposes no first-class * toggle for it either. Without the gesture enabled, iOS never even reaches * `window.history.back()` / `popstate` — so the dashboard's shared nav-history dismissal * stack (`useNavigationHistory.handlePopState`), which board/list/modal/nested task-detail * surfaces already converge on for desktop/browser swipe-back, never gets a chance to run. * * The native `ios/` project is generated by `cap sync` (via `cap add ios`, which extracts * `@capacitor/cli`'s bundled `ios-pods-template.tar.gz`) and is git-ignored (see AGENTS.md / * `.gitignore`), so durable changes cannot live there directly — mirroring FN-7583's Android * manifest-patch precedent (`patch-android-manifest.ts`), this script idempotently patches * the generated `AppDelegate.swift` immediately after every sync, wired into Capacitor's own * `capacitor:sync:after` npm-script hook (see `package.json`), which the Capacitor CLI runs * automatically after `cap sync` (and therefore after `cap run ios` / `build:mobile` too). * * Patch target: the stock template has NO custom `CAPBridgeViewController` subclass — the * generated `Main.storyboard` wires `customClass="CAPBridgeViewController"` directly onto the * scene's view controller. `AppDelegate.didFinishLaunchingWithOptions` is therefore the * stable, minimal patch point: by the time it runs, `UIApplicationMain` has already loaded * the storyboard's root view controller into `window.rootViewController`. The patch casts it * to `CAPBridgeViewController`, forces `loadViewIfNeeded()` so `webView` is non-nil (the * bridge view controller creates its WKWebView lazily in `loadView()`), then sets * `webView?.allowsBackForwardNavigationGestures = true`. * * This keeps a SINGLE dismissal invariant across platforms: once the gesture is delivered, * iOS drives ordinary WKWebView back-forward navigation -> `popstate`, the exact same arm * browser/desktop swipe-back already uses and that `useNavigationHistory.handlePopState` * already consumes for board main-panel / list-mobile / modal / nested task-detail surfaces. * iOS has no Capacitor `backButton` event (that's Android-only, see * `src/plugins/native-shell.ts`'s `AndroidBackButtonManager`), so this does NOT add an iOS * `fusion:native-back` emitter — it reuses the existing `popstate` arm of the shared * invariant instead of introducing a second, divergent back-handling path. */ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const MARKER = "allowsBackForwardNavigationGestures"; const DID_FINISH_LAUNCHING_SIGNATURE = /func application\(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: \[UIApplication\.LaunchOptionsKey: Any\]\?\) -> Bool \{/; function buildInjectedSnippet(): string { return ` // FNXC:TaskDetailIOSSwipeBack 2026-07-05-12:10: // FN-7586: enable the WKWebView edge-swipe-back gesture so it drives ordinary // back-forward navigation (-> popstate), matching Android Back's fusion:native-back // arm and desktop/browser swipe-back. See packages/mobile/scripts/patch-ios-webview.ts // for the full rationale (idempotent — this line is the presence marker). if let bridgeViewController = self.window?.rootViewController as? CAPBridgeViewController { bridgeViewController.loadViewIfNeeded() bridgeViewController.webView?.${MARKER} = true } `; } export function patchAppDelegateSource(swift: string): { changed: boolean; swift: string } { if (swift.includes(MARKER)) { // Already opted in (idempotent — do not duplicate on repeat `cap sync` runs). return { changed: false, swift }; } const signatureMatch = swift.match(DID_FINISH_LAUNCHING_SIGNATURE); if (!signatureMatch || signatureMatch.index === undefined) { // Unrecognized AppDelegate shape (e.g. a hand-customized template) — do not guess. return { changed: false, swift }; } const insertAt = signatureMatch.index + signatureMatch[0].length; const patched = swift.slice(0, insertAt) + buildInjectedSnippet() + swift.slice(insertAt); return { changed: true, swift: patched }; } export function resolveAppDelegatePath(mobilePackageDir: string): string { return join(mobilePackageDir, "ios", "App", "App", "AppDelegate.swift"); } export function patchIOSWebView(mobilePackageDir: string): { patched: boolean; skipped: boolean; path: string } { const appDelegatePath = resolveAppDelegatePath(mobilePackageDir); if (!existsSync(appDelegatePath)) { // No iOS platform added yet (e.g. Android-only sync) — safe no-op, not an error. return { patched: false, skipped: true, path: appDelegatePath }; } const original = readFileSync(appDelegatePath, "utf8"); const { changed, swift } = patchAppDelegateSource(original); if (changed) { writeFileSync(appDelegatePath, swift, "utf8"); } return { patched: changed, skipped: false, path: appDelegatePath }; } function isMainModule(): boolean { return process.argv[1] === fileURLToPath(import.meta.url); } if (isMainModule()) { const mobilePackageDir = dirname(fileURLToPath(import.meta.url)).replace(/[/\\]scripts$/, ""); const result = patchIOSWebView(mobilePackageDir); if (result.skipped) { console.log(`[patch-ios-webview] no ios/ project at ${result.path}; skipping (nothing to patch yet)`); } else if (result.patched) { console.log(`[patch-ios-webview] enabled allowsBackForwardNavigationGestures in ${result.path}`); } else { console.log(`[patch-ios-webview] ${result.path} already enables allowsBackForwardNavigationGestures; no change`); } }