Appearance
React Native
@aw-chat/react-native renders the SDK with native components only — View, FlatList, TextInput, Modal, react-native-svg. No WebView.
Install
bash
pnpm add @aw-chat/react-native react-native-svg react-native-fast-imagereact-native-svg— peer (already present in Expo apps).react-native-fast-image— peer. The SDK renders all images through it: it plays the animated-WebP channel emoji on iOS + Android and disk-caches remote feed photos. It uses noexpo-imageand adds no other native image library, so shipping SDK updates over-the-air needs no native rebuild (the host binary just has to includereact-native-fast-image).
Mount
tsx
import { AWChat } from "@aw-chat/react-native";
export function SupportScreen() {
return (
<AWChat
userId={user.id}
sessionToken={user.token}
serverUrl="https://chat.example.com"
telemetry={{
appVersion: "6.1.4",
hardwareId: "iPhone15,2",
osVersion: "iOS 17.4",
deviceLang: "ru",
}}
fcmToken={fcmToken}
onPickFiles={pickFiles}
theme="light"
locale="ru"
onSessionExpired={refreshToken}
/>
);
}Render inside a flex container (e.g. SafeAreaView with flex: 1).
Attachments (onPickFiles)
On native there is no hidden <input type="file">. The host must pass onPickFiles or the attach control stays hidden and uploads cannot start.
Use a document picker (not photos-only) and honour opts.accept / opts.multiple so chat matches web/TMA: JPEG/PNG/GIF/WEBP/BMP, PDF, TXT, MP4. Comments pass image-only accept. Tapping a non-image chip opens/downloads it via the OS (Linking.openURL), same as web.
tsx
import * as DocumentPicker from "expo-document-picker";
onPickFiles={async (opts) => {
const r = await DocumentPicker.getDocumentAsync({
multiple: opts.multiple,
copyToCacheDirectory: true,
type: [...opts.accept],
});
if (r.canceled) return [];
return r.assets.slice(0, opts.maxFiles).map((a) => ({
uri: a.uri,
name: a.name,
type: a.mimeType ?? "application/octet-stream",
size: a.size ?? 0,
}));
}}Return { uri, name, type, size }[]. Shared MIME lists: CHAT_ACCEPT_MIMES / COMMENT_ACCEPT_MIMES from @aw-chat/react-native (or @aw-chat/core). See Props.
Navigation
Two modes (do not mix them):
- All-in-one
<AWChat />(default) — SDK owns an internal navigator. Fast to integrate; iOS back is an edge swipe (not the system interactive pop). - Host stack — you register Support screens in your
react-navigationStack. System iOS swipe-back works. Use this when you need native gesture feel.
iPhone swipe-back behaves differently from Android?
This is the most common integration issue, and it is not an SDK bug — it is a gesture conflict. A react-navigation native stack enables the interactive swipe-back gesture by default on iOS, but not on Android.
So when <AWChat /> sits on a host Stack.Screen and you leave that gesture on, an iPhone left-edge swipe is caught by the host stack and pops the entire Support screen (chat → your app's page) in one swipe — instead of the SDK's chat → hub. Android has no host edge-swipe, so the SDK's own back runs there and it "just works". That asymmetry is exactly the "swap back works strange, different than Android" report.
Fix depends on which mode you want:
- Staying all-in-one → turn the host gesture off on the Support screen (set
gestureEnabled: falsein itsStack.Screenoptions). The SDK's internal edge-swipe then handleschat → hub, andonClosedismisses Support at the hub. One line. - Want the real native iOS interactive swipe → use Host stack mode below. Each Support screen becomes its own
Stack.ScreenwithgestureEnabled: true, so iOS pops natively — and you must wireemitSupportFocus(see below), or the native swipe will reach the hub without restoring your header/tab bar.
Do not leave the host gesture enabled over an all-in-one <AWChat />.
All-in-one (default)
The SDK owns internal navigation (hub → chat / ticket / complaint / channel). You do not wire a navigator for inner screens.
Use onRouteChange to hide the host tab bar / header when the user leaves the hub (name !== "hub"), so chat content can go fullscreen. The callback also fires { name: "hub" } when <AWChat /> unmounts (restore chrome). Error / welcome swaps inside the widget do not restore chrome.
Back is handled inside the SDK:
- Android hardware / system gesture back pops overlays → inner screens → hub.
- iOS (and Android) left-edge swipe does the same.
- At the hub (or welcome gate), back fires
onClose— pass it so the host can dismiss the Support screen instead of closing the whole app.
tsx
<Stack.Screen
name="Support"
options={{ gestureEnabled: false }} // disable host swipe-back while Support is open
/>
<AWChat
onClose={() => navigation.goBack()}
onRouteChange={({ name }) => setChromeHidden(name !== "hub")}
/* … */
/>Host stack (system iOS swipe)
Mount AWSupportProvider once above a nested Support Stack (shared store / session). Map SDK routes to your screen names via AWSupportNavigation:
| SDK export | Suggested Stack.Screen | Params |
|---|---|---|
AWSupportHub | SupportHub | — |
AWSupportChat | SupportChat | prefill? |
AWSupportTickets | SupportTickets | openId? |
AWSupportNewTicket | SupportNewTicket | — |
AWSupportChannel | SupportChannel | channelId |
AWSupportSoon | SupportSoon | title |
Comments and complaint sheets stay SDK overlays (not stack routes). While an overlay is open, disable the host gesture or the swipe will pop the screen underneath.
tsx
import {
AWSupportProvider,
AWSupportHub,
AWSupportChat,
AWSupportTickets,
AWSupportNewTicket,
AWSupportChannel,
AWSupportSoon,
type AWSupportNavigation,
type AWSupportRoute,
teardown,
} from "@aw-chat/react-native";
const supportNav: AWSupportNavigation = {
navigate: (route: AWSupportRoute) => {
switch (route.name) {
case "hub":
navigation.navigate("SupportHub");
break;
case "chat":
navigation.navigate("SupportChat", route.params);
break;
case "tickets":
navigation.navigate("SupportTickets", route.params);
break;
case "newTicket":
navigation.navigate("SupportNewTicket");
break;
case "channel":
navigation.navigate("SupportChannel", route.params);
break;
case "soon":
navigation.navigate("SupportSoon", route.params);
break;
}
},
goBack: () => navigation.goBack(),
};
// System swipe keeps lower screens mounted — wire focus so chrome/tab bar
// restores on pop (SDK back button already emits via the navigation bridge).
import { emitSupportFocus } from "@aw-chat/react-native";
const focus = (name: "hub" | "chat" | "tickets" | "newTicket" | "channel" | "soon") => ({
focus: () => emitSupportFocus(name),
});
// Provider wraps the Support stack (not each screen).
<AWSupportProvider
navigation={supportNav}
onClose={() => navigation.goBack()}
onRouteChange={({ name }) => setChromeHidden(name !== "hub")}
onOverlayChange={(overlay) => setGestureEnabled(overlay == null)}
userId={…}
sessionToken={…}
serverUrl={…}
telemetry={…}
>
<Stack.Navigator>
<Stack.Screen name="SupportHub" component={AWSupportHub} options={{ gestureEnabled: true }} listeners={focus("hub")} />
<Stack.Screen name="SupportChat" options={{ gestureEnabled: true }} listeners={focus("chat")}>
{({ route }) => <AWSupportChat prefill={route.params?.prefill} />}
</Stack.Screen>
<Stack.Screen name="SupportTickets" options={{ gestureEnabled: true }} listeners={focus("tickets")}>
{({ route }) => <AWSupportTickets openId={route.params?.openId} />}
</Stack.Screen>
<Stack.Screen name="SupportNewTicket" component={AWSupportNewTicket} listeners={focus("newTicket")} />
<Stack.Screen name="SupportChannel" options={{ gestureEnabled: true }} listeners={focus("channel")}>
{({ route }) => <AWSupportChannel channelId={route.params.channelId} />}
</Stack.Screen>
<Stack.Screen name="SupportSoon" listeners={focus("soon")}>
{({ route }) => <AWSupportSoon title={route.params.title} />}
</Stack.Screen>
</Stack.Navigator>
</AWSupportProvider>
// Deep link: navigate into the stack instead of remounting with initialRoute.
// supportNav.navigate({ name: "chat" });
// supportNav.navigate({ name: "tickets", params: { openId } });
// On logout:
await teardown();Gesture rules:
- Hub / chat / tickets / channel / soon:
gestureEnabled: true(system pop). - While
onOverlayChangereports"comments"or"complaint": forcegestureEnabled: false. - Wire
listeners={focus(name)}on every screen. The iOS native swipe-back pops the screen itself — it never calls the SDK'sgoBackbridge — so withoutemitSupportFocusthe gesture reaches the hub butonRouteChangenever fires and your header/tab bar stays hidden. The SDK's own back button does not need this (it emits through the bridge); the native swipe does. - Do not mount
<AWChat />andAWSupportProviderfor the same session.
AW Channel
Native feed parity with web: posts, rich text, media, reactions, views, comments, buttons. Data for posts/comments is Postgres via your backend — not Intercom (Data model).
Push (FCM)
Pass fcmToken to register. On logout, unmount the component (or call teardown from @aw-chat/core). Map push payloads to initialRoute + targetId when remounting.
Account switch
When the signed-in userId / sessionToken changes, remount <AWChat /> (or ensure props update so the live store re-initializes). Otherwise channel/comment caches from the previous user can linger for the session.
Offline
The SDK backs off polling on network failure and shows a session-expired panel on 401. A dedicated offline panel needs optional @react-native-community/netinfo.
Expo
Works with Expo (SDK 52+). examples/react-native is an Expo app with a react-native-web preview — see Try it for how to run it.
Because react-native-fast-image is a native module, running on a device needs a custom dev build (expo prebuild + expo run:ios / run:android, or an EAS dev build) — Expo Go can't load it. Only that native module is build-time; your own JS/asset updates still ship over-the-air.