- What you get
- Install: Expo
- Install: bare
- Configure and present
- Context
- Accounts
- Push
- Email links
- Let your agent do it
What you get, and what it needs
@devreply/react-native is a thin TurboModule over DevReply's native iOS and Android SDKs. The JavaScript only passes calls through, so your users get the same chat as a native app: start buttons (bug, billing, idea, question), their conversations, photos and files, name first, optional email, and “we got it” with your reply time. An unread bubble floats over your app, and the unread count is available in JavaScript.
It needs iOS 17, Android 8 (API 26) and React Native 0.80+ with the New Architecture, the default since 0.76. One package covers both kinds of app. It doesn't run in Expo Go, because it has native code: use a development build.
Install in an Expo app
npx expo install @devreply/react-native
Add the config plugin to app.json. It sets iOS 17 and Android API 26, and adds JitPack, where the Android SDK comes from:
{ "expo": { "plugins": ["@devreply/react-native"] } }
Then rebuild:
npx expo prebuild && npx expo run:ios # or run:android, or a development build with EAS
Install in a bare React Native app
npm install @devreply/react-native
- iOS: in
ios/Podfilesetplatform :ios, '17.0', set the app target's Minimum Deployments to 17.0 in Xcode, thencd ios && pod install. - Android: in
android/build.gradlesetminSdkVersion = 26. The package adds the JitPack repository itself. If yoursettings.gradleforbids project repositories, addmaven { url 'https://jitpack.io' }to itsdependencyResolutionManagement.repositories. - Rebuild the app (
npx react-native run-ios/run-android).
Configure once, present from a button
Create an app in the DevReply dashboard. It gives you a public key per platform, starting with pk_. Public keys are safe to ship; a secret key (sk_…) never goes in an app.
import DevReply from '@devreply/react-native'
// Once, at startup: your public keys from the dashboard (safe to ship). Never a secret key (sk_…).
DevReply.configure({ ios: 'pk_…', android: 'pk_…' })
// From any button
DevReply.present() // or present('bug')
A “Message the developer” button is then one line of JSX:
<Button title="Message the developer" onPress={() => DevReply.present()} />
The optional calls: a name and email so the user doesn't type them, attributes your team sees next to every conversation, and the unread count if you draw your own badge.
DevReply.setUser({ name: 'Ana', email: '[email protected]' })
DevReply.setAttributes({ plan: 'pro', trial: false })
const sub = DevReply.addUnreadListener((count) => setBadge(count)) // sub.remove() to stop
DevReply.showsUnreadBubble = false // if you show the count yourself
If your team switches the chat off in the dashboard, present returns false and shows nothing, and DevReply.isAvailable tells you up front so you can hide your own button. No release needed.
Open it with a draft and context
From a failed purchase, open a billing conversation with text already in the composer and context for your team. The user sees the draft and sends it; nothing is sent on its own. The attributes show in your inbox as “Opened with” (text, number or true/false, up to 20):
DevReply.present('billing', { message: "My purchase didn't go through", attributes: { source: 'paywall', rc_error_code: e.code } })
Sign-in, sign-out and account deletion
DevReply.login(user.id) // after sign-in: your own id for the user, never an email or a secret
DevReply.logout() // on every sign-out and account switch
const ok = await DevReply.deleteUser() // in your delete-account flow; false = queued, retried until done
logout makes the device forget the chat, so the next person starts empty; the conversations stay with your team. deleteUser deletes the user's data, as Apple requires account deletion in the app, and keeps retrying if DevReply can't be reached.
Push notifications
Your app keeps its own push setup and passes DevReply the token, the taps and, on Android, DevReply's messages. DevReply never takes over your notification handling. With @react-native-firebase/messaging (its modular API; the old messaging() calls crash on v26):
import {
getMessaging, getAPNSToken, getToken, onTokenRefresh, onMessage, setBackgroundMessageHandler,
getInitialNotification, onNotificationOpenedApp,
} from '@react-native-firebase/messaging'
const m = getMessaging()
// iOS: the APNs token; Android: the FCM token.
const token = Platform.OS === 'ios' ? await getAPNSToken(m) : await getToken(m)
if (token) DevReply.registerPushToken(token)
onTokenRefresh(m, (t) => { if (Platform.OS === 'android') DevReply.registerPushToken(t) })
// Android: DevReply shows its own notification; a tap opens the conversation.
onMessage(m, async (msg) => { if (DevReply.handlePush(msg.data)) return /* your pushes */ })
setBackgroundMessageHandler(m, async (msg) => { if (DevReply.handlePush(msg.data)) return }) // index.js
// Taps (iOS, and Android notifications your library shows), including the one that launched the app.
getInitialNotification(m).then((n) => n && DevReply.handleNotificationOpened(n.data))
onNotificationOpenedApp(m, (n) => { if (DevReply.handleNotificationOpened(n.data)) return /* yours */ })
With expo-notifications, register the token on iOS with DevReply.registerPushToken((await Notifications.getDevicePushTokenAsync()).data) and pass the whole tap response; DevReply finds its keys inside it:
Notifications.getLastNotificationResponseAsync().then((r) => r && DevReply.handleNotificationOpened(r))
Notifications.addNotificationResponseReceivedListener((r) => { if (DevReply.handleNotificationOpened(r)) return /* yours */ })
handleNotificationOpened returns false for your own notifications. Upload your push keys in the dashboard (the APNs key; the Firebase service account for Android). The chat asks for the notification permission only after the user's first message, and the dashboard's push card shows “✓ Taps open the chat” once a tap opened a conversation.
Replies from email
On Pro, users who left an email and haven't read a reply get it by email, with a “Reply in the app” button that opens yourapp://devreply?devreply=<conversation>. Give the app a URL scheme (Expo: "scheme": "yourapp" in app.json) and set yourapp://devreply as the deep link in the dashboard. The package listens for these links itself after configure. With expo-router, add app/+native-intent.tsx:
export function redirectSystemPath({ path }) { return path.includes('devreply=') ? '/' : path }
If your router swallows links first, pass them on with DevReply.handle(url). In bare React Native, keep android:launchMode="singleTask" on MainActivity, and if your iOS AppDelegate handles URLs itself, end with React Native's RCTLinkingManager.
Let your coding agent install it
Each app in DevReply has a setup guide written for coding agents, with your keys in it: the dashboard, the app → Settings → “Add DevReply to your app”. Give it to Claude Code, Cursor or Codex in your repository and it does the steps above, then checks the install landed. Or connect the agent over MCP, where the setup tools are free on every plan:
claude mcp add --transport http devreply https://api.devreply.com/mcp
Everything an agent needs to know is in llms.txt.