React Native SDK
Complete integration guide for the Encatch React Native SDK — in-app feedback and survey collection for React Native and Expo apps
The Encatch React Native SDK lets you collect in-app feedback and surveys in React Native and Expo apps. Display forms as a modal WebView overlay or inline in your layout, identify users, track screens and events, and submit responses to the Encatch backend.
Overview
- Package:
@encatch/react-native-sdk - Version: 1.4.2
- Platforms: Android, iOS
- Repository: github.com/get-encatch/react-native-sdk
Installation
npm install @encatch/react-native-sdkyarn add @encatch/react-native-sdkpnpm add @encatch/react-native-sdkThe SDK also requires these peer dependencies in your app:
npm install react-native-webview react-native-safe-area-context @react-native-async-storage/async-storageOptional peers for automatic screen tracking and device metadata: @react-navigation/native, expo-router, expo-application, expo-device, expo-localization, react-native-device-info, and react-native-localize.
Quick Start
1. Initialization
Wrap your app's root with EncatchProvider to initialize the SDK, start a session, and optionally enable automatic screen tracking. Mount EncatchWebView once at the root for modal forms.
import { EncatchProvider, EncatchWebView } from '@encatch/react-native-sdk';
export default function App() {
return (
<EncatchProvider
apiKey="your-api-key"
navigationType="expo-router">
<EncatchWebView />
{/* Your app content */}
</EncatchProvider>
);
}Use the useEncatch() hook inside any child component to access the SDK API. For inline forms, mount EncatchInlineForm in your screen component tree separately.
Pass an optional config object to customize SDK behavior:
<EncatchProvider
apiKey="your-api-key"
navigationType="react-navigation"
skippedRoutes={['/login', '/splash']}
config={{
theme: 'system',
debugMode: true,
isFullScreen: false,
apiBaseUrl: 'https://app.encatch.com',
appVersion: '1.2.3',
onBeforeShowForm: async (payload) => {
// Return false to prevent form from showing
return true;
},
}}>
<EncatchWebView />
<App />
</EncatchProvider>Prop
Type
EncatchProvider props:
| Prop | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Your Encatch API key (required) |
config | EncatchConfig | — | SDK configuration |
navigationType | 'expo-router' | 'react-navigation' | null | null | Enable automatic screen tracking |
skippedRoutes | string[] | [] | Routes to skip for screen tracking |
2. Identify users
Identify the current user. The userName is required (can be a username, email, or unique identifier). Traits and options are optional.
Username format
userName must be an ASCII identifier: 1–50 characters, using only letters A–Z / a–z, digits 0–9, and ., _, @, -. Spaces and non-English characters (Unicode, accented letters, emoji, etc.) are not supported. Use an email address, internal user ID, or ASCII username — for example user@example.com or user_123. To store a display name in another language, pass it as a trait instead (e.g. $set: { display_name: '…' }).
identifyUser('user@example.com');import { useEncatch } from '@encatch/react-native-sdk';
const { identifyUser } = useEncatch();
identifyUser('user@example.com', {
$set: { name: 'Alice', plan: 'team' },
});identifyUser('user@example.com', {
$set: { name: 'Alice', plan: 'team' },
$setOnce: { firstSeen: new Date().toISOString() },
$increment: { loginCount: 1 },
$decrement: { credits: 5 },
$unset: ['trialEndDate'],
});Prop
Type
User traits support the following operations:
| Operation | Description |
|---|---|
$set | Set user attributes (overwrites existing values) |
$setOnce | Set user attributes only if they don't already exist |
$increment | Increment numeric user attributes |
$decrement | Decrement numeric user attributes |
$unset | Remove user attributes |
Recommended
Using the secure option with a server-generated signature is recommended to verify that identification requests come from your backend. Keep your secret key on the server only — never expose it in client-side code.
Pass a server-generated HMAC signature so Encatch can validate the request. generatedDateTimeinUTC must be milliseconds since the Unix epoch (for example String(Date.now()) from your server). When your publishable key has a session timeout, use the same value in HMAC-SHA256(userName + epochMs, secretKey). It is sent as the X-User-Signature-Time header and limits the signature's lifespan.
identifyUser('user@example.com', undefined, {
secure: {
signature: 'your-hmac-signature',
generatedDateTimeinUTC: '1741867200000', // ms since epoch (2025-03-13T12:00:00Z)
},
});3. Show a form manually
Show a specific form by slug or ID.
import { useEncatch } from '@encatch/react-native-sdk';
const { showForm } = useEncatch();
showForm('feedback-form');
showForm('feedback-form', { reset: 'always' });Prop
Type
Prop
Type
| Reset mode | Behavior |
|---|---|
'always' | Reset pre-fill and response data on every form display |
'on-complete' | Reset only after the form is completed |
'never' | Never reset response data |
Pass caller context when showing a form:
showForm('feedback-form', {
reset: 'always',
context: { plan: 'team', feature: 'checkout' },
});Other actions
Set the user's preferred language.
const { setLocale } = useEncatch();
setLocale('fr');Set the user's country.
const { setCountry } = useEncatch();
setCountry('FR'); // ISO 3166 country codeSet the theme for forms and surveys.
const { setTheme } = useEncatch();
setTheme('dark');
setTheme('light');
setTheme('system'); // Follows system preferenceconst { trackEvent } = useEncatch();
trackEvent('button_clicked');const { trackScreen } = useEncatch();
trackScreen('HomeScreen');Set navigationType on EncatchProvider for automatic screen tracking:
<EncatchProvider apiKey="..." navigationType="expo-router">
{/* or navigationType="react-navigation" */}
</EncatchProvider>Use skippedRoutes to exclude routes such as login or splash screens.
Subscribe to form lifecycle events. Returns an unsubscribe function.
const { on } = useEncatch();
useEffect(() => {
const unsubscribe = on((eventType, payload) => {
console.log('Event:', eventType, payload.data);
});
return unsubscribe;
}, [on]);| Event | Description |
|---|---|
form:show | Fired when a form is displayed |
form:started | Fired when a user starts interacting |
form:submit | Fired when a form is submitted |
form:complete | Fired when a form is fully completed |
form:close | Fired when a form is closed |
form:dismissed | Fired when a form is dismissed without completion |
form:error | Fired when an error occurs |
form:section:change | Fired when the visible section changes |
form:answered | Fired when a question is answered |
form:remindmelater | Fired when the user taps "Remind me later" |
form:ctaTriggered | Fired when a completion CTA is triggered on thank-you or exit screens |
Handle completion CTAs (in-app navigation, internal redirect, or external browser) via form:ctaTriggered. Configure actions in the form builder — see Call to action. The SDK closes the form overlay after emitting the event — your app handles in-app navigation:
const { on } = useEncatch();
const router = useRouter(); // expo-router
useEffect(() => {
const handler = (eventType, payload) => {
if (eventType !== 'form:ctaTriggered') return;
const action = payload.data?.action;
if (action !== 'app_navigate') return;
const route = payload.data?.route as string | undefined;
// Map route strings to your app's navigation paths
if (route === 'billing' || route === 'billing/upgrade') {
router.push('/billing');
}
};
const unsubscribe = on(handler);
return unsubscribe;
}, [on, router]);For in-app navigation, the SDK closes the form overlay after emitting the event and expects your app to perform navigation. For URL redirect actions, the SDK opens the URL and closes the form automatically.
Pre-fill a form response before showing a form. questionId may be a question UUID or a question slug.
const { addToResponse, showForm } = useEncatch();
addToResponse('question_id', 'pre-filled value');
addToResponse('choice_question_id', ['option-a', 'option-b']);
showForm('your-form-slug');Dismiss the currently displayed form.
const { dismissForm } = useEncatch();
dismissForm();
// Or dismiss a specific form configuration:
dismissForm('config-id');Use onBeforeShowForm in EncatchProvider config to conditionally block forms from showing.
<EncatchProvider
apiKey="your-api-key"
config={{
onBeforeShowForm: async (payload) => {
// Inspect payload.formId, payload.formConfig, payload.triggerType, etc.
if (payload.triggerType === 'automatic' && someCondition) {
return false; // Block this form
}
return true; // Allow
},
}}>
<EncatchWebView />
<App />
</EncatchProvider>EncatchProvider starts a session automatically after initialization. You can also control session lifecycle manually via the Encatch singleton:
import { Encatch } from '@encatch/react-native-sdk';
await Encatch.startSession();
// Skip the immediate ping or screen re-track on start:
await Encatch.startSession({
skipImmediatePing: true,
skipImmediateTrackScreen: true,
});// Temporarily stop the 30-second background ping (not persisted)
Encatch.pauseSession();
// Resume the ping interval after pauseSession()
Encatch.resumeSession();// Fully suspend SDK activity — stops ping and dismisses open forms.
// Persists across app restarts. Re-enable with startSession().
await Encatch.stopSession();Reset the current user identity and clear persisted identity data. Reverts the SDK to anonymous mode. User identity is preserved across stopSession() — use resetUser() after logout.
const { resetUser } = useEncatch();
resetUser();Wipes all persisted SDK data and resets in-memory state. Stronger than resetUser() — also clears session-stopped state and device preferences. Call Encatch.init() again before further use.
await Encatch.clearAll();The SDK sends a background ping every 30 seconds (configurable via server response) to maintain engagement sessions and check for triggered forms. Ping is suppressed while a form is visible.
Inline Forms
Inline forms are a way to show Encatch in-app feedback without a modal — the survey renders directly in your layout instead of as a full-screen overlay.
EncatchInlineForm renders a form directly inside your component tree instead of as a full-screen modal overlay. Place it anywhere — in a ScrollView, View, card, etc.
import { EncatchInlineForm, useEncatch } from '@encatch/react-native-sdk';
function FeedbackScreen() {
const { showForm } = useEncatch();
return (
<ScrollView contentContainerStyle={{ padding: 16 }}>
{/* ... content above ... */}
<EncatchInlineForm
formId="your-form-slug" // exact match; omit for wildcard
minHeight={100}
style={{ borderRadius: 12, overflow: 'hidden' }}
/>
{/* ... content below ... */}
</ScrollView>
);
}
// Then trigger the form from anywhere:
showForm('your-form-slug');When showForm is called, the SDK resolves the presenter in this order:
- Exact match — first registered
EncatchInlineFormwhoseformIdmatches the payload wins. - Wildcard — first registered
EncatchInlineFormwith noformIdcatches anything not exact-matched. - Modal fallback —
EncatchWebViewshows the form as the default overlay when no inline slot is registered or none match.
When @react-navigation/native is installed, EncatchInlineForm registers its inline slot only while the screen is focused (via useIsFocused). Background tab screens do not intercept showForm calls meant for the modal.
If you use a tab navigator that keeps screens mounted in the background, ensure inline slots are on focused screens only. The encatch-expo-tester sample app demonstrates exact and wildcard inline tabs with scroll-into-view when QnA/Scheduler overlays open.
When a screen loses focus, its slot is unregistered, so showForm falls through to the modal or another active slot.
The WebView's internal scroll is disabled. The host ScrollView (or FlatList) provides scrolling. The widget height grows automatically via form:resize messages from the web form.
<ScrollView>
<EncatchInlineForm formId="my-form" />
</ScrollView>The host app controls keyboard avoidance. Use KeyboardAvoidingView, automaticallyAdjustKeyboardInsets, or scroll-into-view when the keyboard opens — WebView focus is not native, so the SDK does not shrink on keyboard.
| Prop | Type | Default | Description |
|---|---|---|---|
formId | string | — | Exact form slug/id to match. Omit for wildcard. |
style | StyleProp<ViewStyle> | — | Outer container layout style. |
minHeight | number | 0 | Minimum height floor applied after form:resize. |
onOverlayOpenChange | (open: boolean) => void | — | Called when a QnA/Scheduler overlay opens or closes. |
Build Your Own Form UX & UI
If your feedback flow uses a fixed, predictable question set — the same fields and workflow every time — you can build the form with your own React Native components and submit responses through the SDK. That keeps typography, spacing, colors, and interaction patterns aligned with the rest of your app, so the survey feels like a native screen rather than an embedded web page.
Example coming soon.
| Method | Description |
|---|---|
init(apiKey, config?) | Initialize the SDK |
identifyUser(userName, traits?, options?) | Identify a user |
setLocale(locale) | Set locale |
setCountry(country) | Set country (ISO 3166) |
setTheme(theme) | Set form theme |
trackEvent(eventName) | Track a custom event |
trackScreen(screenName) | Track screen navigation |
showForm(formId, options?) | Show a form (inline or modal) |
dismissForm(formConfigurationId?) | Dismiss the current form |
addToResponse(questionId, value) | Pre-fill a question answer |
startSession(options?) | Start a new session |
pauseSession() | Pause background ping |
resumeSession() | Resume background ping |
stopSession() | Suspend SDK activity |
resetUser() | Reset user identity |
clearAll() | Wipe all persisted SDK data |
on(callback) | Subscribe to lifecycle events |
off(callback) | Unsubscribe from events |
submitForm(params) | Submit a custom native form |
refineText(params) | AI text refinement |
uploadFile(params) | Upload a file (custom native forms) |
streamQnaWithAi(params, callbacks) | Stream Q&A with AI answers |
stop() | Teardown (called by provider on unmount) |
Use useEncatch() for the same API inside React components. The following are available on the Encatch singleton only (not exposed via useEncatch()): init, startSession, pauseSession, resumeSession, stopSession, clearAll, uploadFile, streamQnaWithAi, stop.
Add usage descriptions to your app's Info.plist (or app.json for Expo) when forms include video/audio capture:
<key>NSCameraUsageDescription</key>
<string>Encatch forms use the camera to record video responses.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Encatch forms use the microphone to record audio and video responses.</string>Expo app.json example:
{
"expo": {
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Allow Encatch to capture photos, videos, and signatures for form responses.",
"NSMicrophoneUsageDescription": "Allow Encatch to record audio for form responses."
}
}
}
}The SDK uses react-native-webview, which handles WebView media permission requests once these keys are present.
Add permissions to android/app/src/main/AndroidManifest.xml (or app.json for Expo):
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />Expo app.json example:
{
"expo": {
"android": {
"permissions": ["CAMERA", "RECORD_AUDIO", "MODIFY_AUDIO_SETTINGS"]
}
}
}INTERNET is required for API calls and the form WebView. The other three permissions are required when forms include video/audio capture questions (video_audio).
The SDK grants WebView media permission requests automatically, but Android still requires runtime approval before the camera or microphone can be used. Request them at startup (Android only):
import { Camera } from 'expo-camera';
async function requestEncatchMediaPermissions() {
const camera = await Camera.requestCameraPermissionsAsync();
const microphone = await Camera.requestMicrophonePermissionsAsync();
if (camera.status !== 'granted' || microphone.status !== 'granted') {
// Handle denied permissions — recording questions will not work
}
}Use expo-camera or your preferred permissions library — it is not a dependency of @encatch/react-native-sdk.
Support
Was this page helpful?
