# Introduction
Source: https://docs.mutalabs.com/introduction
Create dynamic, remotely configurable onboarding experiences in your mobile app
Need help with integration? Click the **Copy AI Context** button in the bottom right corner to copy SDK documentation that you can paste into Claude, Cursor, or any AI assistant.
# Welcome to Muta
Muta is a platform that lets you design, optimize, and deploy mobile app flows without writing code. Think of it as Canva for mobile app experiences - but with the power to instantly update your app's content without App Store reviews.
## Core Features
Create beautiful mobile flows in minutes using our intuitive drag & drop interface. Add text, images, shapes, buttons, and more.
Edit a flow and hit save - changes go live instantly to all users. Want to try something new? Swap entire flows in and out with a click.
Create placements once, reference them in your code, and dynamically swap flows without code changes. Perfect for A/B testing and continuous optimization.
Flows automatically adapt to different screen sizes. Perfect display on all devices without separate tablet and phone designs.
## How It Works
Start with a template or create from scratch using our intuitive drag & drop interface. Add text, images, buttons, and other elements to create the perfect flow for your users.
Think of placements as containers for your flows. Each placement gets a unique ID that you'll reference in your code. The magic happens when you want to update your app - just swap flows in and out of placements without touching any code.
Add our lightweight SDK to your app and reference placement IDs where you want flows to appear. Our SDK handles everything else automatically - loading flows, displaying them beautifully, and updating them instantly when you make changes.
Changes are instant - edit a flow and hit save to update all users immediately. Want to try something completely new? Swap in a different flow without any code changes. A/B test, analyze, and optimize without app store reviews.
## Why Choose Muta?
Cut development time and costs, take control of your app's content, and optimize user experience without technical barriers.
Run experiments without engineering support, A/B test different flows easily, and make data-driven decisions.
Reduce deployment overhead, eliminate App Store review cycles, and focus on core app features.
Get dedicated support for SDK installation, custom template creation, and one-on-one consultation.
## Start Building Today
Muta transforms how you manage your app's content. No more waiting for App Store reviews or writing code for every experiment. Create, optimize, and deploy beautiful mobile flows in minutes.
Ready to take your app to the next level? [Get started](/quickstart) with Muta today.
# Quickstart
Source: https://docs.mutalabs.com/quickstart
Get your first flow live in under 5 minutes
# Get Started with Muta
Follow these steps to add beautiful, dynamic flows to your mobile app. Currently, we support React Native and Swift, with more SDKs coming soon.
Setting up for the first time? Use the **Copy AI Context** button (bottom right) to get comprehensive SDK documentation you can paste into Claude, Cursor, or other AI coding assistants for guided help.
## Installation
Choose your package manager:
```bash
# Using npm
npm install @mutalabs/react-native-muta react-native-webview
```
```bash
# Using yarn
yarn add @mutalabs/react-native-muta react-native-webview
```
For React Native CLI projects, install iOS dependencies:
```bash
cd ios && pod install
```
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/)
## Create Your First Flow
First, log in or sign up at [Muta Dashboard](https://app.mutalabs.com/dashboard). Then choose your path:
1. Start from scratch:
* Click the "+" button next to Flows
* Design your flow using drag & drop
* Hit "Save" when you're done
2. Start with a template:
* Navigate to Templates in the left panel
* Select a template you like
* Click "Create Flow" - the template becomes your base flow
* Edit the flow to match your needs
* Hit "Save" when you're done
1. Click "Placements" in the left panel
2. Click the "+" button to create a placement
3. Give it a memorable name
4. Choose an ID (e.g., "welcome.flow")
5. Select the flow you want to display
6. Click "Create"
1. Add the MutaRoot component to your app:
```tsx
import { MutaRoot, Muta } from '@mutalabs/react-native-muta';
function App() {
return (
<>
{/* Your app content */}
>
);
}
```
2. Display your flow where needed:
```tsx
Muta.displayPlacement({
placementId: 'your-placement-id',
bgColor: '#000000', // Match your first screen's background
presentationType: 'fade' // 'slide' | 'fade' | 'none'
});
```
The `bgColor` should match your first screen's background color for a seamless transition.
1. Run your app in development mode
2. Trigger your flow where implemented
3. Verify the flow appears and works as expected
1. Return to the Muta Dashboard
2. Edit your flow
3. Hit save - changes are ready instantly!
4. Refresh your app to see the changes
Updates affect new app downloads - if you switch from Flow A to Flow B, users who download your app after the switch will see Flow B. Users currently in Flow A will complete their existing flow uninterrupted.
## What's Next?
Learn how to gather text inputs and multiple choice responses from your users.
Integrate with your analytics provider to measure flow performance.
Implement proper error handling for a robust user experience.
Explore the complete SDK API documentation.
## Installation
Add the Muta SDK to your project using Swift Package Manager:
```swift
// In your Package.swift
dependencies: [
.package(url: "https://github.com/muta-labs/MutaSDK-iOS", from: "1.0.0")
]
```
Or in Xcode:
1. File > Add Package Dependencies
2. Enter package URL: `https://github.com/muta-labs/MutaSDK-iOS`
3. Click "Add Package"
## Create Your First Flow
First, log in or sign up at [Muta Dashboard](https://app.mutalabs.com/dashboard). Then choose your path:
1. Start from scratch:
* Click the "+" button next to Flows
* Design your flow using drag & drop
* Hit "Save" when you're done
2. Start with a template:
* Navigate to Templates in the left panel
* Select a template you like
* Click "Create Flow" - the template becomes your base flow
* Edit the flow to match your needs
* Hit "Save" when you're done
1. Click "Placements" in the left panel
2. Click the "+" button to create a placement
3. Give it a memorable name
4. Choose an ID (e.g., "welcome.flow")
5. Select the flow you want to display
6. Click "Create"
1. Initialize the SDK in your app:
```swift
import MutaSDK
@main
struct YourApp: App {
init() {
Muta.configure(apiKey: "your-api-key")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
2. Display your flow where needed:
```swift
Muta.shared.displayPlacement(
placementId: "your-placement-id",
backgroundColor: .white, // Match your first screen's background
presentationType: .fade // .slide | .fade | .none
)
```
The `backgroundColor` should match your first screen's background color for a seamless transition.
1. Run your app in development mode
2. Trigger your flow where implemented
3. Verify the flow appears and works as expected
1. Return to the Muta Dashboard
2. Edit your flow
3. Hit save - changes are ready instantly!
4. Refresh your app to see the changes
Updates affect new app downloads - if you switch from Flow A to Flow B, users who download your app after the switch will see Flow B. Users currently in Flow A will complete their existing flow uninterrupted.
## What's Next?
Learn how to gather text inputs and multiple choice responses from your users.
Integrate with your analytics provider to measure flow performance.
Implement proper error handling for a robust user experience.
Explore the complete SDK API documentation.
Need help? We offer white glove support for SDK installation and implementation. [Contact us](https://www.mutalabs.com/) anytime!
# Analytics Integration
Source: https://docs.mutalabs.com/sdk/react-native/analytics
Learn how to integrate analytics with your Muta placements
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 theme={null}
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()
}, [])
}
```
## Available Events
### 1. Flow Started (`flow_started`)
Emitted when a placement flow begins displaying.
```tsx theme={null}
{
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.
```tsx theme={null}
{
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.
```tsx theme={null}
{
type: 'flow_completed',
timestamp: number, // Unix timestamp in ms
placementId: string, // ID of the placement
flowName?: string, // Name of the flow (if configured)
screenIndex: number, // Index of final screen
totalScreens: number, // Total screens in flow
screenName?: string, // Name of final screen (if configured)
eventData?: { // Optional: Contains collected variables
variables: Record // 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:**
```tsx theme={null}
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.
```tsx theme={null}
{
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
Emitted when users trigger actions configured with "Emit Event" behavior in the Muta web editor.
```tsx theme={null}
{
type: string, // Your custom event name (e.g., 'signup_clicked', 'plan_selected')
timestamp: number, // Unix timestamp in ms
placementId: string, // ID of the placement
flowName?: string, // Name of the flow (if configured)
screenIndex?: number, // Current screen index
eventData?: any // Custom data configured in the editor
}
```
#### 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`)
3. Optionally add custom data to pass along with the event
#### Listening for Custom Events
```tsx theme={null}
// Listen for specific custom events
Muta.on('signup_started', (event) => {
console.log('User started signup', event.eventData)
navigateToSignup()
})
Muta.on('plan_selected', (event) => {
const { planType, price } = event.eventData
trackPlanSelection(planType, price)
})
```
## Integration Examples
### Mixpanel Example
```tsx theme={null}
import { Muta } from '@mutalabs/react-native-muta'
import mixpanel from 'mixpanel-react-native'
function setupAnalytics() {
// Track standard events
Muta.on('flow_completed', (event) => {
const properties = {
flow_name: event.flowName,
total_screens: event.totalScreens,
}
// Include collected variables if present
if (event.eventData?.variables) {
properties.variables = event.eventData.variables
}
mixpanel.track('Onboarding Complete', properties)
})
// Track custom events
Muta.on('form_submitted', (event) => {
mixpanel.track('Form Submitted', {
flow_name: event.flowName,
form_data: event.eventData,
})
})
Muta.on('flow_abandoned', (event) => {
mixpanel.track('Onboarding Abandoned', {
flow_name: event.flowName,
last_screen_index: event.lastScreenIndex,
total_screens: event.totalScreens,
})
})
}
```
### AppsFlyer Example
```tsx theme={null}
import { Muta } from '@mutalabs/react-native-muta'
import appsFlyer from 'react-native-appsflyer'
function setupAnalytics() {
Muta.on('*', (event) => {
const eventName = `onboarding_${event.type}`
appsFlyer.logEvent(eventName, {
placement_id: event.placementId,
flow_name: event.flowName,
timestamp: new Date(event.timestamp).toISOString(),
...event,
})
})
}
```
## Best Practices
1. **Event Naming**: Use consistent event naming conventions across your analytics platform.
2. **Data Enrichment**: Add additional context to events when needed (e.g., user ID, app version).
3. **Error Handling**: Implement proper error handling for analytics tracking.
4. **Performance**: Consider batching events for better performance, especially for high-frequency events.
5. **Privacy**: Ensure you're not tracking sensitive information in your analytics events.
# API Reference
Source: https://docs.mutalabs.com/sdk/react-native/api-reference
Complete API reference for the React Native SDK
## Components
### MutaRoot
The root component that initializes the SDK and manages placements.
```tsx
```
#### Props
| Prop | Type | Required | Description |
| -------- | -------- | -------- | ----------------- |
| `apiKey` | `string` | Yes | Your Muta API key |
## Methods
### Muta.displayPlacement()
Displays a placement in a modal overlay.
```tsx
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
}>
}
interface CodeScreenProps {
onContinue: () => void
onBack: () => void
variables: any[]
screenId: string
screenIndex: number
}
Muta.displayPlacement(options: DisplayPlacementOptions)
```
#### Options
| Option | Type | Required | Description |
| ------------------ | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `placementId` | `string` | Yes | The ID of the placement to display |
| `loadingScreen` | `React.ReactNode` | No | A custom React component to show while the placement is loading |
| `bgColor` | `string` | Yes | Background color that matches your first placement screen. This creates a seamless transition between your app and the placement. |
| `presentationType` | `'slide' \| 'fade' \| 'none'` | No | Animation style when showing the placement. Defaults to 'slide'. |
| `injectedScreens` | `Array` | No | Array of native React components to inject into code screens. See [Code Screens](/sdk/react-native/code-screens) for details. |
### Muta.on()
Subscribe to Muta events.
```tsx
type MutaEventType =
| 'flow_started'
| 'screen_viewed'
| 'flow_completed'
| 'flow_abandoned'
| 'error'
| string // Any custom event name
| '*' // Listen to all events
type MutaEventCallback = (event: MutaEvent) => void
function on(eventType: MutaEventType, callback: MutaEventCallback): { remove: () => void }
```
#### Parameters
| Parameter | Type | Required | Description |
| ----------- | ------------------- | -------- | ------------------------------------------------------------------ |
| `eventType` | `MutaEventType` | Yes | The type of event to listen for. Use '\*' to listen to all events. |
| `callback` | `MutaEventCallback` | Yes | The callback function to be called when the event occurs. |
#### Returns
An object with a `remove` method to unsubscribe from the event.
## Types
### MutaEvent
Base interface for all Muta events.
```tsx
interface MutaEvent {
type: MutaEventType
timestamp: number
placementId: string
flowName?: string
}
```
### FlowStartedEvent
```tsx
interface FlowStartedEvent extends MutaEvent {
type: 'flow_started'
totalScreens: number
}
```
### ScreenViewedEvent
```tsx
interface ScreenViewedEvent extends MutaEvent {
type: 'screen_viewed'
screenIndex: number
totalScreens: number
screenName?: string
}
```
### FlowCompletedEvent
```tsx
interface FlowCompletedEvent extends MutaEvent {
type: 'flow_completed'
screenIndex: number
totalScreens: number
screenName?: string
eventData?: { // Optional: Contains collected variables
variables: Record // Variable IDs mapped to their values
}
}
```
### FlowAbandonedEvent
```tsx
interface FlowAbandonedEvent extends MutaEvent {
type: 'flow_abandoned'
screenIndex: number
totalScreens: number
lastScreenIndex: number
screenName?: string
}
```
### CustomEvent
```tsx
interface CustomEvent extends MutaEvent {
type: string // Your custom event name
timestamp: number // Unix timestamp in ms
placementId: string // ID of the placement
flowName?: string // Name of the flow (if configured)
screenIndex?: number // Current screen index
eventData?: any // Custom data configured in the editor
}
```
Custom events are configured in the Muta web editor using the "Emit Event" behavior and can have any name you choose (e.g., `'signup_clicked'`, `'plan_selected'`, `'survey_completed'`).
### ErrorEvent
```tsx
interface ErrorEvent extends MutaEvent {
type: 'error'
message: string
code?: string
}
```
## Example Usage
```tsx
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 (
<>
{/* Your app content */}
>
);
}
```
# Code Screens
Source: https://docs.mutalabs.com/sdk/react-native/code-screens
Inject native React components into your Muta flows for custom functionality
Code Screens enable you to seamlessly integrate custom native React components into your remotely configured Muta flows. This powerful feature allows you to combine the flexibility of no-code flow management with the full capabilities of native development.
## Overview
Code Screens act as placeholders in your Muta flows that are replaced with native React components at runtime. This enables you to:
* Add authentication screens to your onboarding flows
* Integrate payment processing directly in your flows
* Include complex native UI that requires custom logic
* Maintain remote configurability while using native code
## How It Works
In the Muta web editor, add a "Code Screen" to your flow and give it a unique name (e.g., "Login Screen", "Payment Form")
Set up the continue and back navigation destinations for your code screen
Pass your React components when displaying the placement in your app
## Implementation
### Basic Setup
Pass your custom components when calling `displayPlacement`:
```tsx
import { Muta } from '@mutalabs/react-native-muta';
import { LoginScreen } from './screens/LoginScreen';
import { PaymentScreen } from './screens/PaymentScreen';
Muta.displayPlacement({
placementId: 'onboarding',
bgColor: '#000000',
presentationType: 'none',
injectedScreens: [
{
screenName: 'Login Screen', // Must match the name in Muta editor
component: LoginScreen,
},
{
screenName: 'Payment Form',
component: PaymentScreen,
},
],
});
```
### Component Interface
Your injected components receive the following props:
```tsx
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
}
```
### Example Component
Here's a complete example of a login screen component:
```tsx
import React, { useState } from 'react';
import { View, TextInput, Button, StyleSheet, Text } from 'react-native';
function LoginScreen({ onContinue, onBack, variables }: CodeScreenProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleLogin = async () => {
setError('');
setLoading(true);
try {
// Your authentication logic
const response = await fetch('https://api.example.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
const data = await response.json();
// Store auth token, update user state, etc.
await AsyncStorage.setItem('authToken', data.token);
// Navigate to the next screen in the flow
onContinue();
} else {
setError('Invalid email or password');
}
} catch (err) {
setError('Network error. Please try again.');
} finally {
setLoading(false);
}
};
return (
Welcome Back
{error ? {error} : null}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 30,
textAlign: 'center',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
padding: 15,
marginBottom: 15,
borderRadius: 8,
},
error: {
color: 'red',
marginBottom: 15,
textAlign: 'center',
},
});
```
## Animations
Code screens automatically animate in and out matching your flow's configured animations:
* **Entry Animation**: Matches the animation used when navigating TO the code screen
* **Exit Animation**: Uses the configured continue/back animation settings
* **Seamless Transitions**: Native screens blend smoothly with your flow screens
## Use Cases
### Authentication Flows
Integrate login, signup, or authentication screens directly into your onboarding:
```tsx
injectedScreens: [
{ screenName: 'Login', component: LoginScreen },
{ screenName: 'Signup', component: SignupScreen },
{ screenName: 'Verify Email', component: VerifyEmailScreen },
]
```
### Payment Integration
Add payment processing without breaking the flow experience:
```tsx
injectedScreens: [
{ screenName: 'Payment Method', component: PaymentMethodScreen },
{ screenName: 'Billing Details', component: BillingDetailsScreen },
{ screenName: 'Confirm Purchase', component: ConfirmPurchaseScreen },
]
```
### Complex Forms
Handle multi-step forms or complex data entry:
```tsx
injectedScreens: [
{ screenName: 'Profile Setup', component: ProfileSetupScreen },
{ screenName: 'Preferences', component: PreferencesScreen },
{ screenName: 'Location Permissions', component: LocationPermissionScreen },
]
```
## Best Practices
* Use descriptive, unique names for your code screens
* Keep names consistent between the Muta editor and your code
* Avoid special characters in screen names
* Always handle errors gracefully in your components
* Provide clear feedback to users
* Consider offering retry options for network failures
* Always call `onContinue()` or `onBack()` to maintain flow continuity
* Disable navigation buttons during async operations
* Consider confirmation dialogs for destructive back actions
* Keep components lightweight and focused
* Avoid heavy computations in render methods
* Use React.memo for performance-critical components
## Troubleshooting
Screen names must match exactly between the Muta editor and your injected screens configuration. A mismatch will result in the code screen not displaying.
### Common Issues
**Code screen not appearing:**
* Verify the screen name matches exactly (case-sensitive)
* Ensure the component is properly imported
* Check that injectedScreens is passed to displayPlacement
**Navigation not working:**
* Confirm you're calling onContinue() or onBack()
* Check that navigation destinations are configured in the editor
* Ensure no JavaScript errors are preventing execution
**Animation issues:**
* Verify presentationType is set appropriately
* Check that animation durations are reasonable (200-500ms)
* Ensure components render quickly to avoid animation lag
## Advanced Usage
### Accessing Flow Variables
Code screens can read and react to flow variables:
```tsx
function PaymentScreen({ variables, onContinue }: CodeScreenProps) {
// Find specific variable by ID
const planType = variables.find(v => v.id === 'selected_plan')?.value;
const userEmail = variables.find(v => v.id === 'user_email')?.value;
// Use variables to customize the screen
const price = planType === 'premium' ? '$9.99' : '$4.99';
// ... rest of component
}
```
### Sequential Code Screens
You can navigate directly from one code screen to another:
```tsx
// In Muta editor: Login Screen â Verification Screen â Welcome Screen
injectedScreens: [
{ screenName: 'Login Screen', component: LoginScreen },
{ screenName: 'Verification Screen', component: VerificationScreen },
// Welcome Screen is a regular Muta screen
]
```
The SDK handles transitions smoothly between consecutive code screens without any special configuration.
# Deep Linking
Source: https://docs.mutalabs.com/sdk/react-native/deep-linking
Create dynamic onboarding experiences based on user acquisition sources
Deep linking allows you to show different onboarding flows based on where your users come from. This is perfect for tailoring your user experience to match the messaging from different ad campaigns, product pages, or referral sources.
## How It Works
Instead of creating complex variant systems, Muta's placement system already supports deep linking scenarios perfectly. You simply create different placements for different user sources.
## Example: Multi-Channel Ad Campaign
Let's say you're running ads on TikTok, Instagram, and LinkedIn with different messaging for each platform. You want your onboarding to match the ad creative users clicked on.
### Step 1: Create Separate Placements
In your Muta dashboard, create placements for each source:
* `onboarding-tiktok` - Fun, video-focused onboarding
* `onboarding-instagram` - Visual, story-driven flow
* `onboarding-linkedin` - Professional, feature-focused flow
* `onboarding-default` - Generic flow for organic users
### Step 2: Handle Deep Links in Your App
```tsx
import { Muta } from '@mutalabs/react-native-muta';
import { Linking } from 'react-native';
// Parse the deep link to determine the source
const handleDeepLink = (url: string) => {
const source = parseSourceFromUrl(url); // Your URL parsing logic
// Map sources to placement IDs
const placementMap = {
'tiktok': 'onboarding-tiktok',
'instagram': 'onboarding-instagram',
'linkedin': 'onboarding-linkedin',
};
const placementId = placementMap[source] || 'onboarding-default';
// Display the appropriate onboarding flow
Muta.displayPlacement({
placementId: placementId,
bgColor: '#000000'
});
};
// Listen for deep links
Linking.addEventListener('url', (event) => {
handleDeepLink(event.url);
});
```
### Step 3: Configure Your Ad Campaign URLs
Set up your ad campaign URLs to include source information:
* TikTok: `myapp://onboard?source=tiktok`
* Instagram: `myapp://onboard?source=instagram`
* LinkedIn: `myapp://onboard?source=linkedin`
## Best Practices
### 1. Naming Conventions
Use clear, consistent naming for your placements:
* `[placement-name]-[source]`
* `[placement-name]-[source]-[campaign]`
* `[placement-name]-[source]-[variant]`
### 2. Always Have a Default
Always create a default placement as a fallback for:
* Organic users
* Unrecognized sources
## Next Steps
* Learn about [Analytics Integration](/sdk/react-native/analytics) to track performance by source
* Explore [Error Handling](/sdk/react-native/error-handling) for fallback strategies
* View the [API Reference](/sdk/react-native/api-reference) for all available options
# Error Handling
Source: https://docs.mutalabs.com/sdk/react-native/error-handling
Learn how to handle errors in your Muta placements
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
```tsx
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 ;
}
```
## 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
* The flow will not display
* You should show an appropriate error message to the user
## Error Event Structure
### Network Error
```tsx
{
type: 'network_error',
message: string, // Error message
timestamp: number // Unix timestamp in ms
}
```
### Placement Error
```tsx
{
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
}
```
## Best Practices
1. **User-Friendly Messages**: Display error messages that are clear and actionable for users.
2. **Error Recovery**: Implement retry mechanisms for network errors.
3. **Error Logging**: Log errors for debugging and monitoring purposes.
4. **Fallback Content**: Provide fallback content or alternative flows when errors occur.
5. **Error Prevention**: Validate placement IDs and API keys before attempting to display flows.
## Example Implementation
```tsx
import { Muta } from '@mutalabs/react-native-muta';
import { Alert } from 'react-native';
function Onboarding() {
useEffect(() => {
const subscription = Muta.on('error', (error) => {
switch (error.type) {
case 'network_error':
Alert.alert(
'Connection Error',
'Please check your internet connection and try again.',
[
{
text: 'Retry',
onPress: () => {
// Retry the placement
Muta.displayPlacement({
placementId: 'your-placement-id',
bgColor: '#000000'
});
}
},
{
text: 'Cancel',
style: 'cancel'
}
]
);
break;
case 'placement_error':
Alert.alert(
'Error',
'Unable to load the onboarding flow. Please try again later.',
[
{
text: 'OK',
style: 'default'
}
]
);
break;
default:
console.error('Unknown error:', error);
}
});
// Show the placement
Muta.displayPlacement({
placementId: 'your-placement-id',
bgColor: '#000000'
});
return () => subscription.remove();
}, []);
return ;
}
```
## Error Codes
### Network Errors
* `NETWORK_ERROR`: General network connectivity issues
* `TIMEOUT_ERROR`: Request timeout
* `OFFLINE_ERROR`: Device is offline
### Placement Errors
* `INVALID_PLACEMENT_ID`: The provided placement ID is invalid or doesn't exist
* `INVALID_API_KEY`: The API key is invalid or has been revoked
* `PLACEMENT_NOT_FOUND`: The placement could not be found
* `PLACEMENT_DISABLED`: The placement has been disabled
* `PLACEMENT_EXPIRED`: The placement has expired
# React Native SDK
Source: https://docs.mutalabs.com/sdk/react-native/introduction
Create dynamic, remotely configurable onboarding experiences in your React Native mobile app
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, and icons
* ð **Smooth transitions** - Fade and slide animations
* ð§ **Code Screens** - Inject native components into remotely configured flows
* ð **Analytics integration** - Track user behavior with any analytics provider
* ð **User input collection** - Gather text and multiple choice responses
* ðŠ **Full TypeScript support**
* ðŠķ **Extremely lightweight**
## Installation
Choose your package manager:
```bash
# Using npm
npm install @mutalabs/react-native-muta react-native-webview
```
```bash
# Using yarn
yarn add @mutalabs/react-native-muta react-native-webview
```
For React Native CLI projects, install iOS dependencies:
```bash
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:
```tsx
import { MutaRoot, Muta } from '@mutalabs/react-native-muta';
function App() {
return (
<>
{/* Your app content */}
>
);
}
```
2. Display a placement:
```tsx
// 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: ,
presentationType: 'fade' // 'slide' | 'fade' | 'none'
})
```
## Next Steps
* Learn about [Code Screens](/sdk/react-native/code-screens) for native component injection
* Learn about [User Input Collection](/sdk/react-native/user-input)
* Explore [Analytics Integration](/sdk/react-native/analytics)
* Understand [Error Handling](/sdk/react-native/error-handling)
* View the [API Reference](/sdk/react-native/api-reference)
# Custom Events
Source: https://docs.mutalabs.com/sdk/react-native/user-input
Learn how to handle custom events from your Muta flows
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
## Example Usage
```tsx
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);
});
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 });
});
// Show the placement
Muta.displayPlacement({
placementId: 'your-placement-id',
bgColor: '#000000'
});
return () => {
signupListener.remove();
premiumListener.remove();
surveyListener.remove();
formListener.remove();
};
}, []);
return ;
}
```
## Custom Event Structure
```tsx
{
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
### User Actions
Track when users click specific CTAs or make choices:
```tsx
Muta.on('cta_clicked', (event) => {
const buttonName = event.eventData?.buttonName;
analytics.track('CTA Click', { button: buttonName });
});
```
### Navigation Control
Trigger app navigation based on flow interactions:
```tsx
Muta.on('go_to_home', () => navigation.navigate('Home'));
Muta.on('open_settings', () => navigation.navigate('Settings'));
Muta.on('start_tutorial', () => startTutorial());
```
### Data Collection
Gather form submissions or survey responses:
```tsx
Muta.on('preferences_selected', (event) => {
const preferences = event.eventData?.preferences;
AsyncStorage.setItem('userPreferences', JSON.stringify(preferences));
});
```
### Feature Flags
Enable/disable features based on onboarding choices:
```tsx
Muta.on('feature_toggle', (event) => {
const { feature, enabled } = event.eventData;
featureFlags.set(feature, enabled);
});
```
### Conversion Tracking
Track specific conversion events:
```tsx
Muta.on('trial_started', (event) => {
mixpanel.track('Trial Started', {
plan: event.eventData?.plan,
duration: event.eventData?.duration
});
});
Muta.on('purchase_initiated', (event) => {
analytics.track('Purchase Initiated', {
product: event.eventData?.product,
price: event.eventData?.price
});
});
```
## Best Practices
1. **Consistent Naming**: Use descriptive, consistent event names across your flows
2. **Type Safety**: Consider creating a TypeScript interface for your custom events
3. **Error Handling**: Always validate eventData before using it
4. **Documentation**: Document your custom events and their expected data structure
5. **Analytics Integration**: Forward custom events to your analytics provider for tracking
6. **Cleanup**: Always remove event listeners when components unmount to prevent memory leaks
# Analytics Integration
Source: https://docs.mutalabs.com/sdk/swift/analytics
Learn how to integrate analytics with your Muta placements
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.
## Basic Integration
Here's a simple example of how to integrate analytics with your Muta placements:
```swift theme={null}
import MutaSDK
class AnalyticsManager {
var subscription: Subscription?
func setupTracking() {
subscription = Muta.shared.on { event in
switch event {
case .flowStarted(let event):
Analytics.track("Flow Started", properties: [
"placement_id": event.placementId,
"flow_name": event.flowName ?? "",
"total_screens": event.totalScreens,
"timestamp": event.timestamp
])
case .flowCompleted(let event):
var properties: [String: Any] = [
"placement_id": event.placementId,
"flow_name": event.flowName,
"screen_index": event.screenIndex,
"total_screens": event.totalScreens,
"timestamp": event.timestamp
]
// Include collected variables if present
if let variables = event.eventData?["variables"] as? [String: Any] {
properties["variables"] = variables
}
Analytics.track("Flow Completed", properties: properties)
case .custom(let event):
Analytics.track(event.type, properties: [
"placement_id": event.placementId,
"flow_name": event.flowName ?? "",
"event_data": event.eventData ?? [:],
"timestamp": event.timestamp
])
// Handle other events...
}
}
}
deinit {
subscription?.unsubscribe()
}
}
```
## Available Events
### 1. Flow Started (`flowStarted`)
Emitted when a placement flow begins displaying.
```swift theme={null}
case flowStarted(FlowStartedEvent)
// Properties: placementId, flowName?, totalScreens, timestamp
```
### 2. Screen Viewed (`screenViewed`)
Emitted when a user views a new screen in the flow.
```swift theme={null}
case screenViewed(ScreenViewedEvent)
// Properties: placementId, flowName?, screenIndex, totalScreens, screenName?, timestamp
```
### 3. Flow Completed (`flowCompleted`)
Emitted when a user successfully finishes the flow.
```swift theme={null}
case flowCompleted(FlowCompletedEvent)
// Properties: placementId, flowName, screenIndex, totalScreens, screenName, timestamp, eventData?
// eventData contains variables collected during the flow
```
#### Variable Structure
When a flow completes, `eventData["variables"]` contains a dictionary where:
* **Key**: Variable ID (e.g., `var_1758310510757_as20wkeo4`)
* **Value**: Dictionary 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:**
```swift theme={null}
case .flowCompleted(let event):
if let variables = event.eventData?["variables"] as? [String: [String: Any]] {
for (varId, varData) in variables {
let name = varData["name"] as? String ?? ""
let value = varData["value"] as? String ?? ""
print("Variable '\(name)': \(value)")
}
}
```
**Output:**
```
Variable 'MultipleChoice1': Family
Variable 'MultipleChoice2': Fluent
```
### 4. Flow Abandoned (`flowAbandoned`)
Emitted when a user exits the flow before completion.
```swift theme={null}
case flowAbandoned(FlowAbandonedEvent)
// Properties: placementId, flowName?, screenIndex, totalScreens, lastScreenIndex, screenName?, timestamp
```
### 5. Custom Events
Emitted when users trigger actions configured with "Emit Event" behavior in the Muta web editor.
```swift theme={null}
case custom(CustomEvent)
// Properties: type, placementId, flowName?, screenIndex?, timestamp, eventData?
```
#### 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`)
3. Optionally add custom data to pass along with the event
#### Listening for Custom Events
```swift theme={null}
subscription = Muta.shared.on { event in
if case .custom(let customEvent) = event {
switch customEvent.type {
case "signup_started":
// Handle signup
navigateToSignup()
case "plan_selected":
if let planType = customEvent.eventData?["planType"] as? String {
selectPlan(planType)
}
default:
// Track any custom event
analytics.track(customEvent.type, properties: customEvent.eventData ?? [:])
}
}
}
```
### 6. Error Events (`error`)
Emitted when there are network or placement errors.
```swift theme={null}
case error(ErrorEvent)
// Types: .network(message, timestamp) or .placement(message, code, timestamp, placementId)
```
## Integration Examples
### Mixpanel Example
```swift theme={null}
import MutaSDK
import Mixpanel
class MixpanelAnalytics {
var subscription: Subscription?
func setupTracking() {
subscription = Muta.shared.on { event in
switch event {
case .flowCompleted(let event):
Mixpanel.mainInstance().track(
event: "Onboarding Complete",
properties: [
"flow_name": event.flowName,
"total_screens": event.totalScreens
]
)
case .flowAbandoned(let event):
Mixpanel.mainInstance().track(
event: "Onboarding Abandoned",
properties: [
"flow_name": event.flowName ?? "",
"last_screen_index": event.lastScreenIndex,
"total_screens": event.totalScreens
]
)
case .custom(let event):
// Track custom events
Mixpanel.mainInstance().track(
event: event.type,
properties: event.eventData ?? [:]
)
// Example: Track form submission
if event.type == "form_submitted" {
if let formData = event.eventData {
Mixpanel.mainInstance().track(
event: "Form Submitted",
properties: formData
)
}
}
default:
break
}
}
}
deinit {
subscription?.unsubscribe()
}
}
```
### AppsFlyer Example
```swift theme={null}
import MutaSDK
import AppsFlyerLib
class AppsFlyerAnalytics {
var subscription: Subscription?
func setupTracking() {
subscription = Muta.shared.on { event in
var eventName = ""
var properties: [String: Any] = [:]
switch event {
case .flowStarted(let e):
eventName = "onboarding_flow_started"
properties = [
"placement_id": e.placementId,
"flow_name": e.flowName ?? "",
"total_screens": e.totalScreens,
"timestamp": e.timestamp
]
case .screenViewed(let e):
eventName = "onboarding_screen_viewed"
properties = [
"placement_id": e.placementId,
"flow_name": e.flowName ?? "",
"screen_index": e.screenIndex,
"total_screens": e.totalScreens,
"screen_name": e.screenName ?? ""
]
case .flowCompleted(let e):
eventName = "onboarding_flow_completed"
properties = [
"placement_id": e.placementId,
"flow_name": e.flowName,
"screen_index": e.screenIndex,
"total_screens": e.totalScreens,
"screen_name": e.screenName
]
case .flowAbandoned(let e):
eventName = "onboarding_flow_abandoned"
properties = [
"placement_id": e.placementId,
"flow_name": e.flowName ?? "",
"screen_index": e.screenIndex,
"total_screens": e.totalScreens,
"last_screen_index": e.lastScreenIndex
]
case .custom(let e):
// Use the custom event type as the event name
eventName = e.type
properties = [
"placement_id": e.placementId,
"flow_name": e.flowName ?? "",
"screen_index": e.screenIndex ?? -1
]
// Merge custom event data
if let eventData = e.eventData {
properties.merge(eventData) { _, new in new }
}
case .error(let error):
eventName = "onboarding_error"
switch error {
case .network(let message, _):
properties = [
"error_message": message,
"error_type": "network"
]
case .placement(let message, let code, _, let placementId):
properties = [
"error_message": message,
"error_code": code,
"error_type": "placement",
"placement_id": placementId
]
@unknown default:
break
}
@unknown default:
break
}
if !eventName.isEmpty {
AppsFlyerLib.shared().logEvent(eventName, withValues: properties)
}
}
}
deinit {
subscription?.unsubscribe()
}
}
```
## Best Practices
1. **Event Naming**: Use consistent event naming conventions across your analytics platform.
2. **Data Enrichment**: Add additional context to events when needed (e.g., user ID, app version).
3. **Error Handling**: Implement proper error handling for analytics tracking.
4. **Custom Event Documentation**: Document your custom events and their expected data structure.
5. **Performance**: Consider batching events for better performance, especially for high-frequency events.
6. **Privacy**: Ensure you're not tracking sensitive information in your analytics events.
# API Reference
Source: https://docs.mutalabs.com/sdk/swift/api-reference
Complete API reference for the Swift SDK
## Methods
### Muta.configure()
Initializes the SDK with your API key.
```swift
Muta.configure(apiKey: String)
```
#### Parameters
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ----------------- |
| `apiKey` | `String` | Yes | Your Muta API key |
### Muta.shared.displayPlacement()
Displays a placement in a modal overlay.
```swift
// Using SwiftUI Color
func displayPlacement(
placementId: String,
backgroundColor: Color = .white,
presentationType: PresentationType = .slide,
loadingView: AnyView? = nil,
injectedScreens: [InjectedScreen] = []
)
// Using hex color string
func displayPlacement(
placementId: String,
backgroundHexColor: String,
presentationType: PresentationType = .slide,
loadingView: AnyView? = nil,
injectedScreens: [InjectedScreen] = []
)
```
#### Parameters
| Parameter | Type | Required | Description |
| -------------------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `placementId` | `String` | Yes | The ID of the placement to display |
| `backgroundColor` | `Color` | No | Background color that matches your first placement screen. Defaults to white. |
| `backgroundHexColor` | `String` | No | Background color as a hex string. Alternative to backgroundColor. |
| `presentationType` | `PresentationType` | No | Animation style when showing the placement. Defaults to .slide |
| `loadingView` | `AnyView?` | No | A custom SwiftUI view to show while the placement is loading |
| `injectedScreens` | `[InjectedScreen]` | No | Array of native SwiftUI views to inject into code screens. See [Code Screens](/sdk/swift/code-screens) for details. |
### Muta.shared.on()
Subscribe to Muta events.
```swift
// Subscribe to all events
func on(_ handler: @escaping (MutaEvent) -> Void) -> Subscription
// Subscribe to specific event type
func on(_ type: T.Type, handler: @escaping (T) -> Void) -> Subscription
```
#### Parameters
| Parameter | Type | Required | Description |
| --------- | --------------------- | -------- | ------------------------------------------------------- |
| `handler` | `(MutaEvent) -> Void` | Yes | The closure to be called when any event occurs |
| `type` | `T.Type` | Yes | The specific event type to listen for |
| `handler` | `(T) -> Void` | Yes | The closure to be called when the specific event occurs |
#### Returns
A `Subscription` object that can be used to unsubscribe from the event.
## Types
### MutaEvent
Base protocol for all Muta events.
```swift
protocol MutaEvent {
var timestamp: Int { get }
var placementId: String { get }
var flowName: String? { get }
}
```
### FlowStartedEvent
```swift
struct FlowStartedEvent: MutaEvent {
let timestamp: Int
let placementId: String
let flowName: String?
let totalScreens: Int
}
```
### ScreenViewedEvent
```swift
struct ScreenViewedEvent: MutaEvent {
let timestamp: Int
let placementId: String
let flowName: String?
let screenIndex: Int
let totalScreens: Int
let screenName: String?
}
```
### FlowCompletedEvent
```swift
struct FlowCompletedEvent: MutaEvent {
let timestamp: Int
let placementId: String
let flowName: String
let screenIndex: Int
let totalScreens: Int
let screenName: String
let eventData: [String: Any]? // Contains variables collected during the flow
}
```
### FlowAbandonedEvent
```swift
struct FlowAbandonedEvent: MutaEvent {
let timestamp: Int
let placementId: String
let flowName: String?
let screenIndex: Int
let totalScreens: Int
let lastScreenIndex: Int
let screenName: String?
}
```
### CodeScreenDisplayedEvent
```swift
struct CodeScreenDisplayedEvent {
let screenId: String
let screenName: String
let timestamp: Int
}
```
### CodeScreenActionEvent
```swift
struct CodeScreenActionEvent {
let action: String // "continue" or "back"
let screenId: String
let screenName: String
let timestamp: Int
}
```
### CustomEvent
```swift
struct CustomEvent: MutaEvent {
let type: String // Your custom event name
let timestamp: Int
let placementId: String
let flowName: String?
let screenIndex: Int?
let eventData: [String: Any]? // Custom data from the editor
}
```
#### Usage Example
```swift
// Listen for custom events configured in the Muta editor
let subscription = Muta.shared.on(CustomEvent.self) { event in
switch event.type {
case "signup_started":
// Handle signup button click
navigateToSignup()
case "plan_selected":
// Handle plan selection with custom data
if let planType = event.eventData?["planType"] as? String {
selectPlan(planType)
}
case "survey_completed":
// Handle survey completion
if let answers = event.eventData?["answers"] as? [String: Any] {
saveSurveyData(answers)
}
default:
break
}
}
```
### InjectedScreen
```swift
struct InjectedScreen {
let screenName: String // Must match name in Muta editor
let view: (CodeScreenContext) -> AnyView // Factory function for the view
}
```
### CodeScreenContext
```swift
struct CodeScreenContext {
let onContinue: () -> Void // Navigate to configured continue destination
let onBack: () -> Void // Navigate to configured back destination
let variables: [[String: Any]] // Current flow variables
let screenId: String // Unique screen ID
let screenIndex: Int // Screen position in flow
}
```
#### Usage Example
```swift
// Inject a login screen into your flow
Muta.shared.displayPlacement(
placementId: "onboarding",
backgroundColor: .black,
injectedScreens: [
InjectedScreen(
screenName: "Login Screen",
view: { context in
AnyView(LoginScreen(context: context))
}
)
]
)
```
### ErrorEvent
```swift
enum ErrorEvent {
case network(message: String, timestamp: Int)
case placement(message: String, code: String, timestamp: Int, placementId: String)
}
```
## Example Usage
```swift
import SwiftUI
import MutaSDK
@main
struct YourApp: App {
init() {
Muta.configure(apiKey: "your-api-key")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
Button("Show Placement") {
Muta.shared.displayPlacement(
placementId: "your-placement-id",
backgroundColor: .white,
presentationType: .fade,
loadingView: AnyView(
VStack {
ProgressView()
Text("Loading...")
}
)
)
}
}
}
```
# Code Screens
Source: https://docs.mutalabs.com/sdk/swift/code-screens
Inject native SwiftUI components into your Muta flows for custom functionality
Code Screens enable you to seamlessly integrate custom native SwiftUI views into your remotely configured Muta flows. This powerful feature allows you to combine the flexibility of no-code flow management with the full capabilities of native development.
## Overview
Code Screens act as placeholders in your Muta flows that are replaced with native SwiftUI views at runtime. This enables you to:
* Add authentication screens to your onboarding flows
* Integrate payment processing directly in your flows
* Include complex native UI that requires custom logic
* Maintain remote configurability while using native code
## How It Works
In the Muta web editor, add a "Code Screen" to your flow and give it a unique name (e.g., "Login Screen", "Payment Form")
Set up the continue and back navigation destinations for your code screen
Pass your SwiftUI views when displaying the placement in your app
## Implementation
### Basic Setup
Pass your custom views when calling `displayPlacement`:
```swift
import MutaSDK
Muta.shared.displayPlacement(
placementId: "onboarding",
backgroundColor: .black,
presentationType: .none,
injectedScreens: [
InjectedScreen(
screenName: "Login Screen", // Must match the name in Muta editor
view: { context in
AnyView(LoginScreen(context: context))
}
),
InjectedScreen(
screenName: "Payment Form",
view: { context in
AnyView(PaymentScreen(context: context))
}
)
]
)
```
### Component Interface
Your injected views receive a `CodeScreenContext` with the following properties:
```swift
public struct CodeScreenContext {
/// Navigate to the next screen configured in the flow
public let onContinue: () -> Void
/// Navigate to the previous screen configured in the flow
public let onBack: () -> Void
/// Current flow variables
public let variables: [[String: Any]]
/// Unique identifier for this screen
public let screenId: String
/// Position of this screen in the flow
public let screenIndex: Int
}
```
### Example Component
Here's a complete example of a login screen component:
```swift
import SwiftUI
import MutaSDK
struct LoginScreen: View {
let context: CodeScreenContext
@State private var email = ""
@State private var password = ""
@State private var isLoading = false
@State private var errorMessage = ""
var body: some View {
VStack(spacing: 24) {
Spacer()
// Title
VStack(spacing: 8) {
Text("Welcome Back")
.font(.system(size: 32, weight: .bold))
.foregroundColor(.white)
Text("Sign in to continue")
.font(.system(size: 16))
.foregroundColor(.white.opacity(0.7))
}
Spacer()
// Form
VStack(spacing: 16) {
// Email field
VStack(alignment: .leading, spacing: 8) {
Text("Email")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white.opacity(0.7))
TextField("your.email@example.com", text: $email)
.textFieldStyle(.plain)
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.autocorrectionDisabled()
.padding()
.background(Color.white.opacity(0.1))
.foregroundColor(.white)
.cornerRadius(12)
}
// Password field
VStack(alignment: .leading, spacing: 8) {
Text("Password")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white.opacity(0.7))
SecureField("Enter your password", text: $password)
.textFieldStyle(.plain)
.padding()
.background(Color.white.opacity(0.1))
.foregroundColor(.white)
.cornerRadius(12)
}
// Error message
if !errorMessage.isEmpty {
Text(errorMessage)
.font(.system(size: 14))
.foregroundColor(.red)
}
}
.padding(.horizontal, 24)
Spacer()
// Buttons
VStack(spacing: 12) {
// Login button
Button(action: handleLogin) {
HStack {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Sign In")
.font(.system(size: 16, weight: .semibold))
}
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(isLoading || email.isEmpty || password.isEmpty)
.opacity((email.isEmpty || password.isEmpty) ? 0.5 : 1.0)
// Back button
Button(action: {
context.onBack()
}) {
Text("Back")
.font(.system(size: 16, weight: .medium))
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.white)
}
.disabled(isLoading)
}
.padding(.horizontal, 24)
.padding(.bottom, 32)
}
.background(Color.black)
}
private func handleLogin() {
errorMessage = ""
isLoading = true
// Simulate login API call
Task {
do {
try await performLogin(email: email, password: password)
// Navigate to next screen in flow
await MainActor.run {
isLoading = false
context.onContinue()
}
} catch {
await MainActor.run {
isLoading = false
errorMessage = "Invalid email or password"
}
}
}
}
private func performLogin(email: String, password: String) async throws {
// Your authentication logic here
let url = URL(string: "https://api.example.com/login")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["email": email, "password": password]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NSError(domain: "LoginError", code: 401)
}
// Store auth token, update user state, etc.
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let token = json["token"] as? String {
UserDefaults.standard.set(token, forKey: "authToken")
}
}
}
```
## Animations
Code screens automatically animate in and out matching your flow's configured animations:
* **Entry Animation**: Matches the animation used when navigating TO the code screen
* **Exit Animation**: Uses the configured continue/back animation settings
* **Seamless Transitions**: Native screens blend smoothly with your flow screens
## Use Cases
### Authentication Flows
Integrate login, signup, or authentication screens directly into your onboarding:
```swift
injectedScreens: [
InjectedScreen(screenName: "Login", view: { AnyView(LoginScreen(context: $0)) }),
InjectedScreen(screenName: "Signup", view: { AnyView(SignupScreen(context: $0)) }),
InjectedScreen(screenName: "Verify Email", view: { AnyView(VerifyEmailScreen(context: $0)) })
]
```
### Payment Integration
Add payment processing without breaking the flow experience:
```swift
injectedScreens: [
InjectedScreen(screenName: "Payment Method", view: { AnyView(PaymentMethodScreen(context: $0)) }),
InjectedScreen(screenName: "Billing Details", view: { AnyView(BillingDetailsScreen(context: $0)) }),
InjectedScreen(screenName: "Confirm Purchase", view: { AnyView(ConfirmPurchaseScreen(context: $0)) })
]
```
### Complex Forms
Handle multi-step forms or complex data entry:
```swift
injectedScreens: [
InjectedScreen(screenName: "Profile Setup", view: { AnyView(ProfileSetupScreen(context: $0)) }),
InjectedScreen(screenName: "Preferences", view: { AnyView(PreferencesScreen(context: $0)) }),
InjectedScreen(screenName: "Location Permissions", view: { AnyView(LocationPermissionScreen(context: $0)) })
]
```
## Best Practices
* Use descriptive, unique names for your code screens
* Keep names consistent between the Muta editor and your code
* Avoid special characters in screen names
* Always handle errors gracefully in your views
* Provide clear feedback to users
* Consider offering retry options for network failures
* Always call `onContinue()` or `onBack()` to maintain flow continuity
* Disable navigation buttons during async operations
* Consider confirmation dialogs for destructive back actions
* Keep views lightweight and focused
* Avoid heavy computations in body methods
* Use `@State` and `@Binding` appropriately
## Events
When code screens are displayed or interacted with, the SDK emits specific events:
```swift
import MutaSDK
let subscription = Muta.shared.on { event in
switch event {
case .codeScreenDisplayed(let event):
print("Code screen displayed: \(event.screenName)")
// Track in analytics, etc.
case .codeScreenAction(let event):
print("Code screen action: \(event.action)") // "continue" or "back"
// Track navigation events
default:
break
}
}
```
## Troubleshooting
Screen names must match exactly between the Muta editor and your injected screens configuration. A mismatch will result in the code screen not displaying.
### Common Issues
**Code screen not appearing:**
* Verify the screen name matches exactly (case-sensitive)
* Ensure the view is properly wrapped in `AnyView`
* Check that injectedScreens is passed to displayPlacement
**Navigation not working:**
* Confirm you're calling `onContinue()` or `onBack()`
* Check that navigation destinations are configured in the editor
* Ensure no SwiftUI errors are preventing execution
**Animation issues:**
* Verify presentationType is set appropriately
* Check that animation durations are reasonable (200-500ms)
* Ensure views render quickly to avoid animation lag
## Advanced Usage
### Accessing Flow Variables
Code screens can read and react to flow variables:
```swift
struct PaymentScreen: View {
let context: CodeScreenContext
var body: some View {
VStack {
// Find specific variable by ID
let planType = context.variables.first {
($0["id"] as? String) == "selected_plan"
}?["value"] as? String
let userEmail = context.variables.first {
($0["id"] as? String) == "user_email"
}?["value"] as? String
// Use variables to customize the screen
let price = planType == "premium" ? "$9.99" : "$4.99"
Text("Subscribe to \(planType ?? "Basic") Plan")
Text("Price: \(price)")
Text("Email: \(userEmail ?? "N/A")")
// ... rest of view
}
}
}
```
### Sequential Code Screens
You can navigate directly from one code screen to another:
```swift
// In Muta editor: Login Screen â Verification Screen â Welcome Screen
injectedScreens: [
InjectedScreen(screenName: "Login Screen", view: { AnyView(LoginScreen(context: $0)) }),
InjectedScreen(screenName: "Verification Screen", view: { AnyView(VerificationScreen(context: $0)) })
// Welcome Screen is a regular Muta screen
]
```
The SDK handles transitions smoothly between consecutive code screens without any special configuration.
# Deep Linking
Source: https://docs.mutalabs.com/sdk/swift/deep-linking
Create dynamic onboarding experiences based on user acquisition sources
Deep linking allows you to show different onboarding flows based on where your users come from. This is perfect for tailoring your user experience to match the messaging from different ad campaigns, product pages, or referral sources.
## How It Works
Instead of creating complex variant systems, Muta's placement system already supports deep linking scenarios perfectly. You simply create different placements for different user sources.
## Example: Multi-Channel Ad Campaign
Let's say you're running ads on TikTok, Instagram, and LinkedIn with different messaging for each platform. You want your onboarding to match the ad creative users clicked on.
### Step 1: Create Separate Placements
In your Muta dashboard, create placements for each source:
* `onboarding-tiktok` - Fun, video-focused onboarding
* `onboarding-instagram` - Visual, story-driven flow
* `onboarding-linkedin` - Professional, feature-focused flow
* `onboarding-default` - Generic flow for organic users
### Step 2: Handle Deep Links in Your App
```swift
import UIKit
import Muta
// In your AppDelegate or SceneDelegate
func handleDeepLink(url: URL) {
// Parse the source from the URL
let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
let source = components?.queryItems?.first(where: { $0.name == "source" })?.value
// Map sources to placement IDs
let placementMap = [
"tiktok": "onboarding-tiktok",
"instagram": "onboarding-instagram",
"linkedin": "onboarding-linkedin"
]
let placementId = placementMap[source ?? ""] ?? "onboarding-default"
// Display the appropriate onboarding flow
Muta.shared.displayPlacement(
placementId: placementId,
presentingViewController: topViewController()
)
}
// Handle Universal Links
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
handleDeepLink(url: url)
return true
}
return false
}
// Handle URL Schemes
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
handleDeepLink(url: url)
return true
}
```
### Step 3: Configure Your Ad Campaign URLs
Set up your ad campaign URLs to include source information:
* TikTok: `myapp://onboard?source=tiktok`
* Instagram: `myapp://onboard?source=instagram`
* LinkedIn: `myapp://onboard?source=linkedin`
## SwiftUI Integration
For SwiftUI apps:
```swift
import SwiftUI
import Muta
struct DeepLinkHandler: ViewModifier {
@State private var showingPlacement = false
@State private var placementId = ""
func body(content: Content) -> some View {
content
.onOpenURL { url in
handleDeepLink(url: url)
}
.mutaPlacement(
isPresented: $showingPlacement,
placementId: placementId
)
}
private func handleDeepLink(url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
let source = components?.queryItems?.first(where: { $0.name == "source" })?.value
let placementMap = [
"tiktok": "onboarding-tiktok",
"instagram": "onboarding-instagram",
"linkedin": "onboarding-linkedin"
]
placementId = placementMap[source ?? ""] ?? "onboarding-default"
showingPlacement = true
}
}
// Usage
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modifier(DeepLinkHandler())
}
}
}
```
## Best Practices
### 1. Naming Conventions
Use clear, consistent naming for your placements:
* `[placement-name]-[source]`
* `[placement-name]-[source]-[campaign]`
* `[placement-name]-[source]-[variant]`
### 2. Always Have a Default
Always create a default placement as a fallback for:
* Organic users
* Unrecognized sources
## URL Scheme Configuration
Don't forget to configure your URL schemes in Info.plist:
```xml
CFBundleURLTypesCFBundleURLSchemesmyapp
```
## Next Steps
* Learn about [Analytics Integration](/sdk/swift/analytics) to track performance by source
* Explore [Error Handling](/sdk/swift/error-handling) for fallback strategies
* View the [API Reference](/sdk/swift/api-reference) for all available options
# Error Handling
Source: https://docs.mutalabs.com/sdk/swift/error-handling
Learn how to handle errors in your Muta placements
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
```swift
import MutaSDK
import SwiftUI
struct OnboardingView: View {
@State private var errorMessage: String?
@State private var showError = false
var body: some View {
VStack {
// Your content
}
.alert("Error", isPresented: $showError) {
Button("OK", role: .cancel) { }
} message: {
Text(errorMessage ?? "An unknown error occurred")
}
.onAppear {
setupErrorHandling()
}
}
private func setupErrorHandling() {
let subscription = Muta.shared.on { event in
if case .error(let error) = event {
switch error {
case .network(let message, _):
errorMessage = "Please check your internet connection and try again."
showError = true
case .placement(let message, _, _, _):
errorMessage = "Unable to load the onboarding flow. Please try again later."
showError = true
}
}
}
// Show the placement
Muta.shared.displayPlacement(
placementId: "your-placement-id",
backgroundColor: .white
)
}
}
```
## 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 trying to display a placement that has been deleted
* May indicate an incorrect placement ID or API key
* The flow will not display
* You should show an appropriate error message to the user
## Error Event Structure
### Network Error
```swift
case network(message: String, timestamp: Int)
// message: Error message
// timestamp: Unix timestamp in ms
```
### Placement Error
```swift
case placement(message: String, code: String, timestamp: Int, placementId: String)
// message: Error message
// code: Error code
// timestamp: Unix timestamp in ms
// placementId: ID of the placement that failed
```
## Best Practices
1. **User-Friendly Messages**: Display error messages that are clear and actionable for users.
2. **Error Recovery**: Implement retry mechanisms for network errors.
3. **Error Logging**: Log errors for debugging and monitoring purposes.
4. **Fallback Content**: Provide fallback content or alternative flows when errors occur.
## Example Implementation
```swift
import MutaSDK
import SwiftUI
struct OnboardingView: View {
@State private var errorMessage: String?
@State private var showError = false
@State private var showRetry = false
var body: some View {
VStack {
// Your content
}
.alert("Error", isPresented: $showError) {
if showRetry {
Button("Retry") {
retryPlacement()
}
}
Button("OK", role: .cancel) { }
} message: {
Text(errorMessage ?? "An unknown error occurred")
}
.onAppear {
setupErrorHandling()
}
}
private func setupErrorHandling() {
let subscription = Muta.shared.on { event in
if case .error(let error) = event {
switch error {
case .network(let message, _):
errorMessage = "Please check your internet connection and try again."
showError = true
showRetry = true
case .placement(let message, _, _, _):
errorMessage = "Unable to load the onboarding flow. Please try again later."
showError = true
showRetry = false
}
}
}
// Show the placement
Muta.shared.displayPlacement(
placementId: "your-placement-id",
backgroundColor: .white
)
}
private func retryPlacement() {
Muta.shared.displayPlacement(
placementId: "your-placement-id",
backgroundColor: .white
)
}
}
```
## Error Codes
### Network Errors
* `NETWORK_ERROR`: General network connectivity issues
* `TIMEOUT_ERROR`: Request timeout
* `OFFLINE_ERROR`: Device is offline
### Placement Errors
* `INVALID_PLACEMENT_ID`: The provided placement ID is invalid or doesn't exist
* `INVALID_API_KEY`: The API key is invalid or has been revoked
* `PLACEMENT_NOT_FOUND`: The placement could not be found
* `PLACEMENT_DISABLED`: The placement has been disabled
* `PLACEMENT_EXPIRED`: The placement has expired
# Swift SDK
Source: https://docs.mutalabs.com/sdk/swift/introduction
Create dynamic, remotely configurable onboarding experiences in your iOS app
A Swift SDK for creating dynamic, remotely configurable onboarding experiences in your iOS 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, and icons
* ð **Smooth transitions** - Fade and slide animations
* ð§ **Code Screens** - Inject native SwiftUI components into remotely configured flows
* ð **Analytics integration** - Track user behavior with any analytics provider
* ð **User input collection** - Gather text and multiple choice responses
* ðŠ **Full Swift support**
* ðŠķ **Extremely lightweight**
## Installation
### Swift Package Manager
Add the following dependency to your Package.swift:
```swift
dependencies: [
.package(url: "https://github.com/muta-labs/MutaSDK-iOS", from: "1.0.0")
]
```
Or in Xcode:
1. File > Add Package Dependencies
2. Enter package URL: `https://github.com/muta-labs/MutaSDK-iOS`
3. Click "Add Package"
## Quick Start
1. Initialize the SDK:
```swift
import MutaSDK
// In your app initialization
Muta.configure(apiKey: "your-api-key")
```
2. Display a placement:
```swift
// Basic usage
Muta.shared.displayPlacement(
placementId: "your-placement-id",
backgroundColor: .white
)
// With custom loading view and presentation type
Muta.shared.displayPlacement(
placementId: "your-placement-id",
backgroundColor: .white,
presentationType: .fade,
loadingView: AnyView(
VStack {
Image("your-logo")
.resizable()
.frame(width: 100, height: 100)
ProgressView()
Text("Loading...")
}
)
)
```
## Next Steps
* Learn about [Code Screens](/sdk/swift/code-screens) for native component injection
* Learn about [User Input Collection](/sdk/swift/user-input)
* Explore [Analytics Integration](/sdk/swift/analytics)
* Understand [Error Handling](/sdk/swift/error-handling)
* View the [API Reference](/sdk/swift/api-reference)
# Custom Events
Source: https://docs.mutalabs.com/sdk/swift/user-input
Learn how to handle custom events from your Muta flows
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
## Example Usage
```swift
import MutaSDK
import SwiftUI
struct OnboardingView: View {
@State private var isPremiumUser = false
@State private var surveyData: [String: Any] = [:]
var body: some View {
VStack {
// Your content
}
.onAppear {
setupCustomEventHandlers()
}
}
private func setupCustomEventHandlers() {
// Listen for custom events
let subscription = Muta.shared.on(CustomEvent.self) { event in
switch event.type {
case "signup_started":
// Handle signup button click
print("User started signup process")
navigateToSignup()
case "premium_selected":
// Handle premium selection with custom data
if let planType = event.eventData?["planType"] as? String,
let price = event.eventData?["price"] as? Double {
print("User selected \(planType) plan at $\(price)")
isPremiumUser = true
}
case "survey_completed":
// Handle survey completion
if let answers = event.eventData?["answers"] as? [String: Any] {
print("Survey completed with answers: \(answers)")
surveyData = answers
saveSurveyToBackend(answers)
}
case "form_submitted":
// Handle form submission
if let formData = event.eventData {
processFormData(formData)
}
default:
print("Received custom event: \(event.type)")
}
}
// Show the placement
Muta.shared.displayPlacement(
placementId: "your-placement-id",
backgroundColor: .white
)
}
}
```
## Custom Event Structure
```swift
struct CustomEvent: MutaEvent {
let type: String // Your custom event name
let timestamp: Int // Unix timestamp in ms
let placementId: String // ID of the placement
let flowName: String? // Name of the flow (if configured)
let screenIndex: Int? // Current screen index
let eventData: [String: Any]? // Custom data from the editor
}
```
## Common Use Cases
### User Actions
Track when users click specific CTAs or make choices:
```swift
Muta.shared.on(CustomEvent.self) { event in
if event.type == "cta_clicked" {
let buttonName = event.eventData?["buttonName"] as? String
analytics.track("CTA Click", ["button": buttonName ?? "unknown"])
}
}
```
### Navigation Control
Trigger app navigation based on flow interactions:
```swift
Muta.shared.on(CustomEvent.self) { event in
switch event.type {
case "go_to_home":
navigationController.popToRootViewController(animated: true)
case "open_settings":
presentSettingsScreen()
default:
break
}
}
```
### Data Collection
Gather form submissions or survey responses:
```swift
Muta.shared.on(CustomEvent.self) { event in
if event.type == "preferences_selected" {
if let preferences = event.eventData?["preferences"] as? [String] {
UserDefaults.standard.set(preferences, forKey: "userPreferences")
}
}
}
```
### Feature Flags
Enable/disable features based on onboarding choices:
```swift
Muta.shared.on(CustomEvent.self) { event in
if event.type == "feature_toggle" {
if let featureName = event.eventData?["feature"] as? String,
let enabled = event.eventData?["enabled"] as? Bool {
FeatureFlags.shared.setFeature(featureName, enabled: enabled)
}
}
}
```
## Best Practices
1. **Consistent Naming**: Use descriptive, consistent event names across your flows
2. **Type Safety**: Consider creating an enum for your event types to avoid typos
3. **Error Handling**: Always validate eventData before using it
4. **Documentation**: Document your custom events and their expected data structure
5. **Analytics Integration**: Forward custom events to your analytics provider for tracking