Flutter
AI Implementation Guide
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 Flutter app using the context I provided"
- "Help me configure Universal Links and App Links for MovableInk in Flutter"
- "Implement behavior events to capture when users view products"
- "Add orderCompleted event tracking to my checkout flow"
- "Help me test my MovableInk integration"
- "Debug why my MovableInk deeplinks aren't working in Flutter"
- "Set up both iOS and Android deeplinking for Flutter"
Context for AI Assistant
Current SDK Version: 3.0.0
Copy everything inside the code block below and paste into your AI assistant:
# MovableInk Flutter 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 Flutter 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
- **Minimum Flutter**: 2.5.0
- **Minimum iOS**: 13.0
- **Minimum Android**: Android 7.0 (API 24)
- **Language**: Dart
- **Package**: `movable_ink_plugin`
## 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
Add the MovableInk plugin to your `pubspec.yaml`:
```yaml
dependencies:
movable_ink_plugin: ^VERSION
```
Replace `VERSION` with the current version, then run:
```bash
flutter pub get
```
Import in your Dart code:
```dart
import 'package:movable_ink_plugin/movable_ink_plugin.dart';
```
## Configuration for Deeplinking
### iOS Configuration
#### Step 1: Add Associated Domains in Xcode
1. Open your Flutter project in Xcode: `File > Open > ios/Runner.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/Runner/Info.plist`:
```xml
<key>movable_ink_universal_link_domains</key>
<array>
<string>mi.yourcompany.com</string>
</array>
```
**For Flutter 3.27.0+**: Also disable Flutter's built-in deep linking:
```xml
<key>FlutterDeepLinkingEnabled</key>
<false/>
```
**Important**: Replace `mi.yourcompany.com` with actual MovableInk subdomain
### 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>
```
**For Flutter 3.27.0+**: Also disable Flutter's built-in deep linking:
```xml
<activity android:name=".MainActivity">
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
</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
### Basic Implementation with Stream Subscription
```dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:movable_ink_plugin/movable_ink_plugin.dart';
class MyApp extends StatefulWidget
class _MyAppState extends State<MyApp>
void _initDeepLinking() ,
onError: (error) ,
);
}
void _handleDeeplink(String url) else if (uri.path.contains('/categories/'))
}
@override
void dispose()
@override
Widget build(BuildContext context)
}
```
### Alternative: Using subscribeToResolvedURL
```dart
void _initDeepLinking()
});
}
```
### Manual URL Resolution
You can also manually resolve URLs:
```dart
Future<void> resolveUrl(String url) async
}
```
### Get Last Resolved URL
If you need to retrieve the last resolved URL (e.g., after login):
```dart
Future<void> getLastResolvedUrl() async
}
```
**Note**: It's safer to use the stream subscription instead of polling for the last URL.
## Behavior Events Implementation (Optional)
### iOS Setup
#### Step 1: Add API Key to Info.plist
Add to `ios/Runner/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="$" />
</application>
```
#### Step 3: Update build.gradle
In `android/app/build.gradle`:
```groovy
android
}
```
#### Step 4: Configure Region (Optional)
For EU compliance:
```groovy
android
}
```
### Set User ID (MIU)
Set the MIU as soon as user is identified (typically after login):
```dart
_movableInkPlugin.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:
```dart
_movableInkPlugin.setMIU("USER_UNIQUE_ID");
_movableInkPlugin.identifyUser();
```
### Product Searched
```dart
final `Map<String, dynamic>` properties =
};
_movableInkPlugin.productSearched(properties);
```
### Product Viewed
```dart
final `Map<String, dynamic>` properties = ,
],
"meta":
};
_movableInkPlugin.productViewed(properties);
```
**Important**: Product ID must not be empty string
### Using Currency Enum
```dart
import 'package:movable_ink_plugin/currency.dart';
final `Map<String, dynamic>` properties = ;
_movableInkPlugin.productViewed(properties);
```
### Product Added (to Cart)
```dart
final `Map<String, dynamic>` properties = ,
],
"meta":
};
_movableInkPlugin.productAdded(properties);
```
### Product Removed (from Cart)
```dart
final `Map<String, dynamic>` properties = ;
_movableInkPlugin.productRemoved(properties);
```
### Category Viewed
```dart
final `Map<String, dynamic>` properties =
};
_movableInkPlugin.categoryViewed(properties);
```
### Order Completed
```dart
final `Map<String, dynamic>` properties =
}
]
};
_movableInkPlugin.orderCompleted(properties);
```
**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
```dart
final `Map<String, dynamic>` properties = ;
_movableInkPlugin.logEvent("video_started", properties);
```
**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 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 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: `_movableInkPlugin.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: `_movableInkPlugin.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
### 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
### Flutter 3.27.0+ Deep Linking Conflict
**Issue**: Deep links not working after upgrading to Flutter 3.27.0+
**Solution**: Disable Flutter's built-in deep linking:
iOS (`Info.plist`):
```xml
<key>FlutterDeepLinkingEnabled</key>
<false/>
```
Android (`AndroidManifest.xml`):
```xml
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
```
### 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)
### Stream Not Receiving Links
**Issue**: Stream subscription not being called when deeplink is tapped
**Solutions**:
- Ensure `_movableInkPlugin.start()` is called in `initState()`
- Verify stream subscription is active (not disposed)
- Check that native configuration (iOS/Android) is correct
- Try both `start()` and `subscribeToResolvedURL()` methods
### 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
- **Flutter 3.27.0+**: Must disable Flutter's built-in deep linking on both platforms
- **Stream management**: Always cancel stream subscriptions in `dispose()`
## Full Example App
Minimal working example:
```dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:movable_ink_plugin/movable_ink_plugin.dart';
import 'package:movable_ink_plugin/currency.dart';
void main()
class MyApp extends StatefulWidget
class _MyAppState extends State<MyApp>
void _initDeepLinking() ,
onError: (error) ,
);
}
void _setUserIfLoggedIn()
}
void _handleDeeplink(String url) else if (uri.path.contains('/categories/'))
}
@override
void dispose()
@override
Widget build(BuildContext context) ,
);
}
String? getUserId()
}
class ProductDetailPage extends StatelessWidget );
return Scaffold(
appBar: AppBar(title: Text('Product Detail')),
body: Center(child: Text('Product: $productId')),
);
}
}
class CategoryPage extends StatelessWidget
}
class HomePage extends StatelessWidget
}
```
## Push Notifications (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 Flutter app using a package like `push` or `firebase_messaging`.
### Implementation with Push Package
Using the `push` package to handle notifications:
```dart
import 'package:movable_ink_plugin/movable_ink_plugin.dart';
import 'package:push/push.dart';
class _MyAppState extends State<MyApp>
@override
void dispose()
void initPush() async );
// Handle notification that launched app from terminated state
Push.instance.notificationTapWhichLaunchedAppFromTerminated.then((data)
});
}
}
```
### Manual Tracking Without SDK
Extract the tracking URL from the notification payload and make a request:
```dart
import 'package:http/http.dart' as http;
void handleMIPush(`Map<String?, Object?>` data) async
}
// Check for flattened mi_url key
else if (data['mi_url'] is String)
if (url != null && url.isNotEmpty)
}
```
### Payload Formats
**iOS Payload (Standard):**
```json
,
"sound": "default"
},
"mi":
}
```
**iOS Payload (DaVinci):**
```json
,
"sound": "default"
},
"data":
}
```
**Android Payload:**
```json
,
"data":
}
```
## Additional Resources
- Full Documentation: https://sdk-mobile.movableink.com/
- pub.dev Package: https://pub.dev/packages/movable_ink_plugin
- 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