Skip to main content

Install the React Native mobile SDK

Beta: The mobile SDK is still in beta. To get access, chat with us at the bottom right of our web pages.

This guide adds the Marker.io SDK to a React Native app, so your testers can report bugs from inside the app. Setup takes two steps: install the packages, then initialize the SDK. For what the SDK does once it is installed, see Mobile SDK overview.

Before you start

Check that your app meets these requirements:

  • React Native 0.76 or higher. Both the New Architecture and the legacy architecture work.

  • React 18.0 or higher.

  • iOS 15.1 or higher.

  • Android API 24 or higher. API 26 or higher gives pixel-perfect capture. Automatic screenshot detection needs API 34 or higher.

You also need your destination ID. Find it in the widget install snippet on your Marker.io dashboard, where it is the value of project.


Step 1: Install the packages

The SDK ships as @marker.io/react-native-sdk. Install it from the beta tag, which always points at the newest beta build. It needs 11 peer dependencies, so install them in the same command.

If you use bare React Native, run this in your project root:

yarn add @marker.io/react-native-sdk@beta \
  @shopify/react-native-skia \
  react-native-gesture-handler \
  react-native-reanimated \
  react-native-device-info \
  react-native-keyboard-controller \
  react-native-haptic-feedback \
  react-native-safe-area-context \
  react-native-keychain \
  react-native-blob-util \
  react-native-image-picker \
  @react-native-documents/picker

Then install the iOS pods:

cd ios && pod install

If you use managed Expo, run npx expo install instead. It picks versions that match your Expo SDK:

npx expo install @marker.io/react-native-sdk@beta \
  @shopify/react-native-skia \
  react-native-gesture-handler \
  react-native-reanimated \
  react-native-device-info \
  react-native-keyboard-controller \
  react-native-haptic-feedback \
  react-native-safe-area-context \
  react-native-keychain \
  react-native-blob-util \
  react-native-image-picker \
  @react-native-documents/picker

Skia, Reanimated, and Gesture Handler each need a one-time setup of their own. If your app does not already use them, follow their install guides before you continue.


Step 2: Set up your app

Pick the path that matches your project. If you have no ios/ and android/ folders committed, you are on managed Expo. If you do have them, use the bare React Native path.

Managed Expo

Add the config plugin to app.json. It does the native wiring for you:

{
  "expo": {
    "plugins": ["@marker.io/react-native-sdk"]
  }
}

On your next npx expo prebuild, the plugin registers the SDK in AppDelegate.swift and adds the iOS photo library, camera, and microphone usage strings that attachments need. Any usage string you set yourself in app.json always wins. To change the defaults:

{
  "plugins": [
    ["@marker.io/react-native-sdk", {
      "photoLibraryPermission": "Attach a screenshot to your report.",
      "cameraPermission": "Take a photo to attach to your report.",
      "microphonePermission": "Record a video to attach to your report."
    }]
  ]
}

Then initialize the SDK from your root layout with <MarkerProvider>. It takes the same options as Marker.init() and runs exactly once:

// app/_layout.tsx
import { Slot } from 'expo-router';
import { MarkerProvider, InvocationEvent } from '@marker.io/react-native-sdk';export default function RootLayout() {
  return (
    <MarkerProvider
      destination="YOUR_DESTINATION_ID"
      invocationEvents={[
        InvocationEvent.floatingButton,
        InvocationEvent.shake,
        InvocationEvent.screenshot,
      ]}
    >
      <Slot />
    </MarkerProvider>
  );
}

Console and network logging start when this module loads. Calls made before your root layout renders are not recorded. To catch the very first calls in your app, initialize with Marker.init() from a custom entry file instead, as shown below.

Bare React Native

Call Marker.init() as early as you can in index.js:

// index.js
import { AppRegistry } from 'react-native';
import Marker, { InvocationEvent } from '@marker.io/react-native-sdk';
import App from './App';
import { name as appName } from './app.json';Marker.init({
  destination: 'YOUR_DESTINATION_ID',
  invocationEvents: [
    InvocationEvent.floatingButton,
    InvocationEvent.shake,
    InvocationEvent.screenshot,
  ],
});AppRegistry.registerComponent(appName, () => App);

On iOS, register the React Native host once in AppDelegate.swift. The feedback overlay renders in its own UIWindow so it sits above any React Native <Modal>, and iOS gives no public way to reach your app's React host. Add this right after you start React Native:

import MarkerIo// inside application(_:didFinishLaunchingWithOptions:), after factory.startReactNative(...):
MarkerSdkConfig.register(withReactNativeFactory: factory)

Skip this step and the floating button still works, but opening the feedback overlay fails with E_FEEDBACK_SHOW.

On Android, there is nothing to add. The SDK declares the SYSTEM_ALERT_WINDOW permission for you through manifest merging. It lets the floating button draw above your <Modal> windows. Your user grants Display over other apps at runtime. Until they do, the button falls back to in-app rendering and hides behind a <Modal>. The feedback overlay itself never needs this permission.

Good to know

Marker.init() is synchronous and idempotent, so Fast Refresh will not break it. The first call wins, and a later call with a different destination warns you in development. Any Marker.capture() you make before warmup finishes is buffered and runs once the SDK is ready.

There is no widget component to mount. Marker.init() owns the floating button.


