npm package: @pncl/react-native-webview
Description: A React Native WebView wrapper that auto-requests native camera/microphone permissions and overrides navigator.permissions.query so web apps see the correct grant state.
Latest version: 1.0.1

# @pncl/react-native-webview

A WebView component for React Native, designed to maximize [Pencil Spaces](https://pencilapp.com) integration. It ensures that audio and video work seamlessly inside the WebView with a simple one-prop integration on both iOS and Android.

## Why this exists

Embedding Pencil Spaces (or any app that uses camera and microphone) inside a React Native WebView requires two things to go right:

1. The device must grant camera and microphone permissions to the native app
2. The web app must be told that those permissions are already granted

Without (2), the web app calls `navigator.permissions.query()` and receives `'prompt'` or `'denied'` — even after the user has already accepted the native permission dialogs. This breaks audio/video flows entirely.

`@pncl/react-native-webview` handles both steps automatically.

---

## Installation

```sh
npm install @pncl/react-native-webview
```

`react-native-webview` is included as a dependency and installed automatically. No config plugins or manual linking required.

---

## Permissions requested on mount

When the WebView mounts, it calls the required `requestPermissions` prop to request camera and microphone permissions. This is consistent on both iOS and Android — you supply the callback using whichever permissions library your project already uses.

| Permission | iOS | Android |
|---|---|---|
| Camera | Requested via `requestPermissions` prop | Requested via `requestPermissions` prop |
| Microphone | Requested via `requestPermissions` prop | Requested via `requestPermissions` prop |

> Your app's `Info.plist` (iOS) and `AndroidManifest.xml` (Android) must still declare `NSCameraUsageDescription`, `NSMicrophoneUsageDescription`, and the corresponding Android `<uses-permission>` entries. These are standard requirements for any app that uses camera or microphone.

---

## Usage

### With Expo (expo-camera + expo-av)

```tsx
import WebView from '@pncl/react-native-webview';
import { Camera } from 'expo-camera';
import { Audio } from 'expo-av';

async function requestPermissions() {
  const cam = await Camera.requestCameraPermissionsAsync();
  const mic = await Audio.requestPermissionsAsync();
  return {
    camera: cam.status === 'granted',
    microphone: mic.status === 'granted',
  };
}

export default function SpaceScreen({ url }: { url: string }) {
  return (
    <WebView
      source={{ uri: url }}
      style={{ flex: 1 }}
      requestPermissions={requestPermissions}
    />
  );
}
```

### With react-native-permissions

```tsx
import { Platform } from 'react-native';
import WebView from '@pncl/react-native-webview';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';

async function requestPermissions() {
  const [cam, mic] = await Promise.all([
    request(Platform.select({
      ios: PERMISSIONS.IOS.CAMERA,
      android: PERMISSIONS.ANDROID.CAMERA,
    })),
    request(Platform.select({
      ios: PERMISSIONS.IOS.MICROPHONE,
      android: PERMISSIONS.ANDROID.RECORD_AUDIO,
    })),
  ]);
  return {
    camera: cam === RESULTS.GRANTED,
    microphone: mic === RESULTS.GRANTED,
  };
}

export default function SpaceScreen({ url }: { url: string }) {
  return (
    <WebView
      source={{ uri: url }}
      style={{ flex: 1 }}
      requestPermissions={requestPermissions}
    />
  );
}
```

---

## Named import

Both default and named imports work:

```tsx
import WebView from '@pncl/react-native-webview';
// or
import { WebView } from '@pncl/react-native-webview';
```

---

## Props

`@pncl/react-native-webview`'s `WebView` accepts all props from `react-native-webview`'s `WebView`, plus the following:

| Prop | Type | Default | Description |
|---|---|---|---|
| `requestPermissions` | `() => Promise<{ camera: boolean; microphone: boolean }>` | **required** | Callback to request native camera/microphone permissions on mount. Called on both iOS and Android. Use expo-camera/expo-av or react-native-permissions to implement — see examples above. |
| `permissionsOverride` | `boolean` | `true` | Controls whether the WebView receives the correct permission state from the native layer. Disable only if you are managing `navigator.permissions` yourself. |

---

## Forwarding refs

Refs are forwarded to the underlying WebView instance, so you can still call `.reload()`, `.goBack()`, etc.

```tsx
import { useRef } from 'react';
import WebView from '@pncl/react-native-webview';
import type { WebView as WebViewRef } from 'react-native-webview';

async function requestPermissions() {
  // ... your permissions logic
  return { camera: true, microphone: true };
}

export default function SpaceScreen({ url }: { url: string }) {
  const ref = useRef<WebViewRef>(null);

  return (
    <WebView
      ref={ref}
      source={{ uri: url }}
      style={{ flex: 1 }}
      requestPermissions={requestPermissions}
    />
  );
}
```

---

## License

MIT