npm package: @mutalabs/react-native-muta
Description: A React Native SDK for creating dynamic, remotely configurable onboarding experiences
Author: Muta Labs
Latest version: 1.2.1

# <div align="center"><img src="https://unpkg.com/@mutalabs/react-native-muta/assets/mutablack.svg" alt="Muta Logo" width="200" /></div>

A React Native SDK for creating dynamic, remotely configurable onboarding experiences in your mobile app. Muta allows you to create, update, and A/B test your onboarding flows without deploying app updates or writing a single line of code.

## Features

- 🎨 No-code editor - design and update flows with a drag-and-drop interface
- 🚀 Remote updates - modify onboarding flows instantly without app releases
- ✨ Rich components - buttons, text, images, shapes, icons, and more
- 🔄 Smooth transitions - fade and slide animations
- 📊 Analytics integration - track user behavior with any analytics provider
- 📝 User input collection - gather text and multiple choice responses
- 💪 Full TypeScript support
- 🪶 Extremely lightweight

## Installation

```bash
# Install the package and its required dependencies
npm install @mutalabs/react-native-muta react-native-webview
# or
yarn add @mutalabs/react-native-muta react-native-webview

# For React Native CLI projects only (not needed for Expo)
cd ios && pod install
```

> Note: If you're using Expo:
>
> - The pod install step is not required
> - You cannot use Expo Go - you must create a fresh development build
> - Follow the official Expo guide to create a development build: [Creating Development Builds](https://docs.expo.dev/develop/development-builds/create-a-build/)

## Quick Start

1. Add the MutaRoot component to your app:

```typescript
import { MutaRoot, Muta } from '@mutalabs/react-native-muta';

function App() {
  return (
    <>
      <MutaRoot apiKey="your-api-key" />
      {/* Your app content */}
    </>
  );
}
```

2. Display a placement:

```typescript
// Basic usage
Muta.displayPlacement({
  placementId: 'your-placement-id',
  bgColor: '#000000' // Should match your first placement screen's background color
})

// With custom loading screen and presentation type
Muta.displayPlacement({
  placementId: 'your-placement-id',
  bgColor: '#000000', // Should match your first placement screen's background color
  loadingScreen: <YourCustomLoadingScreen />,
  presentationType: 'fade' // 'slide' | 'fade' | 'none'
})
```

## API Reference

### MutaRoot

The root component that initializes the SDK and manages placements.

```typescript
<MutaRoot apiKey={string} />
```

### Muta.displayPlacement()

Displays a placement in a modal overlay.

```typescript
interface DisplayPlacementOptions {
  placementId: string
  loadingScreen?: React.ReactNode
  bgColor: string // Should match your first placement screen's background color
  presentationType?: 'slide' | 'fade' | 'none'
  injectedScreens?: Array<{
    screenName: string
    component: React.ComponentType<{
      onContinue: () => void
      onBack: () => void
      variables: any[]
      screenId: string
      screenIndex: number
    }>
  }>
}

Muta.displayPlacement(options: DisplayPlacementOptions)
```

#### Options

- `placementId` (required): The ID of the placement to display
- `bgColor` (required): Background color that matches your first placement screen. This creates a seamless transition between your app and the placement.
- `loadingScreen` (optional): A custom React component to show while the placement is loading
- `presentationType` (optional): Animation style when showing the placement. Defaults to 'slide'.
  - `'slide'`: Slides up from the bottom
  - `'fade'`: Fades in from transparent
  - `'none'`: No animation
- `injectedScreens` (optional): Array of native React components to inject into code screens (see Code Screens section below)

## Example App

```typescript
import { Muta, MutaRoot } from '@mutalabs/react-native-muta';

function App() {
  useEffect(() => {
    // Show placement when needed
    Muta.displayPlacement({
      placementId: 'your-placement-id',
      bgColor: '#1a1a1a', // Should match your first placement screen's background color
      presentationType: 'fade' // Optional: customize the animation
    });
  }, []);

  return (
    <>
      <MutaRoot apiKey="your-api-key" />
      {/* Your app content */}
    </>
  );
}
```

## Custom Events

Muta allows you to emit custom events from your flows using the platform's behavior system. You can configure custom events in the Muta web editor and listen for them in your app to trigger specific actions.

### Setting Up Custom Events

1. In the Muta web editor, add an "Emit Event" behavior to any element
2. Give your event a unique name (e.g., `signup_started`, `premium_selected`, `survey_completed`)
3. Optionally add custom data to pass along with the event

### Listening for Custom Events

```typescript
import { Muta } from '@mutalabs/react-native-muta';

function Onboarding() {
  useEffect(() => {
    // Listen for your custom events
    const signupListener = Muta.on('signup_started', (event) => {
      console.log('User started signup process');
      // Trigger your signup flow
      navigateToSignup();
    });

    const premiumListener = Muta.on('premium_selected', (event) => {
      console.log('User selected premium option', event.eventData);
      // Handle premium selection
      setPremiumUser(true);
    });

    const surveyListener = Muta.on('survey_completed', (event) => {
      console.log('Survey answers:', event.eventData);
      // Save survey responses
      saveSurveyData(event.eventData);
    });

    // Show the placement
    Muta.displayPlacement({
      placementId: 'your-placement-id',
      bgColor: '#000000'
    });

    return () => {
      signupListener.remove();
      premiumListener.remove();
      surveyListener.remove();
    };
  }, []);

  return <View style={{ flex: 1 }} />;
}
```

### Custom Event Structure

```typescript
{
  type: string,         // Your custom event name
  timestamp: number,    // Unix timestamp in ms
  placementId: string,  // ID of the placement
  flowName?: string,    // Name of the flow
  screenIndex?: number, // Current screen index
  eventData?: any       // Custom data you configured in the editor
}
```

### Common Use Cases

1. **User Actions**: Track when users click specific CTAs or make choices
2. **Navigation Control**: Trigger app navigation based on flow interactions
3. **Data Collection**: Gather form submissions or survey responses
4. **Feature Flags**: Enable/disable features based on onboarding choices
5. **Conversion Tracking**: Track specific conversion events for analytics

## Code Screens

Code Screens allow you to inject native React components into your Muta flows. This enables you to combine remotely configured flows with custom native functionality like authentication forms, payment screens, or any complex native UI.

### How It Works

1. In the Muta web editor, add a "Code Screen" to your flow and give it a unique name (e.g., "Login Screen", "Payment Form")
2. Configure the navigation destinations for continue and back actions
3. In your app, pass the corresponding React component when displaying the placement

### Example Usage

```typescript
import { Muta } from '@mutalabs/react-native-muta';
import { LoginScreen } from './screens/LoginScreen';
import { PaymentScreen } from './screens/PaymentScreen';

// Display placement with injected native screens
Muta.displayPlacement({
  placementId: 'onboarding',
  bgColor: '#000000',
  presentationType: 'none',
  injectedScreens: [
    {
      screenName: 'Login Screen',  // Must match the name in Muta editor
      component: LoginScreen,
    },
    {
      screenName: 'Payment Form',   // Must match the name in Muta editor
      component: PaymentScreen,
    },
  ],
});
```

### Creating a Code Screen Component

Your component will receive props for navigation and flow data:

```typescript
interface CodeScreenProps {
  onContinue: () => void      // Navigate to configured continue destination
  onBack: () => void          // Navigate to configured back destination
  variables: any[]            // Current flow variables
  screenId: string            // Unique screen ID
  screenIndex: number         // Screen position in flow
}

function LoginScreen({ onContinue, onBack, variables }: CodeScreenProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleLogin = async () => {
    try {
      await loginUser(email, password);
      onContinue(); // Navigate to next screen in flow
    } catch (error) {
      // Handle error
    }
  };

  return (
    <View style={styles.container}>
      <TextInput
        value={email}
        onChangeText={setEmail}
        placeholder="Email"
      />
      <TextInput
        value={password}
        onChangeText={setPassword}
        placeholder="Password"
        secureTextEntry
      />
      <Button title="Login" onPress={handleLogin} />
      <Button title="Back" onPress={onBack} />
    </View>
  );
}
```

### Benefits

- **Hybrid Flows**: Combine no-code screens with custom native screens
- **Seamless Transitions**: Native screens animate in/out matching your flow animations
- **Full Control**: Access to all native capabilities while maintaining remote configurability
- **Dynamic Routing**: Configure navigation destinations remotely without code changes

## Analytics Integration

Muta provides a flexible event system that works with any analytics provider (Mixpanel, AppsFlyer, Amplitude, Firebase, etc.). Events are emitted during key user interactions, allowing you to track and analyze your placement performance.

### Tracking All Events

Use the wildcard '\*' to listen to all events:

```tsx
import { Muta } from '@mutalabs/react-native-muta'

// Example with Mixpanel
function MixpanelTracking() {
  useEffect(() => {
    const subscription = Muta.on('*', (event) => {
      mixpanel.track(event.type, {
        placement_id: event.placementId,
        flow_name: event.flowName,
        timestamp: event.timestamp,
        ...event, // includes all event-specific properties
      })
    })

    return () => subscription.remove()
  }, [])
}

// Example with AppsFlyer
function AppsFlyerTracking() {
  useEffect(() => {
    const subscription = Muta.on('*', (event) => {
      appsflyer.logEvent(event.type, {
        placement_id: event.placementId,
        flow_name: event.flowName,
        ...event,
      })
    })

    return () => subscription.remove()
  }, [])
}
```

## Error Handling

Muta will automatically handle errors and prevent the flow from displaying if there are any issues. You can listen for these errors to show appropriate messages to your users.

### Example Usage

```typescript
import { Muta } from '@mutalabs/react-native-muta';

function Onboarding() {
  useEffect(() => {
    // Listen for error events
    const subscription = Muta.on('error', (error) => {
      if (error.type === 'network_error') {
        // Handle network errors (e.g., show "No internet connection" message)
        console.log('Network error:', error.message);
      } else if (error.type === 'placement_error') {
        // Handle placement errors (e.g., show "Invalid placement ID" message)
        console.log('Placement error:', error.message);
      }
    });

    // Show the placement
    Muta.displayPlacement({
      placementId: 'your-placement-id',
      bgColor: '#000000'
    });

    return () => subscription.remove();
  }, []);

  return <View style={{ flex: 1 }} />;
}
```

### Error Types

1. Network Errors

   - Emitted when there's no internet connection
   - The flow will not display
   - You should show an appropriate "No internet connection" message

2. Placement Errors
   - Emitted when there's an issue with the placement ID or API key, typically this is when a key or placement does not match what is in the web app or has been deleted
   - The flow will not display
   - You should show an appropriate error message to the user

### Error Event Structure

```typescript
// Network Error
{
  type: 'network_error',
  message: string,      // Error message
  timestamp: number     // Unix timestamp in ms
}

// Placement Error
{
  type: 'placement_error',
  message: string,      // Error message
  code: string,         // Error code
  timestamp: number,    // Unix timestamp in ms
  placementId: string   // ID of the placement that failed
}
```

### Available Events

1. Flow Started (`flow_started`)
   Emitted when a placement flow begins displaying.

```typescript
{
  type: 'flow_started',
  timestamp: number,      // Unix timestamp in ms
  placementId: string,    // ID of the placement
  flowName?: string,      // Name of the flow (if configured)
  totalScreens: number    // Total number of screens in flow
}
```

2. Screen Viewed (`screen_viewed`)
   Emitted when a user views a new screen in the flow.

```typescript
{
  type: 'screen_viewed',
  timestamp: number,
  placementId: string,
  flowName?: string,
  screenIndex: number,    // Zero-based index of current screen
  totalScreens: number,   // Total number of screens in flow
  screenName?: string     // Name of the screen (if configured)
}
```

3. Flow Completed (`flow_completed`)
   Emitted when a user successfully finishes the flow.

```typescript
{
  type: 'flow_completed',
  timestamp: number,      // Unix timestamp in ms
  placementId: string,    // ID of the placement
  flowName: string,       // Name of the flow
  screenIndex: number,    // Index of final screen
  totalScreens: number,   // Total screens in flow
  screenName: string,     // Name of final screen
  eventData?: {           // Contains all collected variables
    variables: Record<string, any>  // Variable IDs mapped to their values
  }
}
```

#### Variable Structure

When a flow completes, `eventData.variables` contains an object where:
- **Key**: Variable ID (e.g., `var_1758310510757_as20wkeo4`)
- **Value**: Object containing:
  - `id` (string): Unique variable identifier
  - `name` (string): Human-readable variable name from the Muta editor
  - `type` (string): Variable type (e.g., "text")
  - `value` (any): The collected value
  - `defaultValue` (any): The default value

**Example:**
```typescript
Muta.on('flow_completed', (event) => {
  if (event.eventData?.variables) {
    Object.entries(event.eventData.variables).forEach(([varId, varData]) => {
      console.log(`Variable '${varData.name}': ${varData.value}`)
    })
  }
})
```

**Output:**
```
Variable 'MultipleChoice1': Family
Variable 'MultipleChoice2': Fluent
```

4. Flow Abandoned (`flow_abandoned`)
   Emitted when a user exits the flow before completion.

```typescript
{
  type: 'flow_abandoned',
  timestamp: number,
  placementId: string,
  flowName?: string,
  screenIndex: number,    // Index of last viewed screen
  totalScreens: number,   // Total screens in flow
  lastScreenIndex: number, // Index of last viewed screen
  screenName?: string     // Name of last viewed screen
}
```

5. Custom Events (configured in web editor)
   Emitted when user triggers actions configured with "Emit Event" behavior.

```typescript
{
  type: string,         // Your custom event name
  timestamp: number,    // Unix timestamp in ms
  placementId: string,  // ID of the placement
  flowName?: string,    // Name of the flow
  screenIndex?: number, // Current screen index
  eventData?: any       // Custom data configured in editor
}
```

6. Error Events (`error`)
   Emitted when there are network or placement errors.

```typescript
// Network Error
{
  type: 'network_error',
  message: string,      // Error message
  timestamp: number     // Unix timestamp in ms
}

// Placement Error
{
  type: 'placement_error',
  message: string,      // Error message
  code: string,         // Error code
  timestamp: number,    // Unix timestamp in ms
  placementId: string   // ID of the placement that failed
}
```

### Practical Example with Custom Events

Here's an example of using custom events to handle user interactions:

```typescript
import { Muta } from '@mutalabs/react-native-muta'

function OnboardingWithCustomEvents() {
  useEffect(() => {
    // Listen for flow completion with all collected variables
    const completionListener = Muta.on('flow_completed', (event) => {
      // Access all variables collected during the flow
      if (event.eventData?.variables) {
        const { user_name, user_email, selected_plan, preferences } = event.eventData.variables

        console.log('Flow completed with data:', {
          name: user_name,
          email: user_email,
          plan: selected_plan,
          preferences: preferences
        })

        // Save to your backend
        saveOnboardingData(event.eventData.variables)
      }
    })

    // Listen for form submission event
    const formListener = Muta.on('form_submitted', (event) => {
      // Access the form data from eventData
      const { name, email, preferences } = event.eventData

      console.log('User submitted:', { name, email, preferences })

      // Save to your backend
      saveUserProfile({ name, email, preferences })
    })

    // Listen for plan selection
    const planListener = Muta.on('plan_selected', (event) => {
      const { planType, price } = event.eventData

      // Handle plan selection
      if (planType === 'premium') {
        navigateToPremiumSignup()
      }
    })

    return () => {
      completionListener.remove()
      formListener.remove()
      planListener.remove()
    }
  }, [])

  return null
}
```