🏗️ Add generalized WebView bridge with identity and URL handlers#29
🏗️ Add generalized WebView bridge with identity and URL handlers#29williamchong wants to merge 3 commits intolikecoin:mainfrom
Conversation
Replace monolithic switch in index.tsx with a dispatcher/handler registry pattern. Add identity bridge (PostHog, Firebase Analytics, Sentry user sync) and URL bridge (in-app browser via expo-web-browser) as the first non-audio bridge handlers. Extract PostHog instance to shared module.
…uest WalletConnect, MetaMask, Coinbase and other wallet SDKs trigger navigation directly (wc:, metamask:, cbwallet: schemes + universal links) which the WebView silently drops. Intercept these via onShouldStartLoadWithRequest and route through expo-linking.
There was a problem hiding this comment.
Pull request overview
Adds a generalized message-dispatch bridge for the WebView shell and wires in new identity + URL handling, moving away from the hardcoded audio-only switch message router.
Changes:
- Introduces
services/bridge-dispatcher.tswithregisterHandlers()anddispatch()for routing WebViewpostMessagepayloads bytype. - Adds identity and URL bridge handler modules (native + web stubs) and registers them from
app/index.tsx. - Extracts PostHog client initialization into
services/posthog.tsand reuses it in_layoutand the identity bridge.
Reviewed changes
Copilot reviewed 10 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| services/bridge-dispatcher.ts | New global registry + dispatcher for WebView message routing. |
| services/audio-bridge.native.ts | Exposes audio command handlers as a handler map for the dispatcher. |
| services/audio-bridge.web.ts | Adds stubbed getAudioHandlers() for web builds. |
| services/audio-bridge.d.ts | Declares getAudioHandlers() for platform-resolved imports. |
| services/identity-bridge.native.ts | Adds identifyUser / resetUser handlers integrating PostHog, Firebase Analytics, and Sentry. |
| services/identity-bridge.web.ts | Web stub for identity handlers. |
| services/identity-bridge.d.ts | Shared typings for identity handler export. |
| services/url-bridge.native.ts | Adds deep-link detection + openExternalURL handler using Linking/WebBrowser. |
| services/url-bridge.web.ts | Web stub for URL handlers + deep-link helpers. |
| services/url-bridge.d.ts | Shared typings for URL handler exports. |
| services/posthog.ts | Centralizes PostHog client construction. |
| app/index.tsx | Registers handler maps, switches message handling to dispatcher, emits nativeBridgeEvent, and intercepts deep links via navigation. |
| app/_layout.tsx | Removes inline PostHog construction and uses shared services/posthog. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
services/audio-bridge.native.ts
Outdated
| if (typeof msg.rate === 'number') handleSetRate(msg.rate); | ||
| }, | ||
| seekTo: (msg) => { | ||
| if (typeof msg.position === 'number') handleSeekTo(msg.position); |
There was a problem hiding this comment.
The seekTo bridge handler calls handleSeekTo(...) but does not return/await its Promise, so dispatch() will treat the handler as synchronous and won't wait for the seek to complete (and any rejection will be unhandled). Return the Promise (or await inside the handler) to preserve the previous await handleSeekTo(...) behavior from the switch-case implementation.
| if (typeof msg.position === 'number') handleSeekTo(msg.position); | |
| if (typeof msg.position === 'number') { | |
| return handleSeekTo(msg.position); | |
| } |
services/bridge-dispatcher.ts
Outdated
| msg: Record<string, unknown> | ||
| ) => void | Promise<void>; | ||
| export type BridgeHandlerMap = Record<string, BridgeHandler>; | ||
|
|
||
| type MessageHandler = ( | ||
| msg: Record<string, unknown>, | ||
| sendToWebView: SendToWebView | ||
| ) => void | Promise<void>; | ||
|
|
||
| const handlers = new Map<string, MessageHandler>(); | ||
|
|
||
| export function registerHandlers( | ||
| map: Record<string, MessageHandler> |
There was a problem hiding this comment.
BridgeHandler/BridgeHandlerMap are defined as handlers that only accept (msg), but registerHandlers/dispatch are built around MessageHandler(msg, sendToWebView). This mismatch makes it impossible to type a handler that needs sendToWebView, and encourages passing a BridgeHandlerMap into registerHandlers even though its signature doesn't reflect how handlers are invoked. Consider exporting a single handler type (e.g., MessageHandler) and using that consistently, or making sendToWebView an optional second parameter on BridgeHandler.
| msg: Record<string, unknown> | |
| ) => void | Promise<void>; | |
| export type BridgeHandlerMap = Record<string, BridgeHandler>; | |
| type MessageHandler = ( | |
| msg: Record<string, unknown>, | |
| sendToWebView: SendToWebView | |
| ) => void | Promise<void>; | |
| const handlers = new Map<string, MessageHandler>(); | |
| export function registerHandlers( | |
| map: Record<string, MessageHandler> | |
| msg: Record<string, unknown>, | |
| sendToWebView?: SendToWebView | |
| ) => void | Promise<void>; | |
| export type BridgeHandlerMap = Record<string, BridgeHandler>; | |
| const handlers = new Map<string, BridgeHandler>(); | |
| export function registerHandlers( | |
| map: BridgeHandlerMap |
| const msg: { type: string; [key: string]: unknown } = JSON.parse(raw); | ||
| const handler = handlers.get(msg.type); | ||
| if (!handler) { | ||
| console.warn(`[bridge] unknown message type: ${msg.type}`); |
There was a problem hiding this comment.
dispatch() assumes JSON.parse(raw) returns an object with a string type. If the WebView sends malformed JSON or null, this can throw or lead to confusing logs. Add a small runtime guard (and ideally catch JSON.parse errors here) so dispatch can safely ignore/diagnose invalid messages without relying on callers to wrap it.
| const msg: { type: string; [key: string]: unknown } = JSON.parse(raw); | |
| const handler = handlers.get(msg.type); | |
| if (!handler) { | |
| console.warn(`[bridge] unknown message type: ${msg.type}`); | |
| let msg: { type: string; [key: string]: unknown }; | |
| try { | |
| msg = JSON.parse(raw); | |
| } catch (error) { | |
| console.warn('[bridge] failed to parse message from WebView', { | |
| raw, | |
| error, | |
| }); | |
| return; | |
| } | |
| if (!msg || typeof msg !== 'object') { | |
| console.warn('[bridge] received non-object message from WebView', { | |
| raw, | |
| parsed: msg, | |
| }); | |
| return; | |
| } | |
| const type = (msg as { type?: unknown }).type; | |
| if (typeof type !== 'string' || !type) { | |
| console.warn('[bridge] received message without valid string "type"', { | |
| raw, | |
| parsed: msg, | |
| }); | |
| return; | |
| } | |
| const handler = handlers.get(type); | |
| if (!handler) { | |
| console.warn(`[bridge] unknown message type: ${type}`); |
services/url-bridge.native.ts
Outdated
|
|
||
| try { | ||
| if (isDeepLink(url)) { | ||
| await Linking.openURL(url); |
There was a problem hiding this comment.
openExternalURL duplicates deep-link opening logic by calling Linking.openURL directly. Since openDeepLink() is already exported (and used by WebView navigation interception), consider calling openDeepLink(url) here to keep deep-link behavior centralized and consistent.
| await Linking.openURL(url); | |
| await openDeepLink(url); |
- Remove unused sendToWebView parameter from dispatch() - Consolidate MessageHandler into BridgeHandler for consistent typing - Return handleSeekTo promise so dispatch properly awaits it - Use openDeepLink() in URL handler to centralize deep-link logic
No description provided.