Step 3: Test it

  1. Run your app on a real device or a simulator.

  2. Tap the floating button, or shake the device.

  3. Annotate the screenshot and submit the report.

  4. Open your Marker.io project. The issue is there, with device, console, and network context attached.


Init options

Pass these to Marker.init() or to <MarkerProvider>. Only destination is required.

  • destination: your Marker.io destination ID.

  • bundleId: your app's bundle identifier. Detected automatically.

  • invocationEvents: which triggers open the capture flow. Defaults to the floating button. Pass [] to disable every automatic trigger and open capture only with Marker.capture().

  • identify: pre-fill the reporter with email and an optional fullName.

  • customMetadata: initial key and value pairs attached to every report.

  • onBeforeSend: change or drop a report before it is sent. Return null to drop it.

  • privacy: strict, balanced (the default), or permissive.

  • privacyOverrides: change individual device signals inside a preset.

  • networkRecording: configure network capture and redaction.

  • redactionPatterns: extra patterns scrubbed from console and network logs.


Common commands

Everything is on the default Marker export.

Capture and submit

  • Marker.capture(): take a screenshot and open the annotation flow. Resolves to the screenshot file URI.

  • Marker.submit({ title, description, screenshotPath }): submit a report with no UI. Resolves to the created issue ID.

Floating button

  • Marker.show() and Marker.hide(): show or hide the floating button.

  • Marker.isVisible(): current visibility.

  • Marker.reattachFab(): re-attach the button on Android after the Display over other apps grant. Does nothing on iOS.

To ask for that Android grant from your own UI:

import { FabOverlay } from '@marker.io/react-native-sdk';if (!(await FabOverlay.canDrawFabOverlayAboveModals())) {
  await FabOverlay.requestFabOverlayPermission(); // opens system Settings
  await FabOverlay.reattachFabOverlay();          // re-attach after the user returns
}

Reporter and metadata

  • Marker.identify({ email, fullName }): set the reporter. email is required.

  • Marker.logout(): clear the reporter, session, and anonymous ID. Later reports are sent anonymously.

  • Marker.customMetadata.set(key, value) and .unset(key): change one entry.

  • Marker.customMetadata.replace(record), .clear(), and .getAll(): manage the whole bag.

For example, tag every report with the build under test:

Marker.identify({ email: 'tester@example.com', fullName: 'Jane Tester' });
Marker.customMetadata.set('appVersion', '1.4.2');
Marker.customMetadata.set('environment', 'staging');

Current screen name

Report the screen your tester was on. It shows as View name on the issue. Set it yourself with Marker.setViewName('Checkout'), or wire it to your navigation library once.

With React Navigation, attach the tracker to your NavigationContainer:

import { createNavigationViewTracker } from '@marker.io/react-native-sdk';
import { useNavigationContainerRef } from '@react-navigation/native';const navigationRef = useNavigationContainerRef();
const tracker = createNavigationViewTracker(navigationRef);<NavigationContainer
  ref={navigationRef}
  onReady={tracker.onReady}
  onStateChange={tracker.onStateChange}
>
  {/* ...navigator... */}
</NavigationContainer>;

With expo-router, call the hook once in your root layout:

import { useMarkerViewName } from '@marker.io/react-native-sdk';
import { usePathname } from 'expo-router';useMarkerViewName(usePathname());

Masking sensitive screens

Wrap any component that shows private data. It is blacked out natively, before the screenshot is written to disk, so the content never reaches the file.

import { MarkerPrivateView } from '@marker.io/react-native-sdk';<MarkerPrivateView>
  <CreditCardInput />
</MarkerPrivateView>

Privacy and network

  • Marker.setPrivacy(preset, overrides): switch privacy preset at runtime.

  • Marker.setNetworkRecordingSettings({ excludedKeys, excludedDomains }): update the live network recorder.

  • Marker.refreshConfig(): re-fetch branding and redaction rules from Marker.io.

Events

Subscribe with Marker.on(event, listener), which returns an unsubscribe function. Remove a listener with Marker.off(event, listener).

  • load: the SDK finished warming up.

  • feedbackopen and feedbackclose: the overlay opened or was dismissed.

  • feedbacksent: a report was submitted. Payload has issueId, url, and projectId.

  • feedbackerror: submission failed.

  • feedbackdiscarded: the user cancelled, or onBeforeSend returned null.

  • capture and shake: an OS screenshot or a device shake was detected.

  • capturewarning: the capture can be blank because of hardware-accelerated content.

For example, log every successful submission:

const unsubscribe = Marker.on('feedbacksent', (e) => {
  console.log('Submitted', e.issueId, e.url);
});

You can also use Marker.onReady(cb) to run code once warmup finishes, Marker.onFeedbackError(cb) for detailed error events, and Marker.onAttachmentProgress(cb) for upload progress.


Known limitations

  • Automatic screenshot detection needs Android API 34. All other triggers work on every supported version. On iOS it works everywhere.

  • Hardware-accelerated views can capture blank. WebViews, video, maps, and camera views are composited by the GPU and will not always render into the screenshot. A capturewarning event fires when this is detected.

  • Test private views on a real device. Simulators and emulators render secure content differently. Confirm that <MarkerPrivateView> regions are blacked out in a real capture before you ship.


Need Help?

If you have any questions, comments, or corrections, chat with us at the bottom right of our web pages.

Did this answer your question?