Skip to content

feat(native/bridge): web fallback hooks + useNativeTransition (Android & iOS)#254

Open
mayankmahavar1mg wants to merge 8 commits into
mainfrom
feature/web_fallback_clean
Open

feat(native/bridge): web fallback hooks + useNativeTransition (Android & iOS)#254
mayankmahavar1mg wants to merge 8 commits into
mainfrom
feature/web_fallback_clean

Conversation

@mayankmahavar1mg

Copy link
Copy Markdown
Collaborator

Summary

Two major additions on top of feature/video_hook_android_ios:

  1. All 9 native hooks refactored into per-file structure with web fallbacks
  2. useNativeTransition — snapshot overlay page transitions (Android + JS)

1. Hooks refactor + web fallbacks

All hooks moved from the monolithic hooks.js into src/native/bridge/hooks/
with individual files per hook.

Hooks with web fallbacks added:

  • useVideoStream — web: getUserMedia stream
  • useCamera — web: <input type=file capture=environment> (no getUserMedia — iOS WebView safe)
  • useFilePicker — web: <input type=file>
  • useDataProtection — web: localStorage/sessionStorage
  • useHapticFeedback — web: no-op (API unavailable in browser)
  • useGoogleSignIn — web: Google Identity Services SDK
  • useIntent — web: window.open
  • useSafeArea — web: CSS env() vars
  • useNetworkStatus — web: navigator.onLine + Network Information API

useNotification — web fallback intentionally skipped (Browser Push requires
Service Worker; unreliable in iOS WebView).

API surface added to every hook:

const { ..., webFallbackActive, webFallbackDisabled, setWebFallback } = useXxx()

webFallback prop is synchronous source of truth; overrides internal state.

Bug fixes included:

  • useBaseHook: isNative detection fixed to use nativeBridge.isAvailable()
    instead of window.WebBridge check (was always false on real Android devices)
  • Rules of Hooks violations fixed in useDataProtection, useFilePicker, useCamera
  • isNative / isWeb added to thin-state hooks (useSafeArea, useNetworkStatus, useDeviceInfo)
  • hooks.js retained as re-export shim for backwards compatibility

2. useNativeTransition

Snapshot overlay pattern for native-quality page transitions:

  1. JS calls navigate(to, opts) → bridge startTransition
  2. Native snapshots current WebView, places bitmap overlay over full screen
  3. JS router navigates underneath (snapshot hides the content swap)
  4. JS calls commitTransition → native animates overlay out
  5. Per-call safety timeout on native auto-commits if JS never calls commit

Web fallback: CSS overlay + requestAnimationFrame double-frame commit.

Android: TransitionManager.kt — Canvas bitmap, ImageView on decorView,
Handler safety timer, slide/fade ObjectAnimator. Bridge wired in
NativeBridge.kt + BridgeUtils.kt.

JS: useNativeTransition.js hook, NativeBridge.js helpers, command
registrations in NativeInterfaces.js.

navigate('/path', {
  type: 'slide',      // 'slide' | 'fade'
  direction: 'left',  // 'left' | 'right'
  duration: 300,
  timeout: 900,       // safety ms, default max(duration*3, 800)
})

iOS implementation complete but not included in this PR (pending iOS-only commit).


Files changed

