Userbubble
SdksReact native

Quick Start

Get up and running with the Userbubble React Native SDK in 5 minutes

Step 1: Install the SDK

Follow the installation guide to install the SDK and get your API key.

Step 2: Wrap Your App

Wrap your root component with UserbubbleProvider:

import { UserbubbleProvider } from "@userbubble/react-native";

export default function App() {
  return (
    <UserbubbleProvider
      config={{
        apiKey: process.env.EXPO_PUBLIC_USERBUBBLE_API_KEY!,
        debug: __DEV__, // Enable debug logging in development
      }}
    >
      <YourAppContent />
    </UserbubbleProvider>
  );
}

Step 3: Identify Users

Use the useUserbubble() hook to identify users after they log in:

import { useUserbubble } from "@userbubble/react-native";
import { Button } from "react-native";

function LoginScreen() {
  const { identify } = useUserbubble();

  const handleLogin = async () => {
    // Your login logic here
    const user = await yourLoginFunction();

    // Identify the user with Userbubble
    await identify({
      id: user.id,           // Your user ID
      email: user.email,     // User email
      name: user.name,       // Optional: user name
      avatar: user.avatar,   // Optional: avatar URL
    });

    // Navigate to home screen
    navigation.navigate("Home");
  };

  return <Button title="Login" onPress={handleLogin} />;
}

Step 4: Show the Feedback Portal

The recommended approach is opening the portal in a modal / form sheet using getEmbedUrl() with a WebView. The embed view includes its own tab navigation (Feedback, Roadmap, Updates) and user info — you just render a WebView.

With Expo Router:

// app/_layout.tsx — register the modal route
<Stack.Screen
  name="feedback"
  options={{
    presentation: "formSheet",
    headerShown: false,
    sheetGrabberVisible: true,
  }}
/>
// app/feedback.tsx — the modal screen
import { useUserbubble } from "@userbubble/react-native";
import { ActivityIndicator, View } from "react-native";
import { WebView } from "react-native-webview";

function LoadingIndicator() {
  return (
    <View style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0, justifyContent: "center", alignItems: "center" }}>
      <ActivityIndicator size="large" />
    </View>
  );
}

export default function FeedbackModal() {
  const { getEmbedUrl } = useUserbubble();
  const url = getEmbedUrl("/feedback");

  if (!url) return null;

  return (
    <View style={{ flex: 1 }}>
      <WebView
        source={{ uri: url }}
        style={{ flex: 1 }}
        javaScriptEnabled
        domStorageEnabled
        startInLoadingState
        renderLoading={() => <LoadingIndicator />}
      />
    </View>
  );
}
// Open from anywhere in your app
import { router } from "expo-router";

<Button title="Give Feedback" onPress={() => router.push("/feedback")} />

getEmbedUrl() returns a fully authenticated URL. The embed view handles navigation between feedback, roadmap, and changelog — no need to build native tabs.

Alternative: Open in device browser

If you prefer opening Userbubble in an external browser instead:

const { openUserbubble } = useUserbubble();

// Opens in the device's default browser with full portal header
await openUserbubble("/feedback");

Complete Example

Here's a complete working example with a form sheet modal:

import React, { useState } from "react";
import { ActivityIndicator, Alert, Modal, Pressable, SafeAreaView, Text, TextInput, View } from "react-native";
import {
  UserbubbleProvider,
  useUserbubble,
} from "@userbubble/react-native";
import { WebView } from "react-native-webview";

function LoadingIndicator() {
  return (
    <View style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0, justifyContent: "center", alignItems: "center" }}>
      <ActivityIndicator size="large" />
    </View>
  );
}

export default function App() {
  return (
    <UserbubbleProvider
      config={{
        apiKey: process.env.EXPO_PUBLIC_USERBUBBLE_API_KEY!,
        debug: __DEV__,
      }}
    >
      <MyApp />
    </UserbubbleProvider>
  );
}

function MyApp() {
  const { identify, getEmbedUrl, isIdentified, user, logout } = useUserbubble();
  const [showFeedback, setShowFeedback] = useState(false);
  const url = getEmbedUrl("/feedback");

  if (!isIdentified) {
    return <IdentifyForm identify={identify} />;
  }

  return (
    <View style={{ flex: 1, justifyContent: "center", padding: 24, gap: 12 }}>
      <Text style={{ fontSize: 20, fontWeight: "bold", textAlign: "center" }}>
        Welcome, {user?.name || user?.email}
      </Text>
      <Pressable
        onPress={() => setShowFeedback(true)}
        style={{ backgroundColor: "#3b82f6", borderRadius: 8, padding: 14, alignItems: "center" }}
      >
        <Text style={{ color: "#fff", fontWeight: "600" }}>Give Feedback</Text>
      </Pressable>
      <Pressable onPress={logout} style={{ padding: 14, alignItems: "center" }}>
        <Text style={{ color: "#888" }}>Logout</Text>
      </Pressable>

      {url ? (
        <Modal
          visible={showFeedback}
          animationType="slide"
          presentationStyle="formSheet"
          onRequestClose={() => setShowFeedback(false)}
        >
          <SafeAreaView style={{ flex: 1 }}>
            <WebView
              source={{ uri: url }}
              style={{ flex: 1, minHeight: 0 }}
              javaScriptEnabled
              domStorageEnabled
              startInLoadingState
              renderLoading={() => <LoadingIndicator />}
            />
          </SafeAreaView>
        </Modal>
      ) : null}
    </View>
  );
}

function IdentifyForm({
  identify,
}: {
  identify: (user: { id: string; email: string; name?: string }) => Promise<void>;
}) {
  const [email, setEmail] = React.useState("");
  const [name, setName] = React.useState("");

  const handleIdentify = async () => {
    try {
      await identify({
        id: "user_123",
        email: email || "user@example.com",
        name: name || "Test User",
      });
    } catch (error) {
      Alert.alert("Error", `Failed to identify: ${error}`);
    }
  };

  return (
    <View style={{ flex: 1, justifyContent: "center", padding: 24 }}>
      <TextInput
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        style={{ borderWidth: 1, borderColor: "#d4d4d4", borderRadius: 8, padding: 12, marginBottom: 12 }}
      />
      <TextInput
        placeholder="Name"
        value={name}
        onChangeText={setName}
        style={{ borderWidth: 1, borderColor: "#d4d4d4", borderRadius: 8, padding: 12, marginBottom: 12 }}
      />
      <Pressable
        onPress={handleIdentify}
        style={{ backgroundColor: "#3b82f6", borderRadius: 8, padding: 14, alignItems: "center" }}
      >
        <Text style={{ color: "#fff", fontWeight: "600" }}>Continue</Text>
      </Pressable>
    </View>
  );
}

Configuration Options

The config object accepts these options:

OptionTypeDefaultDescription
apiKeystringRequiredYour organization's API key
baseUrlstring"https://app.userbubble.com"Base URL for the Userbubble service
debugbooleanfalseEnable debug logging
storageType"expo" | "async-storage" | "auto""auto"Storage type (auto-detect recommended)
customStorageStorageAdapterundefinedCustom storage implementation

Next Steps

Enable debug: __DEV__ during development to see helpful logs about user identification and SDK operations.

On this page