Skip to content

AI Implementation Guide - Expo

How to use this page: Copy everything below the horizontal line and paste it into your AI coding assistant (Claude, ChatGPT, GitHub Copilot, Cursor, etc.) to get accurate MovableInk SDK implementation help.

Sample Prompts

After pasting the context below into your AI assistant, try these prompts:

  • "Implement MovableInk deeplinking in my Expo app using the context I provided"
  • "Help me configure app.json for MovableInk Universal Links and App Links"
  • "Create the Expo config plugin for MovableInk Android setup"
  • "Implement behavior events to capture when users view products"
  • "Add orderCompleted event tracking to my checkout flow with TypeScript types"
  • "Help me test my MovableInk integration with EAS Build"
  • "Debug why my MovableInk deeplinks aren't working in Expo"
  • "Set up both iOS and Android deeplinking for Expo with CNG"

Context for AI Assistant

Copy everything inside the code block below and paste into your AI assistant:

# MovableInk Expo SDK Implementation Reference

## Overview

### What is MovableInk?
MovableInk is a marketing personalization platform that helps brands create dynamic, personalized content for email, mobile, and web. The Expo SDK (via React Native wrapper) enables two key capabilities:

1. **Deeplinking**: Users tap links in marketing emails → your app opens to specific content
2. **Behavior Events**: Capture user interactions (product views, purchases) to power personalization

### SDK Information
- **Current Version**: 2.3.0
- **Minimum Expo SDK**: 44
- **Minimum iOS**: 13.0
- **Minimum Android**: Android 7.0 (API 24)
- **Language**: TypeScript/JavaScript
- **Package**: `@movable/react-native-sdk`
- **Build Method**: Requires EAS Build or `expo prebuild` (Continuous Native Generation)

## Key Terminology

- **MIU (MovableInk User ID)**: Unique, non-PII identifier linking app users to marketing profiles (typically a UUID, NOT an email)
- **MovableInk Link**: URL format `https://mi.yourcompany.com/p/...` in marketing emails
- **Universal Links** (iOS): Deep linking using https URLs (requires Associated Domains capability)
- **App Links** (Android): Deep linking using https URLs (requires Digital Asset Links file)
- **AASA**: Apple App Site Association file at `https://mi.yourcompany.com/.well-known/apple-app-site-association`
- **assetlinks.json**: Digital Asset Links file at `https://mi.yourcompany.com/.well-known/assetlinks.json`
- **CNG**: Continuous Native Generation - Expo's method of generating native projects
- **EAS Build**: Expo Application Services - Cloud build service for Expo apps
- **Config Plugin**: Expo plugin that modifies native project files during prebuild

## Installation

Install the MovableInk SDK:

```bash
npx expo install @movable/react-native-sdk
```

Or:

```bash
npm install @movable/react-native-sdk
# or
yarn add @movable/react-native-sdk
```

Import in your code:

```typescript
import RNMovableInk from '@movable/react-native-sdk';
```

## Configuration for Deeplinking

### Step 1: Configure app.json

Your `app.json` file needs configuration for both iOS and Android platforms.

#### Complete app.json Example

```json
{
  "expo": {
    "name": "Your App",
    "slug": "your-app",
    "version": "1.0.0",
    "scheme": "yourapp",
    "ios": {
      "bundleIdentifier": "com.yourcompany.yourapp",
      "associatedDomains": [
        "applinks:mi.yourcompany.com"
      ],
      "infoPlist": {
        "movable_ink_universal_link_domains": [
          "mi.yourcompany.com"
        ],
        "movable_ink_api_key": "YOUR_API_KEY",
        "movable_ink_region": "us"
      }
    },
    "android": {
      "package": "com.yourcompany.yourapp",
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "mi.yourcompany.com",
              "pathPrefix": "/p/cpm"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        },
        {
          "action": "VIEW",
          "data": [
            {
              "scheme": "https",
              "host": "mi.yourcompany.com",
              "pathPrefix": "/p/rpm"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        },
        {
          "action": "VIEW",
          "data": [
            {
              "scheme": "https",
              "host": "mi.yourcompany.com",
              "pathPrefix": "/p/gom"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    },
    "plugins": [
      [
        "./plugins/mi_plugin",
        {
          "movable_ink_android_api_key": "YOUR_API_KEY",
          "movable_ink_android_region": "US"
        }
      ]
    ]
  }
}
```

