AI Implementation Guide - React Native
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 React Native app using the context I provided"
- "Help me configure Universal Links and App Links for MovableInk in React Native"
- "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"
- "Debug why my MovableInk deeplinks aren't working in React Native"
- "Set up both iOS and Android deeplinking for React Native"
- "Convert this JavaScript implementation to TypeScript"
Context for AI Assistant
Copy everything inside the code block below and paste into your AI assistant:
# MovableInk React Native 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 React Native SDK 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 React Native**: 0.79
- **Minimum iOS**: 13.0
- **Minimum Android**: Android 7.0 (API 24)
- **Language**: TypeScript/JavaScript
- **Package**: `@movable/react-native-sdk`
## 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`
## Installation
Install the MovableInk SDK:
```bash
npm install @movable/react-native-sdk
# or
yarn add @movable/react-native-sdk
```
Then install iOS dependencies:
```bash
cd ios && pod install && cd ..
```
Import in your code:
```typescript
import RNMovableInk from '@movable/react-native-sdk';
```
## Configuration for Deeplinking
### iOS Configuration
#### Step 1: Add Associated Domains in Xcode
1. Open your React Native project in Xcode: `ios/YourApp.xcworkspace`
2. Select your app target in Xcode
3. Go to **Signing & Capabilities** tab
4. Click **+ Capability** → Add **Associated Domains**
5. Add entry: `applinks:mi.yourcompany.com`
**Important**: Replace `mi.yourcompany.com` with actual MovableInk subdomain (no `https://`)
#### Step 2: Update Info.plist
Add to `ios/YourApp/Info.plist`:
```xml
<key>movable_ink_universal_link_domains</key>
<array>
<string>mi.yourcompany.com</string>
</array>
```
**Important**: Replace `mi.yourcompany.com` with actual MovableInk subdomain
#### Step 3: Update AppDelegate.mm
Add RCTLinkingManager to handle Universal Links in `ios/YourApp/AppDelegate.mm`:
```objc
#import <React/RCTLinkingManager.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [RCTLinkingManager application:app openURL:url options:options];
}
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
@end
```
### Android Configuration
#### Step 1: Add Intent Filters to AndroidManifest.xml
Add these intent filters to your main activity in `android/app/src/main/AndroidManifest.xml`:
```xml
<activity
android:name=".MainActivity"
android:exported="true">
<!-- Existing intent filters -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Add these intent filters for MovableInk App Links -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="mi.yourcompany.com"
android:pathPrefix="/p/cpm" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="mi.yourcompany.com"
android:pathPrefix="/p/rpm" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="mi.yourcompany.com"
android:pathPrefix="/p/gom" />
</intent-filter>
</activity>
```
**Important**:
- Replace `mi.yourcompany.com` with actual MovableInk subdomain
- `android:autoVerify="true"` is required for App Links verification
- `android:exported="true"` is required for Android 12+
## Deeplinking Implementation
### TypeScript Implementation with Hooks
```typescript
import React, { useEffect, useState } from 'react';
import { Linking } from 'react-native';
import RNMovableInk from '@movable/react-native-sdk';
import { useNavigation } from '@react-navigation/native';
function App() {
const navigation = useNavigation();
const [initialURL, setInitialURL] = useState<string | null>(null);
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 (
// Your app content
<YourAppContent />
);
}
export default App;
```
### JavaScript Implementation
```javascript
import React, { useEffect } from 'react';
import { Linking } from 'react-native';
import RNMovableInk from '@movable/react-native-sdk';
function App() {
useEffect(() => {
RNMovableInk.start();
const handleDeeplink = (event) => {
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 = (url) => {
RNMovableInk.resolveURL(url)
.then((resolvedURL) => {
if (resolvedURL) {
console.log('Deeplink resolved to:', resolvedURL);
navigateToContent(resolvedURL);
}
})
.catch((error) => {
console.error('Failed to resolve link:', error);
});
};
const navigateToContent = (url) => {
// Parse URL and navigate
if (url.includes('/products/')) {
// Navigate to product detail
} else if (url.includes('/categories/')) {
// Navigate to category
}
};
return (
<YourAppContent />
);
}
export default App;
```
## Behavior Events Implementation (Optional)
### iOS Setup
#### Step 1: Add API Key to Info.plist
Add to `ios/YourApp/Info.plist`:
```xml
<key>movable_ink_api_key</key>
<string>YOUR_API_KEY</string>
<key>movable_ink_region</key>
<string>us</string>
```
**Regions**: `us` or `eu` (default is `us` if not specified)
### Android Setup
#### Step 1: Add API Key to local.properties
In `android/local.properties`:
```properties
MOVABLE_INK_SDK_API_KEY=YOUR_API_KEY
```
#### Step 2: Update AndroidManifest.xml
Add to `android/app/src/main/AndroidManifest.xml` within `<application>`:
```xml
<application>
<meta-data
android:name="com.movableink.inked.API_KEY"
android:value="${MOVABLE_INK_SDK_API_KEY}" />
</application>
```
#### Step 3: Update build.gradle
In `android/app/build.gradle`:
```groovy
android {
defaultConfig {
manifestPlaceholders["MOVABLE_INK_SDK_API_KEY"] =
project.properties['MOVABLE_INK_SDK_API_KEY'] ?: ""
}
}
```
#### Step 4: Configure Region (Optional)
For EU compliance:
```groovy
android {
defaultConfig {
buildConfigField("String", "MOVABLE_INK_SDK_REGION", "\"EU\"")
}
}
```
### 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
## Testing
### Test Deeplinking
#### iOS Testing
1. Build and run 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 Metro console or Xcode console for: `Deeplink resolved to: <final URL>`
#### Android Testing
1. Build and run 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 Metro console or 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 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 Associated Domains match 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` in Info.plist matches Associated Domains
- Verify `RCTLinkingManager` methods are implemented in AppDelegate.mm
### 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 `android:autoVerify="true"` is set on at least one intent filter
- 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
### Linking Module Not Working
**Issue**: `Linking.addEventListener` not receiving events
**Solutions**:
- Ensure `RNMovableInk.start()` is called before setting up listeners
- Verify AppDelegate.mm has RCTLinkingManager methods (iOS)
- Check that intent filters are correctly configured (Android)
- Make sure to call `Linking.getInitialURL()` for app launch deeplinks
### Events Not Sending
**Issue**: Behavior events not appearing in MovableInk platform
**Solutions**:
- **iOS**: Verify API key is set in Info.plist (`movable_ink_api_key`)
- **Android**: Verify API key is set in local.properties and AndroidManifest.xml
- 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)
### TypeScript Type Errors
**Issue**: TypeScript complaining about missing types
**Solutions**:
- Ensure you're importing types from the SDK: `import { Currency, ProductProperties } from '@movable/react-native-sdk'`
- Check that you're using the correct property names (e.g., `currency` not `currencyCode`)
- Verify SDK version supports TypeScript (0.71+ recommended)
### MIU Mismatch
**Issue**: Events not associating with correct users in campaigns
**Solutions**:
- Verify MIU matches exactly what marketing team uses in ESP
- Ensure MIU is set after user logs in
- MIU should be UUID format, not email address
- Call `identifyUser()` after setting MIU for guest users who log in
## 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
- **Platform-specific config**: Must configure both iOS and Android native projects separately
- **Call start()**: Always call `RNMovableInk.start()` before using SDK features
- **TypeScript support**: SDK has full TypeScript support for better type safety
## Full Example App
Minimal working example with React Navigation:
```typescript
import React, { useEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Linking } from 'react-native';
import RNMovableInk, { Currency } from '@movable/react-native-sdk';
const Stack = createNativeStackNavigator();
function App() {
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
// You would use navigation.navigate() here
}
} catch (error) {
console.error('Failed to resolve link:', error);
}
};
const getUserId = (): string | null => {
// Your logic to get user ID
return null;
};
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="ProductDetail" component={ProductDetailScreen} />
<Stack.Screen name="Category" component={CategoryScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
interface Product {
id: string;
name: string;
price: string;
}
function ProductDetailScreen({ route }: any) {
const { productId } = route.params;
const product: Product = getProduct(productId); // Your method to get product
useEffect(() => {
// 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 (
// Your product detail UI
<View>
<Text>{product.name}</Text>
</View>
);
}
function CategoryScreen({ route }: any) {
return (
<View>
<Text>Category Screen</Text>
</View>
);
}
function HomeScreen() {
return (
<View>
<Text>Home Screen</Text>
</View>
);
}
function getProduct(id: string): Product {
// Your logic to fetch product
return { id, name: 'Product', price: '0.00' };
}
export default App;
```
## 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 React Native app using a package like `react-native-notifications` or `@react-native-firebase/messaging`.
### Implementation with react-native-notifications
Using the `react-native-notifications` package:
```typescript
import React, { useEffect } from 'react';
import { Notifications, type Registered } from 'react-native-notifications';
import RNMovableInk from '@movable/react-native-sdk';
export default function App() {
useEffect(() => {
// Register for remote notifications
Notifications.registerRemoteNotifications();
// Handle device token updates
const registerForNotificationsSubscription =
Notifications.events().registerRemoteNotificationsRegistered(
(event: Registered) => {
console.log(event.deviceToken);
}
);
// Handle notification taps
const openedSubscription =
Notifications.events().registerNotificationOpened(
(notification, completion) => {
RNMovableInk.handlePushNotificationOpened(notification.payload);
completion();
}
);
return () => {
registerForNotificationsSubscription.remove();
openedSubscription?.remove();
};
}, []);
return (
<YourAppContent />
);
}
```
### Manual Tracking Without SDK
Extract the tracking URL from the notification payload and make a request:
```typescript
const openedSubscription =
Notifications.events().registerNotificationOpened(
async (notification, completion) => {
let url;
if (notification.payload.mi?.url) {
url = notification.payload.mi.url;
}
else if (notification.payload.data?.mi_url) {
url = notification.payload.data?.mi_url;
}
if (url) {
try {
await fetch(url);
} catch (error) {
// Handle error
}
}
completion();
}
);
```
### 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/
- npm Package: https://www.npmjs.com/package/@movable/react-native-sdk
- React Native Linking Docs: https://reactnative.dev/docs/linking
- 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