Skip to content

🏗️ Add generalized WebView bridge with identity and URL handlers#29

Draft
williamchong wants to merge 3 commits intolikecoin:mainfrom
williamchong:feature/event
Draft

🏗️ Add generalized WebView bridge with identity and URL handlers#29
williamchong wants to merge 3 commits intolikecoin:mainfrom
williamchong:feature/event

Conversation

@williamchong
Copy link
Copy Markdown
Member

No description provided.

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.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.ts with registerHandlers() and dispatch() for routing WebView postMessage payloads by type.
  • Adds identity and URL bridge handler modules (native + web stubs) and registers them from app/index.tsx.
  • Extracts PostHog client initialization into services/posthog.ts and reuses it in _layout and 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.

if (typeof msg.rate === 'number') handleSetRate(msg.rate);
},
seekTo: (msg) => {
if (typeof msg.position === 'number') handleSeekTo(msg.position);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
if (typeof msg.position === 'number') handleSeekTo(msg.position);
if (typeof msg.position === 'number') {
return handleSeekTo(msg.position);
}

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +15
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>
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +29
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}`);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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}`);

Copilot uses AI. Check for mistakes.

try {
if (isDeepLink(url)) {
await Linking.openURL(url);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
await Linking.openURL(url);
await openDeepLink(url);

Copilot uses AI. Check for mistakes.
- 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
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