feat(native/bridge): web fallback hooks + useNativeTransition (Android & iOS)#254
feat(native/bridge): web fallback hooks + useNativeTransition (Android & iOS)#254mayankmahavar1mg wants to merge 8 commits into
Conversation
… 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 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) |
There was a problem hiding this comment.
debug log? (check logs across)
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
why 4 variables and role of each?
| } | ||
|
|
||
| useEffect(() => { | ||
| window.WebBridge.register(NATIVE_CALLBACKS.ON_CAMERA_CAPTURE, (data) => { |
There was a problem hiding this comment.
do we register callbacks even if its web and not app webview?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
webFallbackDisabled - not a state variable to monitor useEffect upon
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
repetitive returnables across, see if a function helps here with overrides as arg
| overlay.remove() | ||
| secureOverlayRef.current = null | ||
| } | ||
| }, [base.webFallbackActive, screenSecureWeb]) |
There was a problem hiding this comment.
does base reference change during re-renders ?
There was a problem hiding this comment.
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([ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
Summary
Two major additions on top of
feature/video_hook_android_ios:useNativeTransition— snapshot overlay page transitions (Android + JS)1. Hooks refactor + web fallbacks
All hooks moved from the monolithic
hooks.jsintosrc/native/bridge/hooks/with individual files per hook.
Hooks with web fallbacks added:
useVideoStream— web:getUserMediastreamuseCamera— web:<input type=file capture=environment>(no getUserMedia — iOS WebView safe)useFilePicker— web:<input type=file>useDataProtection— web: localStorage/sessionStorageuseHapticFeedback— web: no-op (API unavailable in browser)useGoogleSignIn— web: Google Identity Services SDKuseIntent— web:window.openuseSafeArea— web: CSS env() varsuseNetworkStatus— web:navigator.onLine+ Network Information APIuseNotification— web fallback intentionally skipped (Browser Push requiresService Worker; unreliable in iOS WebView).
API surface added to every hook:
webFallbackprop is synchronous source of truth; overrides internal state.Bug fixes included:
useBaseHook:isNativedetection fixed to usenativeBridge.isAvailable()instead of
window.WebBridgecheck (was alwaysfalseon real Android devices)useDataProtection,useFilePicker,useCameraisNative/isWebadded to thin-state hooks (useSafeArea,useNetworkStatus,useDeviceInfo)hooks.jsretained as re-export shim for backwards compatibility2. useNativeTransition
Snapshot overlay pattern for native-quality page transitions:
navigate(to, opts)→ bridgestartTransitioncommitTransition→ native animates overlay outWeb fallback: CSS overlay +
requestAnimationFramedouble-frame commit.Android:
TransitionManager.kt— Canvas bitmap,ImageViewondecorView,Handlersafety timer, slide/fadeObjectAnimator. Bridge wired inNativeBridge.kt+BridgeUtils.kt.JS:
useNativeTransition.jshook,NativeBridge.jshelpers, commandregistrations in
NativeInterfaces.js.iOS implementation complete but not included in this PR (pending iOS-only commit).
Files changed
NativeBridge.kt,BridgeUtils.kt,TransitionManager.ktNativeBridge.js,NativeInterfaces.js,useBaseHook.jshooks/directory (11 files),hooks.jsshimknowledge-base.json(114 entries)