**Important**: 
- Replace `mi.yourcompany.com` with your actual MovableInk subdomain
- Replace `YOUR_API_KEY` with your actual API key (or remove if only using deeplinking)
- `movable_ink_region` must be `"us"` or `"eu"`
- `movable_ink_android_region` must be `"US"` or `"EU"` (uppercase)

### Step 2: Create Expo Config Plugin for Android

Create a config plugin to handle Android-specific configuration:

#### File Structure

```
.
├── app/
├── plugins/
│   └── mi_plugin.js
├── app.json
└── package.json
```

#### plugins/mi_plugin.js

```javascript
const { withAndroidManifest, withAppBuildGradle } = require('@expo/config-plugins');

module.exports = function movableInkPlugin(config, data) {
  // Handle AndroidManifest.xml
  config = withAndroidManifest(config, async (config) => {
    let androidManifest = config.modResults.manifest;

    // Handle application-level modifications
    if (androidManifest.application && androidManifest.application[0]) {
      const application = androidManifest.application[0];

      // Initialize meta-data array if it doesn't exist
      if (!application['meta-data']) {
        application['meta-data'] = [];
      }

      // Handle Movable Ink API key
      if (data.movable_ink_android_api_key) {
        application['meta-data'].push({
          $: {
            'android:name': 'com.movableink.inked.API_KEY',
            'android:value': data.movable_ink_android_api_key,
          },
        });
      }
    }

    return config;
  });

  // Handle build.gradle
  config = withAppBuildGradle(config, (config) => {
    const { modResults } = config;

    if (data.movable_ink_android_region) {
      const region = data.movable_ink_android_region.toUpperCase();

      // Add buildConfigField to defaultConfig
      const buildConfigField = `buildConfigField("String", "MOVABLE_INK_SDK_REGION", "\\"${region}\\"")`;

      // Find defaultConfig block and add the buildConfigField
      if (modResults.contents.includes('defaultConfig {')) {
        modResults.contents = modResults.contents.replace(
          /defaultConfig\s*\{/,
          `defaultConfig {\n        ${buildConfigField}`
        );
      }
    }

    return config;
  });

  return config;
};
```

### Step 3: Run Prebuild (if using bare workflow)

```bash
npx expo prebuild
```

This generates the native `ios` and `android` folders with your configuration.

## Deeplinking Implementation

### TypeScript Implementation

```typescript
import React, { useEffect } from 'react';
import { Linking } from 'react-native';
import RNMovableInk from '@movable/react-native-sdk';
import { useNavigation } from '@react-navigation/native';

export default function App() {
  const navigation = useNavigation();

  useEffect(() => {
    // Start MovableInk SDK
    RNMovableInk.start();

    // Handle app launch from deeplink
    const handleInitialURL = async () => {
      const url = await Linking.getInitialURL();
      if (url) {
        resolveMovableInkLink(url);
      }
    };

    // Handle deeplinks while app is running
    const handleDeeplink = (event: { url: string }) => {
      if (event.url) {
        resolveMovableInkLink(event.url);
      }
    };

    const subscription = Linking.addEventListener('url', handleDeeplink);
    handleInitialURL();

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

  const resolveMovableInkLink = async (url: string) => {
    try {
      const resolvedURL = await RNMovableInk.resolveURL(url);

      if (resolvedURL) {
        console.log('Deeplink resolved to:', resolvedURL);
        handleDeeplink(resolvedURL);
      }
    } catch (error) {
      console.error('Failed to resolve link:', error);
    }
  };

  const handleDeeplink = (url: string) => {
    // Parse URL and navigate to appropriate screen
    if (url.includes('/products/')) {
      const productId = url.split('/products/')[1].split('/')[0];
      navigation.navigate('ProductDetail', { productId });
    } else if (url.includes('/categories/')) {
      const categoryId = url.split('/categories/')[1].split('/')[0];
      navigation.navigate('Category', { categoryId });
    }
  };

  return (
    <YourAppContent />
  );
}
```

### With Expo Router (app directory)

In `app/_layout.tsx`:

```typescript
import { useEffect } from 'react';
import { Linking } from 'react-native';
import RNMovableInk from '@movable/react-native-sdk';
import { useRouter } from 'expo-router';
import { Slot } from 'expo-router';

export default function RootLayout() {
  const router = useRouter();

  useEffect(() => {
    // Start MovableInk SDK
    RNMovableInk.start();

    const handleDeeplink = (event: { url: string }) => {
      if (event.url) {
        resolveMovableInkLink(event.url);
      }
    };

    const subscription = Linking.addEventListener('url', handleDeeplink);

    Linking.getInitialURL().then(url => {
      if (url) {
        resolveMovableInkLink(url);
      }
    });

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

  const resolveMovableInkLink = async (url: string) => {
    try {
      const resolvedURL = await RNMovableInk.resolveURL(url);

      if (resolvedURL) {
        console.log('Deeplink resolved to:', resolvedURL);

        // Parse and navigate
        if (resolvedURL.includes('/products/')) {
          const productId = resolvedURL.split('/products/')[1].split('/')[0];
          router.push(`/products/${productId}`);
        }
      }
    } catch (error) {
      console.error('Failed to resolve link:', error);
    }
  };

  return <Slot />;
}
```

## Behavior Events Implementation (Optional)

### iOS Setup (already in app.json)

The iOS API key is configured in `app.json`:

```json
{
  "expo": {
    "ios": {
      "infoPlist": {
        "movable_ink_api_key": "YOUR_API_KEY",
        "movable_ink_region": "us"
      }
    }
  }
}
```

### Android Setup (via config plugin)

The Android API key is configured via the plugin in `app.json`:

```json
{
  "expo": {
    "plugins": [
      [
        "./plugins/mi_plugin",
        {
          "movable_ink_android_api_key": "YOUR_API_KEY",
          "movable_ink_android_region": "US"
        }
      ]
    ]
  }
}
```

### Set User ID (MIU)

Set the MIU as soon as user is identified (typically after login):

```typescript
RNMovableInk.setMIU("USER_UNIQUE_ID");
```

**Important**: 
- MIU must be non-PII (use UUID, NOT email address)
- Must match what marketing team uses in email campaigns
- Should be URL-friendly string

### Identify User (Guest to Logged In)

If user makes actions as guest then logs in:

```typescript
RNMovableInk.setMIU("USER_UNIQUE_ID");
RNMovableInk.identifyUser();
```

### TypeScript Types

The SDK provides TypeScript types for all events:

```typescript
import { Currency, ProductProperties, ProductCategory } from '@movable/react-native-sdk';
```

### Product Searched

```typescript
RNMovableInk.productSearched({
  query: 'running shoes',
  url: 'https://yourapp.com/search?q=running+shoes',
  meta: {
    resultsCount: 42
  }
});
```

### Product Viewed

```typescript
import { Currency } from '@movable/react-native-sdk';

RNMovableInk.productViewed({
  id: 'PROD-12345',
  title: 'Running Shoes',
  price: '89.99',
  currency: Currency.USD,
  url: 'https://yourapp.com/products/12345',
  categories: [
    {
      id: 'shoes',
      url: 'https://yourapp.com/categories/shoes'
    },
    {
      id: 'running',
      url: 'https://yourapp.com/categories/running'
    }
  ],
  meta: {
    inStock: true,
    rating: 4.5
  }
});
```

**Important**: Product ID must not be empty string

### Product Added (to Cart)

```typescript
RNMovableInk.productAdded({
  id: 'PROD-12345',
  title: 'Running Shoes',
  price: '89.99',
  currency: Currency.USD,
  url: 'https://yourapp.com/products/12345',
  categories: [
    { id: 'shoes' },
    { id: 'running' }
  ],
  meta: {
    size: '10',
    color: 'blue'
  }
});
```

### Product Removed (from Cart)

```typescript
RNMovableInk.productRemoved({
  id: 'PROD-12345',
  title: 'Running Shoes',
  price: '89.99',
  currency: Currency.USD,
  url: 'https://yourapp.com/products/12345'
});
```

### Category Viewed

```typescript
RNMovableInk.categoryViewed({
  id: 'shoes',
  title: 'Shoes',
  url: 'https://yourapp.com/categories/shoes',
  meta: {
    itemCount: 156
  }
});
```

### Order Completed

```typescript
RNMovableInk.orderCompleted({
  id: 'ORDER-789',
  revenue: '179.98',
  currency: Currency.USD,
  products: [
    {
      id: 'PROD-12345',
      title: 'Running Shoes',
      price: '89.99',
      url: 'https://yourapp.com/products/12345',
      quantity: 2,
      meta: {
        size: '10',
        color: 'blue'
      }
    }
  ]
});
```

**Important**: 
- Order ID and Product IDs must not be empty strings
- Revenue and price should be in dollars and cents (e.g., "15.99")
- Can include up to 100 products per order

### Custom Events

```typescript
RNMovableInk.logEvent('video_started', {
  title: 'Cat Bloopers 1000',
  tags: ['comedy', 'cats'],
  total: 14
});
```

**Note**: Custom events must be configured with MovableInk team first (max 10 custom events).

### Currency Support

Available via `Currency` enum:
- `Currency.USD`, `Currency.EUR`, `Currency.GBP`, `Currency.CAD`, `Currency.AUD`, `Currency.JPY`, etc.
- Default is `USD` if not specified
- Introduced in SDK v2

## Building and Testing

### Build with EAS Build

Expo requires building native apps to test deeplinking:

```bash
# Install EAS CLI
npm install -g eas-cli

# Configure EAS
eas build:configure

# Build for iOS
eas build --platform ios --profile development

# Build for Android
eas build --platform android --profile development
```

### Build with expo prebuild (local builds)

```bash
# Generate native folders
npx expo prebuild

# Build iOS
cd ios && xcodebuild ...

# Build Android
cd android && ./gradlew assembleDebug
```

### Test Deeplinking

#### iOS Testing

1. Install built app on iOS device or simulator (iOS 13+)
2. Put app in background or close it
3. Open a MovableInk test link in Safari: `https://mi.yourcompany.com/p/rp/test`
4. App should open automatically
5. Check console for: `Deeplink resolved to: <final URL>`

#### Android Testing

1. Install built app on Android device or emulator (API 24+)
2. Put app in background or close it
3. Open a MovableInk test link in Chrome: `https://mi.yourcompany.com/p/rp/test`
4. App should open automatically
5. Check Logcat for: `Deeplink resolved to: <final URL>`

You can also test Android with ADB:

```bash
adb shell am start -a android.intent.action.VIEW \
  -d "https://mi.yourcompany.com/p/rp/test" \
  com.yourcompany.yourapp
```

### Test Behavior Events

#### iOS Testing

1. Set MIU to test value: `RNMovableInk.setMIU("00000000-0000-0000-0000-000000000000")`
2. Trigger an event (e.g., view a product)
3. Open Console app on Mac, select device, search for "MI SDK"
4. You should see success message with event name
5. Coordinate with MovableInk team to verify events are received

#### Android Testing

1. Set MIU to test value: `RNMovableInk.setMIU("00000000-0000-0000-0000-000000000000")`
2. Trigger an event (e.g., view a product)
3. Check Logcat for event logs (filter by "MovableInk" or "MI SDK")
4. You should see success message with event name
5. Coordinate with MovableInk team to verify events are received

## Common Issues and Solutions

### Deeplinks Not Working in Expo Go

**Issue**: Deeplinks don't open app in Expo Go

**Solution**: Deep linking with custom domains (Universal Links/App Links) does NOT work in Expo Go. You must:
- Build a development build with EAS Build: `eas build --profile development`
- Or use `expo prebuild` and build locally
- Expo Go only supports `exp://` scheme, not custom https domains

### Config Plugin Not Applied

**Issue**: Android configuration not being applied

**Solutions**:
- Ensure `plugins/mi_plugin.js` exists at project root
- Verify plugin is referenced correctly in `app.json`
- Run `npx expo prebuild --clean` to regenerate native folders
- Check that plugin path in `app.json` is `"./plugins/mi_plugin"` (no `.js` extension)

### Deeplinks Not Opening App (iOS)

**Issue**: Clicking MovableInk links doesn't open the iOS app

**Solutions**:
- Verify AASA file is accessible: `https://mi.yourcompany.com/.well-known/apple-app-site-association`
- Check `associatedDomains` in `app.json` matches exactly (no wildcards, no `https://`)
- Ensure testing on iOS 13+
- Try uninstalling and reinstalling app (clears AASA cache)
- Verify Bundle ID matches what's in AASA file
- Wait 24-48 hours after AASA changes for Apple CDN to update
- Check `movable_ink_universal_link_domains` matches `associatedDomains`

### Deeplinks Not Opening App (Android)

**Issue**: Clicking MovableInk links doesn't open the Android app

**Solutions**:
- Verify assetlinks.json is accessible: `https://mi.yourcompany.com/.well-known/assetlinks.json`
- Check package name and SHA-256 fingerprint match what's in assetlinks.json
- Ensure `autoVerify: true` is set in `app.json` intent filters
- Verify testing on Android 7.0 (API 24) or later
- Check intent filters match the URL pattern exactly (scheme, host, pathPrefix)
- Try clearing app data and reinstalling
- Wait 24-48 hours after assetlinks.json changes for Google to re-verify

### EAS Build Failing

**Issue**: Build fails when trying to build with EAS

**Solutions**:
- Ensure all dependencies are compatible with your Expo SDK version
- Check `eas.json` has correct build profiles
- Verify API keys are not accidentally committed (use `eas secret` for sensitive values)
- Run `eas build:configure` to regenerate configuration
- Check EAS Build logs for specific errors

### Events Not Sending

**Issue**: Behavior events not appearing in MovableInk platform

**Solutions**:
- **iOS**: Verify API key is set in `app.json` `infoPlist.movable_ink_api_key`
- **Android**: Verify API key is set in plugin configuration
- Ensure `RNMovableInk.start()` is called before sending events
- Ensure MIU is set before sending events
- Check console/Logcat for SDK logs showing success/failure
- Verify region is correct (us or eu)
- Confirm API key is for correct environment (staging/production)
- Run `npx expo prebuild --clean` after changing API keys

### TypeScript Type Errors

**Issue**: TypeScript complaining about missing types

**Solutions**:
- Ensure you're importing types from the SDK: `import { Currency } from '@movable/react-native-sdk'`
- Check that you're using the correct property names
- Verify SDK version supports TypeScript

## Important Notes

- **Replace placeholders**: Always replace `mi.yourcompany.com` with actual MovableInk subdomain
- **MIU requirements**: Must be non-PII, URL-friendly, match marketing team's identifier
- **API Key**: Required only for behavior events, NOT for deeplinking
- **Deeplinking works without API key**: Basic deeplinking functionality requires only Universal Links/App Links configuration
- **Region compliance**: Use `eu` region for European users if required for data compliance
- **Price format**: Always use string format for prices ("15.99", not 15.99)
- **Product IDs**: Must not be empty strings - will cause event to fail
- **Testing**: Use UUID `00000000-0000-0000-0000-000000000000` for testing with MovableInk team
- **Guest users**: Call `identifyUser()` after setting MIU for users who made actions while logged out
- **Expo Go limitation**: Deep linking with custom domains does NOT work in Expo Go - requires development build
- **Call start()**: Always call `RNMovableInk.start()` before using SDK features
- **TypeScript support**: SDK has full TypeScript support for better type safety
- **Prebuild required**: Must run `npx expo prebuild` or use EAS Build for native configuration to take effect
- **Plugin case sensitivity**: Android region in plugin should be uppercase ("US", "EU")

## Full Example App

Minimal working example with Expo Router:

```typescript
// app/_layout.tsx
import { useEffect } from 'react';
import { Linking } from 'react-native';
import RNMovableInk, { Currency } from '@movable/react-native-sdk';
import { Slot, useRouter } from 'expo-router';

export default function RootLayout() {
  const router = useRouter();

  useEffect(() => {
    // Start MovableInk SDK
    RNMovableInk.start();

    // Set MIU when user is identified
    const userId = getUserId(); // Your method to get user ID
    if (userId) {
      RNMovableInk.setMIU(userId);
    }

    // Handle deeplinks
    const handleDeeplink = (event: { url: string }) => {
      if (event.url) {
        resolveMovableInkLink(event.url);
      }
    };

    const subscription = Linking.addEventListener('url', handleDeeplink);

    Linking.getInitialURL().then(url => {
      if (url) {
        resolveMovableInkLink(url);
      }
    });

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

  const resolveMovableInkLink = async (url: string) => {
    try {
      const resolvedURL = await RNMovableInk.resolveURL(url);
      if (resolvedURL) {
        console.log('Opening:', resolvedURL);

        // Navigate based on resolved URL
        if (resolvedURL.includes('/products/')) {
          const productId = resolvedURL.split('/products/')[1].split('/')[0];
          router.push(`/products/${productId}`);
        } else if (resolvedURL.includes('/categories/')) {
          const categoryId = resolvedURL.split('/categories/')[1].split('/')[0];
          router.push(`/categories/${categoryId}`);
        }
      }
    } catch (error) {
      console.error('Failed to resolve link:', error);
    }
  };

  const getUserId = (): string | null => {
    // Your logic to get user ID
    return null;
  };

  return <Slot />;
}

// app/products/[id].tsx
import { useEffect } from 'react';
import { View, Text } from 'react-native';
import { useLocalSearchParams } from 'expo-router';
import RNMovableInk, { Currency } from '@movable/react-native-sdk';

export default function ProductDetailScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  const product = getProduct(id); // Your method to get product

  useEffect(() => {
    if (product) {
      // Capture product view
      RNMovableInk.productViewed({
        id: product.id,
        title: product.name,
        price: product.price,
        currency: Currency.USD,
        url: `https://yourapp.com/products/${product.id}`
      });
    }
  }, [product]);

  return (
    <View>
      <Text>{product?.name}</Text>
    </View>
  );
}