Area Files
Android NativeBridge.kt, BridgeUtils.kt, TransitionManager.kt
JS bridge NativeBridge.js, NativeInterfaces.js, useBaseHook.js
Hooks hooks/ directory (11 files), hooks.js shim
KB knowledge-base.json (114 entries)
```

mayankmahavar1mg and others added 5 commits May 25, 2026 17:49
… with web fallbacks

- Extract all 9 hooks from monolithic hooks.js into src/native/bridge/hooks/ (one file per domain)
- hooks.js becomes a 2-line re-export shim for backwards compatibility
- Add web fallbacks for useVideoStream, useCamera, useCameraPermission, useFilePicker,
  useHapticFeedback, useDataProtection, useNetworkStatus, useSafeArea, useDeviceInfo
- Fix useBaseHook isNative detection to use nativeBridge.isAvailable() instead of raw window check
- Fix Rules of Hooks violations in useDataProtection, useFilePicker, useCamera
- Add isNative/isWeb to useSafeArea, useNetworkStatus, useDeviceInfo return shapes
- Fix useNotification interface alignment (add schedule alias, requestPermission, isNative/isWeb)
- Add webFallback prop dynamic sync to all hooks via useEffect
- Update KB with 4 new entries: notification config, sound asset, googleSignIn config, useBaseHook fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… implementation

- Add TransitionManager.kt: snapshot overlay pattern using Canvas bitmap,
  ImageView on decorView, Handler safety timeout, slide/fade animations
- Wire startTransition/commitTransition/cancelTransition in NativeBridge.kt
  and BridgeUtils.kt (options parsing, delegation, malformed JSON safety)
- Add useNativeTransition.js hook: wraps useNavigate, CSS overlay web
  fallback, rAF double-frame commit, per-call configurable timeout
- Add transition bridge helpers to NativeBridge.js (start/commit/cancel)
- Register transition commands in NativeInterfaces.js
- Export useNativeTransition from hooks/index.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ess)

Adds TransitionManager.swift (snapshot overlay pattern), wires
startTransition/commitTransition/cancelTransition into NativeBridge.swift,
registers transition commands in CatalystConstants.swift and
BridgeMessageValidator.swift. Known bugs — committing to unblock main checkout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cherry-pick placed hook files at old pre-monorepo path src/native/bridge/hooks/.
Repo has since moved to packages/catalyst-core/src/. Copying to the correct path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ebuild

This flag was causing build inconsistencies. Matches the working main branch configuration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@deputydev-agent

Copy link
Copy Markdown

DeputyDev will no longer review pull requests automatically.To request a review, simply comment #review on your pull request—this will trigger an on-demand review whenever you need it.

Remove unused useState/useEffect imports in useFilePicker and useBaseHook.
Add comments to empty catch blocks in useCamera and useDataProtection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
window.WebBridge.register(NATIVE_CALLBACKS.ON_CAMERA_CAPTURE, (data) => {
try {
const result = typeof data === "string" ? JSON.parse(data) : data
console.log("📷 Camera capture result:", result)

@trinachoudhury1mg trinachoudhury1mg May 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug log? (check logs across)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These logs are intentionally scoped to the native path — they're inside the if (!window.WebBridge) return guard (line 105), so they only fire when WebBridge is present (i.e., native app webview). They'll never show on plain web. Happy to wrap them in isDevelopment() if we want to mute them in prod native builds, but they serve as confirmation that the bridge callback is firing correctly.


// Web fallback callback — always declared (Rules of Hooks)
const webTakePhoto = useCallback(() => {
const input = document.createElement("input")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when are we cleaning up the input element from DOM?

const [webFallbackState, setWebFallback] = useState(true)
const webFallbackResolved = webFallback !== undefined ? webFallback !== false : webFallbackState
const webFallbackActive = isWeb && webFallbackResolved
const webFallbackDisabled = isWeb && !webFallbackResolved

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why 4 variables and role of each?

}

useEffect(() => {
window.WebBridge.register(NATIVE_CALLBACKS.ON_CAMERA_CAPTURE, (data) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we register callbacks even if its web and not app webview?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect (line 104) has an early return if (!window.WebBridge) return, so window.WebBridge.register(...) is only ever called when the native bridge is present. Pure web environments exit immediately and no callbacks are registered.

isNative: base.isNative,
webFallbackActive: false,
webFallbackDisabled: false,
setWebFallback: base.setWebFallback,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webFallbackActive: false,
webFallbackDisabled: false,
setWebFallback: base.setWebFallback,

why not just 1 flag for webFallbackEnabled - which determines whether on web , webfallback needs to be used directly

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't defined in useCamera.js — they live in useBaseHook.js (lines 29–31). The logic is:

  • webFallbackResolved — prop wins over state (synchronous, no async gap)
  • webFallbackActive — is web AND fallback enabled
  • webFallbackDisabled — is web AND fallback explicitly disabled

useCamera just passes { hasWebFallback: true, webFallback } to useBaseHook and consumes these computed flags.

window.WebBridge.unregister(NATIVE_CALLBACKS.CAMERA_PERMISSION_STATUS)
}
}
}, [webFallbackDisabled])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webFallbackDisabled - not a state variable to monitor useEffect upon

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On consolidation: webFallback prop being undefined (not passed) vs false (explicitly disabled) are semantically different — "use hook default" vs "explicitly disable fallback". A single flag loses that distinction. webFallbackResolved is the merge point, and webFallbackActive/webFallbackDisabled are the two exit conditions for early returns.

On the dep: webFallbackDisabled is derived from webFallbackState (React state via useState) + isWeb (stable). Its value changes only when webFallbackState changes, so the effect re-runs correctly. It's a derived value but its source is state.

}
}, [])

if (typeof window === "undefined") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repetitive returnables across, see if a function helps here with overrides as arg

overlay.remove()
secureOverlayRef.current = null
}
}, [base.webFallbackActive, screenSecureWeb])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does base reference change during re-renders ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On consolidation: webFallback prop being undefined (not passed) vs false (explicitly disabled) are semantically different — "use hook default" vs "explicitly disable fallback". A single flag loses that distinction. webFallbackResolved is the merge point, and webFallbackActive/webFallbackDisabled are the two exit conditions for early returns.

On the dep: webFallbackDisabled is derived from webFallbackState (React state via useState) + isWeb (stable). Its value changes only when webFallbackState changes, so the effect re-runs correctly. It's a derived value but its source is state.

}
}

const cleanup = registerNativeHandlers([

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have a question, as a dev who wants to manage both for native app and web users he'd have to manage callbacks on their own by adding checks instead of having one set of events which internally identifies at our end whether its called for native or web.
General comment for all hooks where this is applicable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already handled internally — the hook exposes a single execute (aliased as takePhoto for camera) that routes to the correct implementation based on environment. On native it calls nativeBridge.camera.open() and listens via WebBridge callbacks; on web it triggers the fallback. The consumer just calls execute() or takePhoto() without any isNative/isWeb checks on their end.

The isNative / isWeb flags are exposed for informational use (e.g. rendering different UI), not for gating the operation itself. Same pattern applies across all hooks that have a web fallback — useFilePicker, etc. all expose a single execute.

All issues surfaced by CodeRabbit review on feature/web_fallback_clean vs main.
Verified in POC app — all hooks and transitions working correctly.

Critical (1):
- fix(utils.js): add typeof window guard in registerNativeHandlers to prevent
  SSR crash — window is not defined in Node/SSR environments

Major (8):
- fix(useHapticFeedback): move early SSR return after all hook declarations
  (Rules of Hooks violation)
- fix(useGoogleSignIn): move useState/useEffect to top level, remove SSR throw,
  guard native init inside useEffect (Rules of Hooks violation)
- fix(useIntent): same Rules of Hooks fix as useGoogleSignIn — guard inside
  useEffect, no top-level early return before hooks
- fix(useNotificationPermission): move useState/useEffect before SSR early return
- fix(useNotification): move SSR/WebBridge throw inside useEffect, hooks
  unconditional at top (Rules of Hooks violation)
- fix(useCamera): move useCallback before SSR early return, add window.WebBridge
  guard inside native useEffect (Rules of Hooks violation)
- fix(useNativeTransition): isNative now useState+useEffect (was bare
  window.WebBridge check — stale false on SSR hydration); webTimeoutRef cleared
  before and after _hideWebOverlay; dead pendingCommitRef removed; safety timeout
  clears before resetting
- fix(useBaseHook): !!webFallback coercion corrected — was `!== false`
  (treated undefined as truthy), now `!!webFallback`

Minor (12):
- fix(utils.js): wrap parseNativePayload JSON.parse in try-catch — malformed
  native payloads no longer throw uncaught exceptions
- fix(useCamera): revoke previous blob URL before creating new one in
  capturePhoto to prevent URL.createObjectURL memory leaks
- fix(useFilePicker): revoke blob URL before replacing in getFileObject;
  throw instead of caching undefined for unknown transport types
- fix(useCamera): add .toLowerCase() type guard on data in
  requestCameraPermission — native may send non-string values
- fix(useCamera): clean up permResultRef.onchange on unmount
- fix(NativeBridge.swift): clamp handleStartTransition timeout to
  [100, 2000] ms bounds to prevent animation glitches on invalid values

Findings pushed back (not fixed — intentional design):
- 4x webFallback variable names: layered prop/state/resolved design is
  intentional; renaming loses meaning
- isNative vs isWeb separate flags: 3 states (native/web/unknown) cannot
  collapse to 1 boolean
- repetitive return shapes: each hook's domain fields differ — no shared type
- base reference stability: already using individual callbacks, not base object
- unified event surface: out of scope for this PR

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants