Skip to content

Commit fb29e91

Browse files
committed
feat: script method parity, permission-gating audit, and native markdown/LaTeX rendering
1 parent 453ba9b commit fb29e91

32 files changed

Lines changed: 17274 additions & 255 deletions

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
node_modules/
2+
example/node_modules/
3+
example/android/
4+
example/ios/
5+
example/.expo/
26
*.log
37
.DS_Store

‎README.md‎

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,33 @@ export default function App() {
6161
}
6262
```
6363

64-
**Embedded full-screen chat** — place it directly in your own navigation, e.g. as a "Support" screen:
64+
**Script Method (WebView Embed — 100% web widget parity)** — loads the exact production widget served at `/embed/{botId}` with pixel-for-pixel visual and feature parity, including client-side voice messages, file attachments, AI search, and CSAT:
65+
66+
```tsx
67+
import { ChattyEmbedView } from "@personaliai/react-native";
68+
69+
function SupportScreen() {
70+
return (
71+
<ChattyEmbedView
72+
botId="YOUR_BOT_ID"
73+
onReady={() => console.log("Chat ready")}
74+
onMessage={() => console.log("New message")}
75+
onClose={() => navigation.goBack()}
76+
onRequestNotificationPermission={(botName) => {
77+
// Request POST_NOTIFICATIONS contextually (Android 13+ / iOS)
78+
}}
79+
onMicPermissionNeeded={() => {
80+
// Request RECORD_AUDIO permission contextually when user taps the mic
81+
}}
82+
onLocationPermissionNeeded={() => {
83+
// Request location permission contextually when user selects "Location"
84+
}}
85+
/>
86+
);
87+
}
88+
```
89+
90+
**Native Components Chat** — renders with 100% native React Native components (`FlatList`, `View`, `Text`, `ChattyMarkdown`):
6591

6692
```tsx
6793
import { ChattyChatView } from "@personaliai/react-native";
@@ -155,6 +181,37 @@ The button color follows the active design's accent automatically — same as we
155181
/>
156182
```
157183

184+
### `ChattyEmbedView` (Script Method)
185+
186+
The official web widget hosted in a React Native `WebView`. Guaranteed pixel-for-pixel and feature-for-feature parity with the web widget, with automatic permission reflection, alert suppression, and OEM font scale isolation:
187+
188+
```tsx
189+
<ChattyEmbedView
190+
botId={string}
191+
baseUrl={string} // optional, defaults to https://chatty.personaliai.com
192+
onReady={() => void} // optional, fired when web widget finishes loading
193+
onMessage={() => void} // optional, fired on every assistant reply
194+
onClose={() => void} // optional, fired when user closes the widget / CSAT
195+
onRequestNotificationPermission={(botName) => void} // optional, fired on notification bell tap
196+
onMicPermissionNeeded={() => void} // optional, fired on mic tap if RECORD_AUDIO not granted
197+
onLocationPermissionNeeded={() => void} // optional, fired on location attach if not granted
198+
/>
199+
```
200+
201+
### `ChattyMarkdown`
202+
203+
Real CommonMark + GFM (tables, strikethrough, blockquotes, code blocks with copy) + LaTeX equation support (`$inline$` and `$$block$$`), matching web widget's feature set. Used automatically by `ChattyChatView` and available standalone:
204+
205+
```tsx
206+
import { ChattyMarkdown } from "@personaliai/react-native";
207+
208+
<ChattyMarkdown
209+
text="Solve $$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$ for $x$."
210+
color="#111827"
211+
fontSize={13}
212+
/>
213+
```
214+
158215
> [!NOTE]
159216
> This SDK renders the composer's emoji picker and attach/mic UI (with layout animation), but it
160217
> deliberately doesn't bundle a camera, photo-library, document-picker, geolocation, or
@@ -188,24 +245,40 @@ layout.
188245

189246
<br>
190247

191-
This SDK requests exactly one permission itself, and only in direct response to a button tap —
192-
never on load, never speculatively:
248+
This SDK strictly adheres to the **"Reflect, Never Request"** contract:
193249

194-
| Permission | Risk | Used for | Requested when |
250+
| Feature | Android Permission | iOS Info.plist Key | Callback |
195251
|---|---|---|---|
196-
| `POST_NOTIFICATIONS` (Android 13+) | Runtime-gated | Header bell button → local notification-permission ask | User taps the bell, only if `enableNotificationBell` (default `true`) |
197-
198-
Set `enableNotificationBell={false}` to hide the bell button entirely — the SDK then never calls
199-
`PermissionsAndroid.request` at all, so your app fully controls if/when/how notification
200-
permission is ever requested. Your app remains free to request it itself, on its own schedule,
201-
for its own purposes.
202-
203-
**Camera, photo library, and microphone are never requested by this SDK at all** — as noted
204-
above, `onCameraPress` / `onPhotoLibraryPress` / `onMicPress` are plain callbacks; your app
205-
supplies its own picker/recorder package (`expo-image-picker`, `react-native-image-picker`,
206-
`expo-av`, etc.) and that package's own permission flow runs entirely under your control. This
207-
SDK adds no `CAMERA`/`RECORD_AUDIO`/photo-library manifest entries or Info.plist keys of its own —
208-
whatever picker/recorder you choose documents what it needs.
252+
| **Voice Notes (`getUserMedia`)** | `android.permission.RECORD_AUDIO`<br>`android.permission.MODIFY_AUDIO_SETTINGS` | `NSMicrophoneUsageDescription` | `onMicPermissionNeeded` |
253+
| **Location Share (`geolocation`)** | `android.permission.ACCESS_COARSE_LOCATION`<br>`android.permission.ACCESS_FINE_LOCATION` | `NSLocationWhenInUseUsageDescription` | `onLocationPermissionNeeded` |
254+
| **Notifications** | `android.permission.POST_NOTIFICATIONS` | N/A (UserNotifications API) | `onRequestNotificationPermission` |
255+
256+
In `ChattyEmbedView`, the SDK checks permissions read-only and reflects the status to the web widget. If a user taps the mic or location attach without permissions, `onMicPermissionNeeded` or `onLocationPermissionNeeded` fires so you can present your own rationale and request the permission. Once granted, returning to the view or subsequent taps immediately succeed.
257+
258+
#### Android Setup (`android/app/src/main/AndroidManifest.xml`)
259+
```xml
260+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
261+
<uses-permission android:name="android.permission.INTERNET" />
262+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
263+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
264+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
265+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
266+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
267+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
268+
</manifest>
269+
```
270+
271+
#### iOS Setup (`ios/Podfile` / `Info.plist`)
272+
```xml
273+
<key>NSMicrophoneUsageDescription</key>
274+
<string>We need access to your microphone to record voice messages for support.</string>
275+
<key>NSLocationWhenInUseUsageDescription</key>
276+
<string>We need your location to share with support.</string>
277+
<key>NSCameraUsageDescription</key>
278+
<string>We need access to your camera to attach photos.</string>
279+
<key>NSPhotoLibraryUsageDescription</key>
280+
<string>We need access to your photo library to attach photos.</string>
281+
```
209282

210283
</details>
211284

‎dist/ChattyChatView.d.ts‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import { ChattyMessage, UseChattyChatOptions } from "./useChattyChat";
3+
import { ChattyMarkdown } from "./ChattyMarkdown";
34
export interface ChattyChatViewProps extends UseChattyChatOptions {
45
/** Called once theme/config has loaded and the chat is ready to use. */
56
onReady?: () => void;
@@ -34,23 +35,31 @@ export interface ChattyChatViewProps extends UseChattyChatOptions {
3435
* dashboard has voice enabled). This SDK doesn't bundle a voice-call implementation
3536
* (that's a separate LiveKit integration) — wire this up if your app has one. */
3637
onVoiceCallPress?: () => void;
37-
/** Called when the header's notification-bell button is tapped, after the OS notification
38-
* permission has been requested on Android (PermissionsAndroid, built into RN core — no
39-
* extra dependency). There's no cross-platform JS API for this on iOS; request it yourself
40-
* (e.g. via expo-notifications or your own native module) before/inside this callback.
41-
* Native apps still need their own push infrastructure (FCM/APNs) to actually *deliver* a
42-
* notification while backgrounded — this SDK only handles the permission ask. */
38+
/** Called when the header's notification-bell button is tapped. This SDK never calls
39+
* PermissionsAndroid.request (or any permission prompt) itself — it only reflects
40+
* POST_NOTIFICATIONS' already-granted state (via PermissionsAndroid.check on Android 13+,
41+
* re-checked on app foreground) to decide whether to show the bell at all. Request the
42+
* permission yourself (e.g. via expo-notifications, your own native module, or
43+
* PermissionsAndroid.request) wherever your app's onboarding flow calls for it — the bell
44+
* then appears once granted. Native apps still need their own push infrastructure
45+
* (FCM/APNs) to actually *deliver* a notification while backgrounded. */
4346
onNotificationBellPress?: () => void;
4447
/** Renders a close (✕) button in the header when provided — pass this instead of drawing
4548
* your own close bar above ChattyChatView (e.g. in a modal wrapper), so there's one header,
4649
* not two stacked ones. ChattyLauncher already does this for you. */
4750
onClose?: () => void;
48-
/** Shows the header's notification-bell button and, on Android, requests
49-
* POST_NOTIFICATIONS (a runtime permission on API 33+) when tapped. Set `false` to hide the
50-
* button entirely — the SDK then never calls PermissionsAndroid.request at all, so your app
51-
* fully controls if/when/how notification permission is ever requested. Default `true`.
51+
/** Set `false` to force-hide the header's notification-bell button regardless of permission
52+
* state. Left at the default `true`, the bell only shows once POST_NOTIFICATIONS is already
53+
* granted (checked read-only, never requested by this SDK — see onNotificationBellPress).
5254
* Camera/photo/mic aren't listed here because this SDK never requests those permissions
53-
* itself — see onCameraPress/onPhotoLibraryPress/onMicPress above. */
55+
* itself either — see onCameraPress/onPhotoLibraryPress/onMicPress above. */
5456
enableNotificationBell?: boolean;
5557
}
58+
/** Mirrors standalone.tsx's LAUNCHER_ICONS map + its MessageCircle fallback:
59+
* logo(no logoUrl)/custom(no avatarUrl)/anything unmatched falls back to a
60+
* chat-bubble glyph, NOT a robot — web only shows the robot for avatar_icon
61+
* === "bot" specifically. */
62+
export declare function avatarGlyph(avatarIcon: string | null | undefined): string;
5663
export declare function ChattyChatView(props: ChattyChatViewProps): React.JSX.Element;
64+
/** Backward compatibility alias for ChattyMarkdown. */
65+
export declare const SimpleMarkdown: typeof ChattyMarkdown;

0 commit comments

Comments
 (0)