function getProduct(id: string) {
  // Your logic to fetch product
  return { id, name: 'Product', price: '0.00' };
}
```

## Push Notification Analytics (Optional)

### Overview

Track when users open MovableInk push notifications for campaign attribution and conversion measurement. This feature is particularly useful for DaVinci customers.

### Prerequisites

This assumes you have already set up push notifications in your Expo app using `expo-notifications`.

### Implementation with expo-notifications

Using the `expo-notifications` package:

```typescript
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';
import RNMovableInk from '@movable/react-native-sdk';

// Configure how notifications are handled when app is in foreground
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

// Helper function to extract payload from notification
function extractPayload(request: Notifications.NotificationRequest) {
  let rawData: Record<string, unknown> = {};

  // iOS: trigger.payload contains the full APNS payload
  if (request.trigger?.payload) {
    rawData = request.trigger.payload;
  }
  // Android: trigger.remoteMessage contains the FCM payload
  else if (request.trigger?.remoteMessage?.data) {
    rawData = request.trigger.remoteMessage.data;
  }

  return rawData;
}

function App() {
  useEffect(() => {
    // Handle notification click when app is in foreground or background
    const subscription = Notifications.addNotificationResponseReceivedListener(
      async (response) => {
        const payload = extractPayload(response.notification.request);
        RNMovableInk.handlePushNotificationOpened(payload);
      }
    );

    // Handle notification that opened the app from killed state
    Notifications.getLastNotificationResponseAsync().then((response) => {
      if (response) {
        const payload = extractPayload(response.notification.request);
        RNMovableInk.handlePushNotificationOpened(payload);
      }
    });

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

  return (
    <YourAppContent />
  );
}
```

### Manual Tracking Without SDK

Extract the tracking URL from the notification payload and make a request:

```typescript
import * as Notifications from 'expo-notifications';
import { useEffect } from 'react';

// Helper function to extract URL from notification content
function extractTrackingUrl(request: Notifications.NotificationRequest) {
  let rawData: Record<string, unknown> = {};

  // iOS: trigger.payload contains the full APNS payload
  if (request.trigger?.payload) {
    rawData = request.trigger.payload;
  }
  // Android: trigger.remoteMessage contains the FCM payload
  else if (request.trigger?.remoteMessage?.data) {
    rawData = request.trigger.remoteMessage.data;
  }

  if (rawData?.mi?.url) {
    return rawData.mi.url as string;
  } else if (rawData?.mi_url) {
    return rawData.mi_url as string;
  }

  return null;
}

function App() {
  useEffect(() => {
    // Handle notification click when app is in foreground or background
    const subscription = Notifications.addNotificationResponseReceivedListener(
      async (response) => {
        const url = extractTrackingUrl(response.notification.request);

        if (url) {
          try {
            await fetch(url);
          } catch (error) {
            // Handle error
          }
        }
      }
    );

    // Handle notification that opened the app from killed state
    Notifications.getLastNotificationResponseAsync().then((response) => {
      if (response) {
        const url = extractTrackingUrl(response.notification.request);

        if (url) {
          fetch(url).catch((error) => {
            // Handle error
          });
        }
      }
    });

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

  return (
    <YourAppContent />
  );
}
```

### Payload Formats

**iOS Payload (Standard):**
```json
{
  "aps": {
    "alert": {
      "title": "Special Offer Just for You!",
      "body": "Open to unlock exclusive savings."
    },
    "sound": "default"
  },
  "mi": {
    "url": "https://mi.example.com/p/push/abcd12345"
  }
}
```

**iOS Payload (DaVinci):**
```json
{
  "aps": {
    "alert": {
      "title": "Special Offer Just for You!",
      "body": "Open to unlock exclusive savings."
    },
    "sound": "default"
  },
  "data": {
    "mi_url": "https://mi.example.com/deeplink/abc123",
    "mi_source": "davinci"
  }
}
```

**Android Payload:**
```json
{
  "notification": {
    "title": "Special Offer Just for You!",
    "body": "Open to unlock exclusive savings."
  },
  "data": {
    "mi_url": "https://mi.example.com/deeplink/abc123",
    "mi_source": "davinci"
  }
}
```

## Additional Resources

- Full Documentation: https://sdk-mobile.movableink.com/
- Expo Documentation: https://docs.expo.dev
- npm Package: https://www.npmjs.com/package/@movable/react-native-sdk
- Expo Config Plugins: https://docs.expo.dev/guides/config-plugins/
- EAS Build: https://docs.expo.dev/build/introduction/
- iOS Native SDK: https://github.com/movableink/ios-sdk
- Android Native SDK: https://github.com/movableink/android-sdk
- Contact MovableInk team for: API keys, AASA/assetlinks.json configuration, MIU strategy, testing support