From 7339d374eb0e8b45cc4cf6fa534feb2ba72eb7bb Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Tue, 2 Dec 2025 11:57:09 -0500 Subject: [PATCH] feat: add Expo config plugin for mParticle integration and create Expo test app - Introduced a new Expo config plugin to facilitate mParticle SDK integration for both iOS and Android. - Created an Expo test app to demonstrate the usage of the mParticle SDK, including event logging and Rokt placements. - Updated onboarding documentation to include instructions for using the new plugin and test app. --- ExpoTestApp/.gitignore | 27 ++ ExpoTestApp/App.tsx | 519 +++++++++++++++++++++++++++ ExpoTestApp/README.md | 239 ++++++++++++ ExpoTestApp/app.json | 46 +++ ExpoTestApp/assets/adaptive-icon.png | Bin 0 -> 17547 bytes ExpoTestApp/assets/favicon.png | Bin 0 -> 1219 bytes ExpoTestApp/assets/icon.png | Bin 0 -> 22380 bytes ExpoTestApp/assets/splash-icon.png | Bin 0 -> 17547 bytes ExpoTestApp/index.ts | 4 + ExpoTestApp/metro.config.js | 15 + ExpoTestApp/package.json | 26 ++ ExpoTestApp/tsconfig.json | 13 + ONBOARDING.md | 135 ++++++- README.md | 346 +++++++++++++----- app.plugin.js | 1 + js/rokt/rokt-layout-view.android.tsx | 3 +- package.json | 25 +- plugin/src/withMParticle.ts | 106 ++++++ plugin/src/withMParticleAndroid.ts | 359 ++++++++++++++++++ plugin/src/withMParticleIOS.ts | 459 +++++++++++++++++++++++ plugin/tsconfig.json | 8 + 21 files changed, 2228 insertions(+), 103 deletions(-) create mode 100644 ExpoTestApp/.gitignore create mode 100644 ExpoTestApp/App.tsx create mode 100644 ExpoTestApp/README.md create mode 100644 ExpoTestApp/app.json create mode 100644 ExpoTestApp/assets/adaptive-icon.png create mode 100644 ExpoTestApp/assets/favicon.png create mode 100644 ExpoTestApp/assets/icon.png create mode 100644 ExpoTestApp/assets/splash-icon.png create mode 100644 ExpoTestApp/index.ts create mode 100644 ExpoTestApp/metro.config.js create mode 100644 ExpoTestApp/package.json create mode 100644 ExpoTestApp/tsconfig.json create mode 100644 app.plugin.js create mode 100644 plugin/src/withMParticle.ts create mode 100644 plugin/src/withMParticleAndroid.ts create mode 100644 plugin/src/withMParticleIOS.ts create mode 100644 plugin/tsconfig.json diff --git a/ExpoTestApp/.gitignore b/ExpoTestApp/.gitignore new file mode 100644 index 0000000..9d941b6 --- /dev/null +++ b/ExpoTestApp/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ + +# Native builds +ios/ +android/ + +# Debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# Local env files +.env*.local + +# TypeScript +*.tsbuildinfo + diff --git a/ExpoTestApp/App.tsx b/ExpoTestApp/App.tsx new file mode 100644 index 0000000..8c78abe --- /dev/null +++ b/ExpoTestApp/App.tsx @@ -0,0 +1,519 @@ +/** + * Expo Test App for mParticle React Native SDK + * + * This app tests the Expo config plugin integration for the mParticle SDK. + * It demonstrates SDK initialization, event logging, and Rokt placements. + */ + +import React, { useState, useRef, useEffect } from 'react'; +import { + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, + Platform, + findNodeHandle, + NativeEventEmitter, +} from 'react-native'; +import MParticle from 'react-native-mparticle'; + +const { RoktLayoutView, RoktEventManager } = MParticle; + +// Create event emitter for Rokt events +const eventManagerEmitter = new NativeEventEmitter(RoktEventManager); + +export default function App() { + const [eventName, setEventName] = useState('Test Event'); + const [status, setStatus] = useState('SDK initialized via native code'); + const [logs, setLogs] = useState([]); + + // Ref for Rokt embedded placeholder + const roktPlaceholderRef = useRef(null); + + const addLog = (message: string) => { + const timestamp = new Date().toLocaleTimeString(); + setLogs(prev => [`[${timestamp}] ${message}`, ...prev].slice(0, 20)); + }; + + // Set up Rokt event listeners + useEffect(() => { + if (!eventManagerEmitter) { + console.warn('RoktEventManager not available, skipping event listeners'); + return; + } + + const roktCallbackListener = eventManagerEmitter.addListener( + 'RoktCallback', + (data: any) => { + console.log('roktCallback received:', data.callbackValue); + addLog(`Rokt callback: ${data.callbackValue}`); + } + ); + + const roktEventsListener = eventManagerEmitter.addListener( + 'RoktEvents', + (data: any) => { + console.log('Rokt event:', JSON.stringify(data)); + addLog(`Rokt event: ${data.event || JSON.stringify(data)}`); + } + ); + + return () => { + roktCallbackListener.remove(); + roktEventsListener.remove(); + }; + }, []); + + const handleLogEvent = () => { + if (!eventName.trim()) { + setStatus('Error: Event name is required'); + return; + } + + MParticle.logEvent(eventName, MParticle.EventType.Other, { + source: 'ExpoTestApp', + timestamp: new Date().toISOString(), + }); + addLog(`Logged event: ${eventName}`); + setStatus(`Event logged: ${eventName}`); + }; + + const handleLogScreenEvent = () => { + MParticle.logScreenEvent('Expo Test Screen', { + screen_name: 'Main', + source: 'ExpoTestApp', + }); + addLog('Logged screen event: Expo Test Screen'); + setStatus('Screen event logged'); + }; + + const handleLogCommerceEvent = () => { + const product = new MParticle.Product('Test Product', 'SKU-12345', 29.99); + const transactionAttributes = new MParticle.TransactionAttributes( + 'TRANS-001' + ); + const event = MParticle.CommerceEvent.createProductActionEvent( + MParticle.ProductActionType.AddToCart, + [product], + transactionAttributes + ); + + MParticle.logCommerceEvent(event); + addLog('Logged commerce event: AddToCart'); + setStatus('Commerce event logged'); + }; + + const handleSetUserAttribute = () => { + MParticle.Identity.getCurrentUser((currentUser: any) => { + if (currentUser) { + currentUser.setUserAttribute('test_attribute', 'expo_test_value'); + addLog('Set user attribute: test_attribute = expo_test_value'); + setStatus('User attribute set'); + } else { + addLog('Error: No current user found'); + setStatus('Error: No current user'); + } + }); + }; + + const handleIdentify = () => { + const request = new MParticle.IdentityRequest(); + request.email = 'expo-test@example.com'; + request.customerId = 'expo-test-123'; + + MParticle.Identity.identify( + request, + (error: any, userId: string | null) => { + if (error) { + addLog(`Identify error: ${JSON.stringify(error)}`); + setStatus('Identify failed'); + } else { + addLog(`Identify success: userId = ${userId}`); + setStatus(`Identified: ${userId}`); + } + } + ); + }; + + const handleGetCurrentUser = () => { + MParticle.Identity.getCurrentUser((currentUser: any) => { + if (currentUser) { + addLog( + `Current user ID: ${currentUser.userID || currentUser.getMpid?.()}` + ); + setStatus(`Current user retrieved`); + } else { + addLog('No current user found'); + setStatus('No current user'); + } + }); + }; + + const handleCheckOptOut = () => { + MParticle.getOptOut((optedOut: boolean) => { + addLog(`Opt-out status: ${optedOut ? 'Opted out' : 'Opted in'}`); + setStatus(`Opt-out: ${optedOut}`); + }); + }; + + // Rokt placement functions + const handleRoktSelectPlacements = (identifier: string) => { + // Platform-specific attributes + const iosAttributes = { + email: 'ios-expo-user@example.com', + platform: 'ios', + userId: 'ios-expo-54321', + deviceType: 'mobile', + }; + + const androidAttributes = { + email: 'android-expo-user@example.com', + platform: 'android', + userId: 'android-expo-67890', + deviceType: 'mobile', + }; + + // Select attributes based on platform + const attributes = + Platform.OS === 'ios' ? iosAttributes : androidAttributes; + + addLog(`Rokt: Using ${Platform.OS} attributes for ${identifier}`); + + // Create Rokt config + const cacheConfig = MParticle.Rokt.createCacheConfig(30, attributes); + const config = MParticle.Rokt.createRoktConfig('system', cacheConfig); + + // Build placeholder map for embedded placements + const placeholderMap: { [key: string]: number | null } = {}; + const nodeHandle = findNodeHandle(roktPlaceholderRef.current); + if (nodeHandle !== null) { + placeholderMap['Location1'] = nodeHandle; + } + + // Call selectPlacements + MParticle.Rokt.selectPlacements( + identifier, + attributes, + placeholderMap, + config, + undefined + ) + .then((result: any) => { + addLog(`Rokt selectPlacements success: ${JSON.stringify(result)}`); + setStatus(`Rokt: ${identifier} loaded`); + }) + .catch((error: any) => { + addLog(`Rokt selectPlacements error: ${JSON.stringify(error)}`); + setStatus(`Rokt error: ${error.message || 'Unknown error'}`); + }); + }; + + const handleRoktEmbedded = () => + handleRoktSelectPlacements('MSDKEmbeddedLayout'); + const handleRoktOverlay = () => + handleRoktSelectPlacements('MSDKOverlayLayout'); + const handleRoktBottomSheet = () => + handleRoktSelectPlacements('MSDKBottomSheetLayout'); + + return ( + + + + mParticle Expo Test + + Testing Expo Config Plugin Integration + + + + Status: + {status} + + + + Event Name: + + + + + + Log Event + + + + Log Screen + + + + Log Commerce + + + + Identify + + + + Get User + + + + Set Attribute + + + + Check Opt-Out + + + + {/* Rokt Section */} + + Rokt Placements + + Test Rokt SDK integration via mParticle kit + + + + + Embedded + + + + Overlay + + + + Bottom Sheet + + + + {/* Rokt Embedded Placeholder */} + + + Embedded Placement Area: + + + + + + + Activity Log: + {logs.length === 0 ? ( + No activity yet + ) : ( + logs.map((log, index) => ( + + {log} + + )) + )} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8f9fa', + }, + scrollContent: { + padding: 20, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#04a0c1', + marginBottom: 8, + textAlign: 'center', + }, + subtitle: { + fontSize: 14, + color: '#666', + marginBottom: 24, + textAlign: 'center', + }, + statusContainer: { + backgroundColor: '#fff', + padding: 16, + borderRadius: 12, + marginBottom: 20, + flexDirection: 'row', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + statusLabel: { + fontWeight: 'bold', + marginRight: 8, + color: '#333', + }, + statusText: { + flex: 1, + color: '#04a0c1', + }, + inputContainer: { + marginBottom: 20, + }, + label: { + fontSize: 14, + fontWeight: '600', + color: '#333', + marginBottom: 6, + }, + input: { + backgroundColor: '#fff', + borderWidth: 1, + borderColor: '#ddd', + borderRadius: 8, + padding: 12, + fontSize: 16, + }, + buttonGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + marginBottom: 24, + }, + button: { + width: '48%', + padding: 14, + borderRadius: 8, + alignItems: 'center', + marginBottom: 10, + }, + primaryButton: { + backgroundColor: '#04a0c1', + }, + secondaryButton: { + backgroundColor: '#28a745', + }, + tertiaryButton: { + backgroundColor: '#6f42c1', + }, + infoButton: { + backgroundColor: '#17a2b8', + width: '100%', + }, + buttonText: { + color: '#fff', + fontSize: 14, + fontWeight: 'bold', + }, + logsContainer: { + backgroundColor: '#fff', + padding: 16, + borderRadius: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + logsTitle: { + fontSize: 16, + fontWeight: 'bold', + color: '#333', + marginBottom: 12, + }, + logEmpty: { + color: '#999', + fontStyle: 'italic', + }, + logEntry: { + fontSize: 12, + color: '#555', + paddingVertical: 4, + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + sectionContainer: { + backgroundColor: '#fff', + padding: 16, + borderRadius: 12, + marginBottom: 20, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + sectionTitle: { + fontSize: 18, + fontWeight: 'bold', + color: '#e63946', + marginBottom: 4, + }, + sectionSubtitle: { + fontSize: 12, + color: '#666', + marginBottom: 16, + }, + roktButton: { + backgroundColor: '#e63946', + }, + roktButtonAlt: { + backgroundColor: '#d62839', + width: '100%', + }, + roktPlaceholderContainer: { + marginTop: 16, + padding: 12, + backgroundColor: '#f8f9fa', + borderRadius: 8, + borderWidth: 1, + borderColor: '#dee2e6', + borderStyle: 'dashed', + minHeight: 100, + }, + placeholderLabel: { + fontSize: 12, + color: '#6c757d', + marginBottom: 8, + }, +}); diff --git a/ExpoTestApp/README.md b/ExpoTestApp/README.md new file mode 100644 index 0000000..0b0e082 --- /dev/null +++ b/ExpoTestApp/README.md @@ -0,0 +1,239 @@ +# mParticle Expo Test App + +This app tests the Expo config plugin integration for the mParticle React Native SDK. + +## Setup + +1. First, build and pack the main library: + + ```bash + cd .. + yarn install + yarn dev:pack + ``` + +2. Install dependencies in the test app: + + ```bash + cd ExpoTestApp + npm install + ``` + +3. Update the API keys in `app.json`: + + ```json + { + "expo": { + "plugins": [ + [ + "react-native-mparticle", + { + "iosApiKey": "YOUR_IOS_API_KEY", + "iosApiSecret": "YOUR_IOS_API_SECRET", + "androidApiKey": "YOUR_ANDROID_API_KEY", + "androidApiSecret": "YOUR_ANDROID_API_SECRET", + "logLevel": "verbose", + "environment": "development", + "iosKits": ["mParticle-Rokt"], + "androidKits": ["android-rokt-kit"] + } + ] + ] + } + } + ``` + +## Running the App + +### iOS + +```bash +npm run prebuild +npm run ios +``` + +### Android + +```bash +npm run prebuild +npm run android +``` + +## Testing + +The app provides buttons to test various mParticle SDK functionality: + +### Core mParticle Features + +- **Log Event**: Logs a custom event with the specified name +- **Log Screen**: Logs a screen view event +- **Log Commerce**: Logs a commerce event (add to cart) +- **Identify**: Identifies a test user +- **Get User**: Gets the current user +- **Set Attribute**: Sets a user attribute +- **Check Opt-Out**: Checks the current opt-out status + +### Rokt Placements + +The app also includes Rokt placement testing via the mParticle Rokt kit: + +- **Embedded**: Loads an embedded Rokt placement that renders in-line within the app content. The placement appears in the designated placeholder area below the buttons. +- **Overlay**: Loads a full-screen overlay Rokt placement that appears on top of the app content. +- **Bottom Sheet**: Loads a bottom sheet Rokt placement that slides up from the bottom of the screen. + +The Rokt section also demonstrates: + +- Platform-specific attributes (iOS vs Android configurations) +- Rokt event listeners for callbacks and placement events +- Using `RoktLayoutView` as an embedded placeholder component + +All activity is logged in the Activity Log section at the bottom of the screen. + +## Verifying Plugin Integration + +After running `npm run prebuild`, you can verify the plugin worked correctly: + +### Verify iOS Integration + +#### Swift AppDelegate (Expo SDK 53+) + +Check `ios/MParticleExpoTest/AppDelegate.swift` for: + +- Import statement: + + ```swift + import mParticle_Apple_SDK + ``` + +- MParticleOptions initialization in `didFinishLaunchingWithOptions`: + + ```swift + let mParticleOptions = MParticleOptions(key: "YOUR_IOS_API_KEY", secret: "YOUR_IOS_API_SECRET") + mParticleOptions.logLevel = .verbose + mParticleOptions.environment = .development + let identifyRequest = MPIdentityApiRequest.withEmptyUser() + mParticleOptions.identifyRequest = identifyRequest + MParticle.sharedInstance().start(with: mParticleOptions) + ``` + +#### Objective-C AppDelegate (Legacy) + +For older Expo SDK versions, check `ios/MParticleExpoTest/AppDelegate.mm` for: + +- Import statement: + + ```objc + #import "mParticle.h" + ``` + +- MParticleOptions initialization: + + ```objc + MParticleOptions *mParticleOptions = [MParticleOptions optionsWithKey:@"YOUR_IOS_API_KEY" + secret:@"YOUR_IOS_API_SECRET"]; + mParticleOptions.logLevel = MPILogLevelVerbose; + mParticleOptions.environment = MPEnvironmentDevelopment; + MPIdentityApiRequest *identifyRequest = [MPIdentityApiRequest requestWithEmptyUser]; + mParticleOptions.identifyRequest = identifyRequest; + [[MParticle sharedInstance] startWithOptions:mParticleOptions]; + ``` + +#### Podfile + +Check `ios/Podfile` for: + +- pre_install hook for mParticle dynamic framework linking: + + ```ruby + pre_install do |installer| + installer.pod_targets.each do |pod| + if pod.name == 'mParticle-Apple-SDK' || pod.name == 'mParticle-Rokt' || pod.name == 'Rokt-Widget' + def pod.build_type; + Pod::BuildType.new(:linkage => :dynamic, :packaging => :framework) + end + end + end + end + ``` + +- Kit pods (if specified): + + ```ruby + pod 'mParticle-Rokt' + ``` + +### Verify Android Integration + +#### Kotlin MainApplication (Expo SDK 53+) + +Check `android/app/src/main/java/.../MainApplication.kt` for: + +- Import statements: + + ```kotlin + import com.mparticle.MParticle + import com.mparticle.MParticleOptions + import com.mparticle.identity.IdentityApiRequest + ``` + +- MParticleOptions initialization in `onCreate()`: + + ```kotlin + val mParticleOptions = MParticleOptions.builder(this) + .credentials("YOUR_ANDROID_API_KEY", "YOUR_ANDROID_API_SECRET") + .logLevel(MParticle.LogLevel.VERBOSE) + .environment(MParticle.Environment.Development) + .identify(IdentityApiRequest.withEmptyUser().build()) + .build() + MParticle.start(mParticleOptions) + ``` + +#### Java MainApplication (Legacy) + +For older Expo SDK versions, check `android/app/src/main/java/.../MainApplication.java` for: + +- Import statements: + + ```java + import com.mparticle.MParticle; + import com.mparticle.MParticleOptions; + import com.mparticle.identity.IdentityApiRequest; + ``` + +- MParticleOptions initialization: + + ```java + MParticleOptions.Builder optionsBuilder = MParticleOptions.builder(this) + .credentials("YOUR_ANDROID_API_KEY", "YOUR_ANDROID_API_SECRET") + .logLevel(MParticle.LogLevel.VERBOSE) + .environment(MParticle.Environment.Development) + .identify(IdentityApiRequest.withEmptyUser().build()); + MParticle.start(optionsBuilder.build()); + ``` + +#### build.gradle + +Check `android/app/build.gradle` for kit dependencies (if specified): + +```gradle +dependencies { + // mParticle kits + implementation "com.mparticle:android-rokt-kit:+" +} +``` + +## Plugin Configuration Options + +| Option | Type | Description | +|--------|------|-------------| +| `iosApiKey` | string | mParticle iOS API key | +| `iosApiSecret` | string | mParticle iOS API secret | +| `androidApiKey` | string | mParticle Android API key | +| `androidApiSecret` | string | mParticle Android API secret | +| `logLevel` | string | Log level: `none`, `error`, `warning`, `debug`, `verbose` | +| `environment` | string | Environment: `development`, `production`, `autoDetect` | +| `useEmptyIdentifyRequest` | boolean | Initialize with empty identify request (default: true) | +| `dataPlanId` | string | Data plan ID for validation | +| `dataPlanVersion` | number | Data plan version | +| `iosKits` | string[] | iOS kit pod names (e.g., `["mParticle-Rokt"]`) | +| `androidKits` | string[] | Android kit dependencies (e.g., `["android-rokt-kit"]`) | diff --git a/ExpoTestApp/app.json b/ExpoTestApp/app.json new file mode 100644 index 0000000..d7a5398 --- /dev/null +++ b/ExpoTestApp/app.json @@ -0,0 +1,46 @@ +{ + "expo": { + "name": "MParticle Expo Test", + "slug": "mparticle-expo-test", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "newArchEnabled": true, + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.mparticle.expotestapp" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "package": "com.mparticle.expotestapp" + }, + "web": { + "favicon": "./assets/favicon.png" + }, + "plugins": [ + [ + "react-native-mparticle", + { + "iosApiKey": "YOUR_IOS_API_KEY", + "iosApiSecret": "YOUR_IOS_API_SEC", + "androidApiKey": "YOUR_ANDROID_API_KEY", + "androidApiSecret": "YOUR_ANDROID_API_SEC", + "logLevel": "verbose", + "useEmptyIdentifyRequest": true, + "environment": "development", + "iosKits": ["mParticle-Rokt"], + "androidKits": ["android-rokt-kit"] + } + ] + ] + } +} diff --git a/ExpoTestApp/assets/adaptive-icon.png b/ExpoTestApp/assets/adaptive-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03d6f6b6c6727954aec1d8206222769afd178d8d GIT binary patch literal 17547 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 literal 0 HcmV?d00001 diff --git a/ExpoTestApp/assets/icon.png b/ExpoTestApp/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b1526fc7b78680fd8d733dbc6113e1af695487 GIT binary patch literal 22380 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 literal 0 HcmV?d00001 diff --git a/ExpoTestApp/assets/splash-icon.png b/ExpoTestApp/assets/splash-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03d6f6b6c6727954aec1d8206222769afd178d8d GIT binary patch literal 17547 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/AppDelegate.swift`):** + +- Check for `import mParticle_Apple_SDK` +- Check for `MParticleOptions` initialization in `didFinishLaunchingWithOptions` + +**Android (`android/app/src/main/java/.../MainApplication.kt`):** + +- Check for mParticle imports +- Check for `MParticleOptions` initialization in `onCreate()` + +#### Rebuilding After Plugin Changes + +When making changes to the Expo plugin: + +```bash +# From the root directory +yarn build:plugin +yarn dev:pack + +# From ExpoTestApp directory +rm -rf node_modules ios android +npm install +npm run prebuild +``` + ## Development Workflow ### Building the SDK +#### TypeScript/JavaScript + +```bash +yarn build +``` + +#### Expo Plugin + +```bash +yarn build:plugin +``` + +#### Pack for Local Testing + +```bash +yarn dev:pack +``` + +This creates `react-native-mparticle-latest.tgz` which can be used for local testing in the ExpoTestApp or other projects. + #### Android + ```bash cd android ./gradlew build ``` #### iOS + ```bash cd ios pod install @@ -139,8 +261,9 @@ xcodebuild test 1. "Missing config.h" error: This error occurs because the mParticle SDK contains Swift code which requires special handling. To fix this: - + a. Open your `sample/ios/Podfile` and add this block before the target definition: + ```ruby pre_install do |installer| installer.pod_targets.each do |pod| @@ -154,6 +277,7 @@ xcodebuild test ``` b. Clean and reinstall pods: + ```bash cd sample/ios pod cache clean --all @@ -208,6 +332,7 @@ xcodebuild test 4. After merge, create a new release on GitHub 5. Publish to npm: + ```bash npm publish -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index 4cf4e83..1816424 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,136 @@ React Native allows developers to use a single code base to deploy features to multiple platforms. With the mParticle React Native library, you can leverage a single API to deploy your data to hundreds of integrations from your iOS and Android apps. ### Supported Features -| Method | Android | iOS | -| --- | --- | --- | -| Custom Events |

  • [X]
  • |
  • [X]
  • | -| Page Views |
  • [X]
  • |
  • [X]
  • | -| Identity |
  • [X]
  • |
  • [X]
  • | -| eCommerce |
  • [X]
  • |
  • [X]
  • | -| Consent |
  • [X]
  • |
  • [X]
  • | + +| Method | Android | iOS | +| ------------- | ------- | --- | +| Custom Events | ✓ | ✓ | +| Page Views | ✓ | ✓ | +| Identity | ✓ | ✓ | +| eCommerce | ✓ | ✓ | +| Consent | ✓ | ✓ | # Installation **Download and install the mParticle React Native library** from npm: ```bash -$ npm install react-native-mparticle --save +npm install react-native-mparticle --save ``` -## iOS +## Expo + +This library supports Expo projects using the [Expo Config Plugin](https://docs.expo.dev/config-plugins/introduction/). The plugin automatically configures the native iOS and Android projects during `expo prebuild`. + +### Installation - Expo + +1. Install the library: + +```bash +npx expo install react-native-mparticle +``` + +2. Add the plugin to your `app.json` or `app.config.js`: + +```json +{ + "expo": { + "plugins": [ + [ + "react-native-mparticle", + { + "iosApiKey": "YOUR_IOS_API_KEY", + "iosApiSecret": "YOUR_IOS_API_SECRET", + "androidApiKey": "YOUR_ANDROID_API_KEY", + "androidApiSecret": "YOUR_ANDROID_API_SECRET" + } + ] + ] + } +} +``` + +3. Run prebuild: + +```bash +npx expo prebuild --clean +``` + +4. Run the app: + +```bash +npx expo run:ios +# or +npx expo run:android +``` + +### Plugin Configuration Options + +| Option | Type | Required | Description | +| ------------------------- | -------- | -------- | ------------------------------------------------------------------- | +| `iosApiKey` | string | Yes | iOS API key from mParticle dashboard | +| `iosApiSecret` | string | Yes | iOS API secret from mParticle dashboard | +| `androidApiKey` | string | Yes | Android API key from mParticle dashboard | +| `androidApiSecret` | string | Yes | Android API secret from mParticle dashboard | +| `logLevel` | string | No | Log level: `'none'`, `'error'`, `'warning'`, `'debug'`, `'verbose'` | +| `environment` | string | No | Environment: `'development'`, `'production'`, `'autoDetect'` | +| `dataPlanId` | string | No | Data plan ID for validation | +| `dataPlanVersion` | number | No | Data plan version | +| `iosKits` | string[] | No | iOS kit pod names (e.g., `['mParticle-Rokt']`) | +| `androidKits` | string[] | No | Android kit artifact names (e.g., `['android-rokt-kit']`) | +| `useEmptyIdentifyRequest` | boolean | No | Use empty user identify request at init (default: `true`) | + +### Example with Kits + +```json +{ + "expo": { + "plugins": [ + [ + "react-native-mparticle", + { + "iosApiKey": "YOUR_IOS_API_KEY", + "iosApiSecret": "YOUR_IOS_API_SECRET", + "androidApiKey": "YOUR_ANDROID_API_KEY", + "androidApiSecret": "YOUR_ANDROID_API_SECRET", + "environment": "development", + "logLevel": "verbose", + "iosKits": ["mParticle-Rokt", "mParticle-Amplitude"], + "androidKits": ["android-rokt-kit", "android-amplitude-kit"] + } + ] + ] + } +} +``` + +### What the Plugin Does + +**iOS:** + +- Adds mParticle SDK initialization to `AppDelegate` (supports both Swift and Objective-C) +- Configures `pre_install` hook in Podfile for dynamic framework linking +- Adds specified kit pod dependencies + +**Android:** + +- Adds mParticle SDK initialization to `MainApplication` (supports both Kotlin and Java) +- Adds specified kit Maven dependencies to `build.gradle` + +### Version Support + +| Expo SDK | React Native | iOS AppDelegate | Android MainApplication | +| -------- | ------------ | --------------- | ----------------------- | +| 53+ | 0.79+ | Swift | Kotlin | +| 50-52 | 0.73-0.78 | Objective-C++ | Kotlin | +| 49 | 0.72 | Objective-C++ | Java | +| ≤48 | ≤0.71 | Objective-C | Java | + +The plugin automatically detects the language and generates appropriate code for each platform. + +--- + +## iOS (Manual Setup) 1. **Copy your mParticle key and secret** from [your app's dashboard][1]. @@ -33,6 +146,7 @@ $ npm install react-native-mparticle --save The npm install step above will automatically include our react framework and the core iOS framework in your project. However depending on your app and its other dependecies you must integrate it in 1 of 3 ways A. Static Libraries are the React Native default but since mParticle iOS contains swift code you need to add an exception for it in the from of a pre-install command in the Podfile. + ```bash pre_install do |installer| installer.pod_targets.each do |pod| @@ -44,24 +158,31 @@ pre_install do |installer| end end ``` + Then run the following command -``` + +```bash bundle exec pod install ``` -B&C. Frameworks are the default for Swift development and while it isn't preferred by React Native it is supported. Additionally you can define whether the frameworks are built staticly or dynamically. +B&C. Frameworks are the default for Swift development and while it isn't preferred by React Native it is supported. Additionally you can define whether the frameworks are built staticly or dynamically. Update your Podfile to be ready to use dynamically linked frameworks by commenting out the following line + ```bash # :flipper_configuration => flipper_config, ``` + Then run either of the following commands + +```bash +USE_FRAMEWORKS=static bundle exec pod install ``` -$ USE_FRAMEWORKS=static bundle exec pod install -``` + or -``` -$ USE_FRAMEWORKS=dynamic bundle exec pod install + +```bash +USE_FRAMEWORKS=dynamic bundle exec pod install ``` 3. Import and start the mParticle Apple SDK into Swift or Objective-C. @@ -82,10 +203,10 @@ For more help, see [the iOS set up docs](https://docs.mparticle.com/developers/s import mParticle_Apple_SDK func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - + //override point for customization after application launch. let mParticleOptions = MParticleOptions(key: "<<>>", secret: "<<>>") - + //optional- Please see the Identity page for more information on building this object let request = MPIdentityApiRequest() request.email = "email@example.com" @@ -98,7 +219,7 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau mParticleOptions.onAttributionComplete = { (attributionResult, error) in NSLog(@"Attribution Complete. attributionResults = %@", attributionResult.linkInfo) } - MParticle.sharedInstance().start(with: mParticleOptions) + MParticle.sharedInstance().start(with: mParticleOptions) return true } ``` @@ -125,7 +246,7 @@ Next, you'll need to start the SDK: MParticleOptions *mParticleOptions = [MParticleOptions optionsWithKey:@"REPLACE ME" secret:@"REPLACE ME"]; - + //optional - Please see the Identity page for more information on building this object MPIdentityApiRequest *request = [MPIdentityApiRequest requestWithEmptyUser]; request.email = @"email@example.com"; @@ -138,9 +259,9 @@ Next, you'll need to start the SDK: mParticleOptions.onAttributionComplete(MPAttributionResult * _Nullable attributionResult, NSError * _Nullable error) { NSLog(@"Attribution Complete. attributionResults = %@", attributionResult.linkInfo) } - + [[MParticle sharedInstance] startWithOptions:mParticleOptions]; - + return YES; } ``` @@ -148,13 +269,14 @@ Next, you'll need to start the SDK: See [Identity](http://docs.mparticle.com/developers/sdk/ios/identity/) for more information on supplying an `MPIdentityApiRequest` object during SDK initialization. 4. Remember to start Metro with: + ```bash -$ npm start +npm start ``` -and build your workspace from xCode. +and build your workspace from xCode. -## Android +## Android (Manual Setup) 1. Copy your mParticle key and secret from [your workspace's dashboard](https://app.mparticle.com/setup/inputs/apps) and construct an `MParticleOptions` object. @@ -191,15 +313,14 @@ class MyApplication : Application() { } ``` -> **Warning:** Don't log events in your `Application.onCreate()`. Android may instantiate your `Application` class in the background without your knowledge, including when the user isn't using their device, and lead to unexpected results. - +> **Warning:** Don't log events in your `Application.onCreate()`. Android may instantiate your `Application` class in the background without your knowledge, including when the user isn't using their device, and lead to unexpected results. # Usage ## Import the mParticle Module ```js -import MParticle from 'react-native-mparticle' +import MParticle from 'react-native-mparticle'; ``` ## Logging Events @@ -207,38 +328,60 @@ import MParticle from 'react-native-mparticle' To log basic events: ```js -MParticle.logEvent('Test event', MParticle.EventType.Other, { 'Test key': 'Test value' }) +MParticle.logEvent('Test event', MParticle.EventType.Other, { + 'Test key': 'Test value', +}); ``` To log commerce events: ```js -const product = new MParticle.Product('Test product for cart', '1234', 19.99) -const transactionAttributes = new MParticle.TransactionAttributes('Test transaction id') -const event = MParticle.CommerceEvent.createProductActionEvent(MParticle.ProductActionType.AddToCart, [product], transactionAttributes) +const product = new MParticle.Product('Test product for cart', '1234', 19.99); +const transactionAttributes = new MParticle.TransactionAttributes( + 'Test transaction id' +); +const event = MParticle.CommerceEvent.createProductActionEvent( + MParticle.ProductActionType.AddToCart, + [product], + transactionAttributes +); -MParticle.logCommerceEvent(event) +MParticle.logCommerceEvent(event); ``` ```js -const promotion = new MParticle.Promotion('Test promotion id', 'Test promotion name', 'Test creative', 'Test position') -const event = MParticle.CommerceEvent.createPromotionEvent(MParticle.PromotionActionType.View, [promotion]) +const promotion = new MParticle.Promotion( + 'Test promotion id', + 'Test promotion name', + 'Test creative', + 'Test position' +); +const event = MParticle.CommerceEvent.createPromotionEvent( + MParticle.PromotionActionType.View, + [promotion] +); -MParticle.logCommerceEvent(event) +MParticle.logCommerceEvent(event); ``` ```js -const product = new MParticle.Product('Test product that was viewed', '5678', 29.99) -const impression = new MParticle.Impression('Test impression list name', [product]) -const event = MParticle.CommerceEvent.createImpressionEvent([impression]) +const product = new MParticle.Product( + 'Test product that was viewed', + '5678', + 29.99 +); +const impression = new MParticle.Impression('Test impression list name', [ + product, +]); +const event = MParticle.CommerceEvent.createImpressionEvent([impression]); -MParticle.logCommerceEvent(event) +MParticle.logCommerceEvent(event); ``` To log screen events: ```js -MParticle.logScreenEvent('Test screen', { 'Test key': 'Test value' }) +MParticle.logScreenEvent('Test screen', { 'Test key': 'Test value' }); ``` ## User @@ -246,49 +389,59 @@ MParticle.logScreenEvent('Test screen', { 'Test key': 'Test value' }) To set, remove, and get user details, call the `User` or `Identity` methods as follows: ```js -MParticle.User.setUserAttribute('User ID', 'Test key', 'Test value') +MParticle.User.setUserAttribute('User ID', 'Test key', 'Test value'); ``` ```js -MParticle.User.setUserAttribute('User ID', MParticle.UserAttributeType.FirstName, 'Test first name') +MParticle.User.setUserAttribute( + 'User ID', + MParticle.UserAttributeType.FirstName, + 'Test first name' +); ``` ```js -MParticle.User.setUserAttributeArray('User ID', 'Test key', ['Test value 1', 'Test value 2']) +MParticle.User.setUserAttributeArray('User ID', 'Test key', [ + 'Test value 1', + 'Test value 2', +]); ``` ```js -MParticle.User.setUserTag('User ID', 'Test value') +MParticle.User.setUserTag('User ID', 'Test value'); ``` ```js -MParticle.User.removeUserAttribute('User ID', 'Test key') +MParticle.User.removeUserAttribute('User ID', 'Test key'); ``` ```js -MParticle.Identity.getUserIdentities((userIdentities) => { - console.debug(userIdentities); +MParticle.Identity.getUserIdentities(userIdentities => { + console.debug(userIdentities); }); ``` ## IdentityRequest ```js -var request = new MParticle.IdentityRequest() +var request = new MParticle.IdentityRequest(); ``` **Setting** user identities: ```js var request = new MParticle.IdentityRequest(); -request.setUserIdentity('example@example.com', MParticle.UserIdentityType.Email); +request.setUserIdentity( + 'example@example.com', + MParticle.UserIdentityType.Email +); ``` ## Identity ```js -MParticle.Identity.getCurrentUser((currentUser) => { - console.debug(currentUser.userID); +MParticle.Identity.getCurrentUser(currentUser => { + console.debug(currentUser.userID); }); ``` @@ -296,11 +449,11 @@ MParticle.Identity.getCurrentUser((currentUser) => { var request = new MParticle.IdentityRequest(); MParticle.Identity.identify(request, (error, userId) => { - if (error) { - console.debug(error); //error is an MParticleError - } else { - console.debug(userId); - } + if (error) { + console.debug(error); //error is an MParticleError + } else { + console.debug(userId); + } }); ``` @@ -309,11 +462,11 @@ var request = new MParticle.IdentityRequest(); request.email = 'test email'; MParticle.Identity.login(request, (error, userId) => { - if (error) { - console.debug(error); //error is an MParticleError - } else { - console.debug(userId); - } + if (error) { + console.debug(error); //error is an MParticleError + } else { + console.debug(userId); + } }); ``` @@ -321,11 +474,11 @@ MParticle.Identity.login(request, (error, userId) => { var request = new MParticle.IdentityRequest(); MParticle.Identity.logout(request, (error, userId) => { - if (error) { - console.debug(error); - } else { - console.debug(userId); - } + if (error) { + console.debug(error); + } else { + console.debug(userId); + } }); ``` @@ -334,31 +487,33 @@ var request = new MParticle.IdentityRequest(); request.email = 'test email 2'; MParticle.Identity.modify(request, (error, userId) => { - if (error) { - console.debug(error); //error is an MParticleError - } else { - console.debug(userId); - } + if (error) { + console.debug(error); //error is an MParticleError + } else { + console.debug(userId); + } }); ``` ## Attribution -``` + +```js var attributions = MParticle.getAttributions(); ``` -In order to listen for Attributions asynchronously, you need to set the proper field in `MParticleOptions` as shown in the [Android](#Android) or the [iOS](#iOS) SDK start examples. +In order to listen for Attributions asynchronously, you need to set the proper field in `MParticleOptions` as shown in the [Android](#android-manual-setup) or the [iOS](#ios-manual-setup) SDK start examples. ## Kits + Check if a kit is active -``` +```js var isKitActive = MParticle.isKitActive(kitId); ``` Check and set the SDK's opt out status -``` +```js var isOptedOut = MParticle.getOptOut(); MParticle.setOptOut(!isOptedOut); ``` @@ -369,55 +524,58 @@ The method `MParticle.logPushRegistration()` accepts 2 parameters. For Android, ### Android -``` +```js MParticle.logPushRegistration(pushToken, senderId); ``` ### iOS -``` +```js MParticle.logPushRegistration(pushToken, null); ``` ## GDPR Consent + Add a GDPRConsent -``` +```js var gdprConsent = GDPRConsent() - .setConsented(true) - .setDocument("the document") - .setTimestamp(new Date().getTime()) // optional, native SDK will automatically set current timestamp if omitted - .setLocation("the location") - .setHardwareId("the hardwareId"); + .setConsented(true) + .setDocument('the document') + .setTimestamp(new Date().getTime()) // optional, native SDK will automatically set current timestamp if omitted + .setLocation('the location') + .setHardwareId('the hardwareId'); -MParticle.addGDPRConsentState(gdprConsent, "the purpose"); +MParticle.addGDPRConsentState(gdprConsent, 'the purpose'); ``` Remove a GDPRConsent -``` -MParticle.removeGDPRConsentStateWithPurpose("the purpose"); + +```js +MParticle.removeGDPRConsentStateWithPurpose('the purpose'); ``` ## CCPA Consent + Add a CCPAConsent -``` +```js var ccpaConsent = CCPAConsent() - .setConsented(true) - .setDocument("the document") - .setTimestamp(new Date().getTime()) // optional, native SDK will automatically set current timestamp if omitted - .setLocation("the location") - .setHardwareId("the hardwareId"); + .setConsented(true) + .setDocument('the document') + .setTimestamp(new Date().getTime()) // optional, native SDK will automatically set current timestamp if omitted + .setLocation('the location') + .setHardwareId('the hardwareId'); MParticle.addCCPAConsentState(ccpaConsent); ``` Remove CCPAConsent -``` + +```js MParticle.removeCCPAConsentState(); ``` - # License Apache 2.0 diff --git a/app.plugin.js b/app.plugin.js new file mode 100644 index 0000000..d9ecc60 --- /dev/null +++ b/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build/withMParticle'); diff --git a/js/rokt/rokt-layout-view.android.tsx b/js/rokt/rokt-layout-view.android.tsx index 037f2af..f6f6d6f 100644 --- a/js/rokt/rokt-layout-view.android.tsx +++ b/js/rokt/rokt-layout-view.android.tsx @@ -103,9 +103,10 @@ export class RoktLayoutView extends Component< // Return the native component with the props // Cast to React.ComponentType to make it compatible with JSX + // Using 'unknown' intermediate cast for compatibility with different @types/react versions const RoktComponent = // eslint-disable-next-line @typescript-eslint/no-explicit-any - RoktNativeLayoutComponent as React.ComponentType; + RoktNativeLayoutComponent as unknown as React.ComponentType; return ( = 16.0.0-alpha.12", - "react-native": ">= 0.45.0" + "react-native": ">= 0.45.0", + "@expo/config-plugins": ">=7.0.0" + }, + "peerDependenciesMeta": { + "@expo/config-plugins": { + "optional": true + } }, "devDependencies": { + "@expo/config-plugins": "^7.2.5", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "eslint": "^8.45.0", "eslint-config-prettier": "^8.10.0", "eslint-plugin-prettier": "^4.2.1", + "expo-module-scripts": "^3.0.0", "prettier": "^2.8.8", "@types/react": "^18.0.0", "@types/react-native": "^0.70.0", diff --git a/plugin/src/withMParticle.ts b/plugin/src/withMParticle.ts new file mode 100644 index 0000000..8ef9980 --- /dev/null +++ b/plugin/src/withMParticle.ts @@ -0,0 +1,106 @@ +import { ConfigPlugin, createRunOncePlugin } from '@expo/config-plugins'; +import { withMParticleIOS } from './withMParticleIOS'; +import { withMParticleAndroid } from './withMParticleAndroid'; + +const pkg = require('../../package.json'); + +/** + * mParticle plugin configuration options + */ +export interface MParticlePluginProps { + /** + * iOS API key from mParticle dashboard + */ + iosApiKey: string; + + /** + * iOS API secret from mParticle dashboard + */ + iosApiSecret: string; + + /** + * Android API key from mParticle dashboard + */ + androidApiKey: string; + + /** + * Android API secret from mParticle dashboard + */ + androidApiSecret: string; + + /** + * Log level for debugging + * @default 'none' + */ + logLevel?: 'none' | 'error' | 'warning' | 'debug' | 'verbose'; + + /** + * mParticle environment + * @default 'autoDetect' + */ + environment?: 'development' | 'production' | 'autoDetect'; + + /** + * Data plan ID for validation + */ + dataPlanId?: string; + + /** + * Data plan version for validation + */ + dataPlanVersion?: number; + + /** + * iOS kit pod names to include + * @example ['mParticle-Rokt', 'mParticle-Amplitude'] + */ + iosKits?: string[]; + + /** + * Android kit artifact names to include (version auto-detected from core SDK) + * @example ['android-rokt-kit', 'android-amplitude-kit'] + */ + androidKits?: string[]; + + /** + * Whether to use an empty identify request at initialization + * If true or omitted, uses requestWithEmptyUser/withEmptyUser() + * If false, no identify request is made at initialization + * Identity should be updated from React Native code after initialization + * @default true + */ + useEmptyIdentifyRequest?: boolean; +} + +/** + * Expo Config Plugin for mParticle React Native SDK + * + * This plugin configures your Expo project to use the mParticle SDK by: + * - Adding mParticle SDK initialization to iOS AppDelegate + * - Adding mParticle SDK initialization to Android MainApplication + * - Adding kit dependencies to iOS Podfile + * - Adding kit dependencies to Android build.gradle + */ +const withMParticle: ConfigPlugin = (config, props) => { + // Validate required props + if (!props.iosApiKey || !props.iosApiSecret) { + throw new Error( + 'react-native-mparticle plugin requires iosApiKey and iosApiSecret' + ); + } + if (!props.androidApiKey || !props.androidApiSecret) { + throw new Error( + 'react-native-mparticle plugin requires androidApiKey and androidApiSecret' + ); + } + + // Apply iOS modifications + config = withMParticleIOS(config, props); + + // Apply Android modifications + config = withMParticleAndroid(config, props); + + return config; +}; + +export default createRunOncePlugin(withMParticle, pkg.name, pkg.version); diff --git a/plugin/src/withMParticleAndroid.ts b/plugin/src/withMParticleAndroid.ts new file mode 100644 index 0000000..779a261 --- /dev/null +++ b/plugin/src/withMParticleAndroid.ts @@ -0,0 +1,359 @@ +import { + ConfigPlugin, + withMainApplication, + withAppBuildGradle, +} from '@expo/config-plugins'; +import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; +import { MParticlePluginProps } from './withMParticle'; + +// Tag used for mergeContents to identify code blocks added by this plugin +const MPARTICLE_TAG = 'react-native-mparticle'; + +/** + * Get the mParticle log level string for Android + */ +function getAndroidLogLevel( + logLevel?: MParticlePluginProps['logLevel'] +): string | null { + switch (logLevel) { + case 'none': + return 'MParticle.LogLevel.NONE'; + case 'error': + return 'MParticle.LogLevel.ERROR'; + case 'warning': + return 'MParticle.LogLevel.WARNING'; + case 'debug': + return 'MParticle.LogLevel.DEBUG'; + case 'verbose': + return 'MParticle.LogLevel.VERBOSE'; + default: + return null; + } +} + +/** + * Get the mParticle environment string for Android + */ +function getAndroidEnvironment( + environment?: MParticlePluginProps['environment'] +): string | null { + switch (environment) { + case 'development': + return 'MParticle.Environment.Development'; + case 'production': + return 'MParticle.Environment.Production'; + case 'autoDetect': + return 'MParticle.Environment.AutoDetect'; + default: + return null; + } +} + +/** + * Generate mParticle initialization code for Kotlin MainApplication + */ +function generateKotlinInitCode(props: MParticlePluginProps): string { + const { + androidApiKey, + androidApiSecret, + logLevel, + environment, + useEmptyIdentifyRequest = true, + dataPlanId, + dataPlanVersion, + } = props; + + const lines: string[] = [ + '// mParticle SDK initialization', + 'val mParticleOptions = MParticleOptions.builder(this)', + ` .credentials("${androidApiKey}", "${androidApiSecret}")`, + ]; + + const androidLogLevel = getAndroidLogLevel(logLevel); + if (androidLogLevel) { + lines.push(` .logLevel(${androidLogLevel})`); + } + + const androidEnvironment = getAndroidEnvironment(environment); + if (androidEnvironment) { + lines.push(` .environment(${androidEnvironment})`); + } + + if (dataPlanId) { + const versionParam = dataPlanVersion ? `, ${dataPlanVersion}` : ''; + lines.push(` .dataplan("${dataPlanId}"${versionParam})`); + } + + if (useEmptyIdentifyRequest) { + lines.push(' .identify(IdentityApiRequest.withEmptyUser().build())'); + } + + lines.push(' .build()'); + lines.push('MParticle.start(mParticleOptions)'); + + return lines.join('\n '); +} + +/** + * Generate mParticle initialization code for Java MainApplication + */ +function generateJavaInitCode(props: MParticlePluginProps): string { + const { + androidApiKey, + androidApiSecret, + logLevel, + environment, + useEmptyIdentifyRequest = true, + dataPlanId, + dataPlanVersion, + } = props; + + const lines: string[] = [ + '// mParticle SDK initialization', + 'MParticleOptions.Builder optionsBuilder = MParticleOptions.builder(this)', + ` .credentials("${androidApiKey}", "${androidApiSecret}")`, + ]; + + const androidLogLevel = getAndroidLogLevel(logLevel); + if (androidLogLevel) { + lines.push(` .logLevel(${androidLogLevel})`); + } + + const androidEnvironment = getAndroidEnvironment(environment); + if (androidEnvironment) { + lines.push(` .environment(${androidEnvironment})`); + } + + if (dataPlanId) { + const versionParam = dataPlanVersion ? `, ${dataPlanVersion}` : ''; + lines.push(` .dataplan("${dataPlanId}"${versionParam})`); + } + + if (useEmptyIdentifyRequest) { + lines.push(' .identify(IdentityApiRequest.withEmptyUser().build())'); + } + + // Java needs semicolons + lines.push(';'); + lines.push('MParticle.start(optionsBuilder.build());'); + + return lines.join('\n '); +} + +/** + * Generate mParticle import statements for Kotlin + */ +function getKotlinImports(): string { + return `import com.mparticle.MParticle +import com.mparticle.MParticleOptions +import com.mparticle.identity.IdentityApiRequest`; +} + +/** + * Generate mParticle import statements for Java + */ +function getJavaImports(): string { + return `import com.mparticle.MParticle; +import com.mparticle.MParticleOptions; +import com.mparticle.identity.IdentityApiRequest;`; +} + +/** + * Add mParticle configuration to MainApplication + * Handles both Kotlin and Java + */ +const withMParticleMainApplication: ConfigPlugin = ( + config, + props +) => { + return withMainApplication(config, config => { + const { contents, language } = config.modResults; + + // Check if mParticle is already initialized + if ( + contents.includes('MParticleOptions') || + contents.includes('mParticleOptions') + ) { + return config; + } + + const isKotlin = language === 'kt'; + + if (isKotlin) { + config.modResults.contents = addMParticleToKotlinMainApplication( + contents, + props + ); + } else if (language === 'java') { + config.modResults.contents = addMParticleToJavaMainApplication( + contents, + props + ); + } else { + console.warn( + `[react-native-mparticle] Unsupported MainApplication language: ${language}. ` + + 'mParticle initialization must be added manually.' + ); + } + + return config; + }); +}; + +/** + * Add mParticle import and initialization to Kotlin MainApplication + */ +function addMParticleToKotlinMainApplication( + contents: string, + props: MParticlePluginProps +): string { + // Add import statements using mergeContents + const withImports = mergeContents({ + src: contents, + newSrc: getKotlinImports(), + anchor: /^package .+$/m, + offset: 1, // Add after package declaration + tag: `${MPARTICLE_TAG}-import`, + comment: '//', + }); + + // Generate initialization code + const initCode = generateKotlinInitCode(props); + + // Find the right place to add initialization code + // Try ApplicationLifecycleDispatcher first (Expo pattern), then super.onCreate() + let result = withImports.contents; + + if ( + result.includes('ApplicationLifecycleDispatcher.onApplicationCreate(this)') + ) { + const withInit = mergeContents({ + src: result, + newSrc: `\n ${initCode}\n`, + anchor: /ApplicationLifecycleDispatcher\.onApplicationCreate\(this\)/, + offset: 1, // Add after the anchor + tag: `${MPARTICLE_TAG}-init`, + comment: '//', + }); + result = withInit.contents; + } else if (result.includes('super.onCreate()')) { + const withInit = mergeContents({ + src: result, + newSrc: `\n ${initCode}\n`, + anchor: /super\.onCreate\(\)/, + offset: 1, // Add after the anchor + tag: `${MPARTICLE_TAG}-init`, + comment: '//', + }); + result = withInit.contents; + } + + return result; +} + +/** + * Add mParticle import and initialization to Java MainApplication + */ +function addMParticleToJavaMainApplication( + contents: string, + props: MParticlePluginProps +): string { + // Add import statements using mergeContents + const withImports = mergeContents({ + src: contents, + newSrc: getJavaImports(), + anchor: /^package .+;$/m, + offset: 1, // Add after package declaration + tag: `${MPARTICLE_TAG}-import`, + comment: '//', + }); + + // Generate initialization code + const initCode = generateJavaInitCode(props); + + // Find the right place to add initialization code + let result = withImports.contents; + + if ( + result.includes('ApplicationLifecycleDispatcher.onApplicationCreate(this);') + ) { + const withInit = mergeContents({ + src: result, + newSrc: `\n ${initCode}\n`, + anchor: /ApplicationLifecycleDispatcher\.onApplicationCreate\(this\);/, + offset: 1, // Add after the anchor + tag: `${MPARTICLE_TAG}-init`, + comment: '//', + }); + result = withInit.contents; + } else if (result.includes('super.onCreate();')) { + const withInit = mergeContents({ + src: result, + newSrc: `\n ${initCode}\n`, + anchor: /super\.onCreate\(\);/, + offset: 1, // Add after the anchor + tag: `${MPARTICLE_TAG}-init`, + comment: '//', + }); + result = withInit.contents; + } + + return result; +} + +/** + * Add kit dependencies to app/build.gradle + */ +const withMParticleAppBuildGradle: ConfigPlugin = ( + config, + props +) => { + return withAppBuildGradle(config, config => { + const { contents } = config.modResults; + + if (!props.androidKits || props.androidKits.length === 0) { + return config; + } + + // Check if kits are already added + const kitsAlreadyAdded = props.androidKits.every(kit => + contents.includes(`com.mparticle:${kit}`) + ); + + if (kitsAlreadyAdded) { + return config; + } + + // Generate kit dependency lines + // Use + for version to auto-match core SDK version + const kitDependencies = props.androidKits + .map(kit => ` implementation "com.mparticle:${kit}:+"`) + .join('\n'); + + // Use mergeContents for idempotent injection + const withKits = mergeContents({ + src: contents, + newSrc: `\n // mParticle kits\n${kitDependencies}`, + anchor: /dependencies\s*\{/, + offset: 1, // Add after the opening brace + tag: `${MPARTICLE_TAG}-kits`, + comment: '//', + }); + + config.modResults.contents = withKits.contents; + return config; + }); +}; + +/** + * Apply all Android-specific mParticle configurations + */ +export const withMParticleAndroid: ConfigPlugin = ( + config, + props +) => { + config = withMParticleMainApplication(config, props); + config = withMParticleAppBuildGradle(config, props); + + return config; +}; diff --git a/plugin/src/withMParticleIOS.ts b/plugin/src/withMParticleIOS.ts new file mode 100644 index 0000000..6c1f38a --- /dev/null +++ b/plugin/src/withMParticleIOS.ts @@ -0,0 +1,459 @@ +import { + ConfigPlugin, + withAppDelegate, + withDangerousMod, +} from '@expo/config-plugins'; +import { mergeContents } from '@expo/config-plugins/build/utils/generateCode'; +import { MParticlePluginProps } from './withMParticle'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Tag used for mergeContents to identify code blocks added by this plugin +const MPARTICLE_TAG = 'react-native-mparticle'; + +/** + * Get the mParticle log level for iOS (Swift syntax) + */ +function getSwiftLogLevel( + logLevel?: MParticlePluginProps['logLevel'] +): string | null { + switch (logLevel) { + case 'none': + return '.none'; + case 'error': + return '.error'; + case 'warning': + return '.warning'; + case 'debug': + return '.debug'; + case 'verbose': + return '.verbose'; + default: + return null; + } +} + +/** + * Get the mParticle environment for iOS (Swift syntax) + */ +function getSwiftEnvironment( + environment?: MParticlePluginProps['environment'] +): string | null { + switch (environment) { + case 'development': + return '.development'; + case 'production': + return '.production'; + case 'autoDetect': + return '.autoDetect'; + default: + return null; + } +} + +/** + * Get the mParticle log level for iOS (Objective-C syntax) + */ +function getObjcLogLevel( + logLevel?: MParticlePluginProps['logLevel'] +): string | null { + switch (logLevel) { + case 'none': + return 'MPILogLevelNone'; + case 'error': + return 'MPILogLevelError'; + case 'warning': + return 'MPILogLevelWarning'; + case 'debug': + return 'MPILogLevelDebug'; + case 'verbose': + return 'MPILogLevelVerbose'; + default: + return null; + } +} + +/** + * Get the mParticle environment for iOS (Objective-C syntax) + */ +function getObjcEnvironment( + environment?: MParticlePluginProps['environment'] +): string | null { + switch (environment) { + case 'development': + return 'MPEnvironmentDevelopment'; + case 'production': + return 'MPEnvironmentProduction'; + case 'autoDetect': + return 'MPEnvironmentAutoDetect'; + default: + return null; + } +} + +/** + * Generate mParticle initialization code for Swift AppDelegate + */ +function generateSwiftInitCode(props: MParticlePluginProps): string { + const { + iosApiKey, + iosApiSecret, + logLevel, + environment, + useEmptyIdentifyRequest = true, + dataPlanId, + dataPlanVersion, + } = props; + + const lines: string[] = [ + '// mParticle SDK initialization', + `let mParticleOptions = MParticleOptions(key: "${iosApiKey}", secret: "${iosApiSecret}")`, + ]; + + const swiftLogLevel = getSwiftLogLevel(logLevel); + if (swiftLogLevel) { + lines.push(`mParticleOptions.logLevel = ${swiftLogLevel}`); + } + + const swiftEnvironment = getSwiftEnvironment(environment); + if (swiftEnvironment) { + lines.push(`mParticleOptions.environment = ${swiftEnvironment}`); + } + + if (dataPlanId) { + lines.push(`mParticleOptions.dataPlanId = "${dataPlanId}"`); + if (dataPlanVersion) { + lines.push( + `mParticleOptions.dataPlanVersion = ${dataPlanVersion} as NSNumber` + ); + } + } + + if (useEmptyIdentifyRequest) { + lines.push('let identifyRequest = MPIdentityApiRequest.withEmptyUser()'); + lines.push('mParticleOptions.identifyRequest = identifyRequest'); + } + + lines.push('MParticle.sharedInstance().start(with: mParticleOptions)'); + + return lines.join('\n '); +} + +/** + * Generate mParticle initialization code for Objective-C AppDelegate + */ +function generateObjcInitCode(props: MParticlePluginProps): string { + const { + iosApiKey, + iosApiSecret, + logLevel, + environment, + useEmptyIdentifyRequest = true, + dataPlanId, + dataPlanVersion, + } = props; + + const lines: string[] = [ + '// mParticle SDK initialization', + `MParticleOptions *mParticleOptions = [MParticleOptions optionsWithKey:@"${iosApiKey}"`, + ` secret:@"${iosApiSecret}"];`, + ]; + + const objcLogLevel = getObjcLogLevel(logLevel); + if (objcLogLevel) { + lines.push(`mParticleOptions.logLevel = ${objcLogLevel};`); + } + + const objcEnvironment = getObjcEnvironment(environment); + if (objcEnvironment) { + lines.push(`mParticleOptions.environment = ${objcEnvironment};`); + } + + if (dataPlanId) { + lines.push(`mParticleOptions.dataPlanId = @"${dataPlanId}";`); + if (dataPlanVersion) { + lines.push(`mParticleOptions.dataPlanVersion = @(${dataPlanVersion});`); + } + } + + if (useEmptyIdentifyRequest) { + lines.push( + 'MPIdentityApiRequest *identifyRequest = [MPIdentityApiRequest requestWithEmptyUser];' + ); + lines.push('mParticleOptions.identifyRequest = identifyRequest;'); + } + + lines.push('[[MParticle sharedInstance] startWithOptions:mParticleOptions];'); + + return lines.join('\n '); +} + +/** + * Add mParticle configuration to AppDelegate + * Handles both Swift and Objective-C/Objective-C++ + */ +const withMParticleAppDelegate: ConfigPlugin = ( + config, + props +) => { + return withAppDelegate(config, config => { + const { contents, language } = config.modResults; + + // Check if mParticle is already initialized + if ( + contents.includes('MParticleOptions') || + contents.includes('mParticleOptions') + ) { + return config; + } + + if (language === 'swift') { + config.modResults.contents = addMParticleToSwiftAppDelegate( + contents, + props + ); + } else if (language === 'objc' || language === 'objcpp') { + config.modResults.contents = addMParticleToObjcAppDelegate( + contents, + props + ); + } else { + console.warn( + `[react-native-mparticle] Unsupported AppDelegate language: ${language}. ` + + 'mParticle initialization must be added manually.' + ); + } + + return config; + }); +}; + +/** + * Add mParticle import and initialization to Swift AppDelegate + */ +function addMParticleToSwiftAppDelegate( + contents: string, + props: MParticlePluginProps +): string { + // Add import statement + // Use mergeContents for safe, idempotent code injection + const withImport = mergeContents({ + src: contents, + newSrc: 'import mParticle_Apple_SDK', + anchor: /import Expo/, + offset: 1, // Add after the anchor + tag: `${MPARTICLE_TAG}-import`, + comment: '//', + }); + + // Generate initialization code + const initCode = generateSwiftInitCode(props); + + // Find the right place to add initialization code + // For Expo SDK 53+, it should be in didFinishLaunchingWithOptions before the return + // Look for the return super.application pattern + const withInit = mergeContents({ + src: withImport.contents, + newSrc: `\n ${initCode}\n`, + anchor: + /return super\.application\(application, didFinishLaunchingWithOptions: launchOptions\)/, + offset: 0, // Add before the anchor + tag: `${MPARTICLE_TAG}-init`, + comment: '//', + }); + + return withInit.contents; +} + +/** + * Add mParticle import and initialization to Objective-C AppDelegate + */ +function addMParticleToObjcAppDelegate( + contents: string, + props: MParticlePluginProps +): string { + // Add import statement after React import or first import + const withImport = mergeContents({ + src: contents, + newSrc: '#import "mParticle.h"', + anchor: /#import |#import "AppDelegate\.h"/, + offset: 1, // Add after the anchor + tag: `${MPARTICLE_TAG}-import`, + comment: '//', + }); + + // Generate initialization code + const initCode = generateObjcInitCode(props); + + // Try different patterns for where to insert the init code + let result = withImport.contents; + + // Pattern 1: Expo's return [super application... + if ( + result.includes( + 'return [super application:application didFinishLaunchingWithOptions:launchOptions];' + ) + ) { + const withInit = mergeContents({ + src: result, + newSrc: `\n ${initCode}\n`, + anchor: + /return \[super application:application didFinishLaunchingWithOptions:launchOptions\];/, + offset: 0, + tag: `${MPARTICLE_TAG}-init`, + comment: '//', + }); + result = withInit.contents; + } + // Pattern 2: self.initialProps = @{}; + else if (result.includes('self.initialProps = @{};')) { + const withInit = mergeContents({ + src: result, + newSrc: `\n ${initCode}\n`, + anchor: /self\.initialProps = @\{\};/, + offset: 1, + tag: `${MPARTICLE_TAG}-init`, + comment: '//', + }); + result = withInit.contents; + } + // Pattern 3: return YES; + else if (result.includes('return YES;')) { + const withInit = mergeContents({ + src: result, + newSrc: `\n ${initCode}\n`, + anchor: /return YES;/, + offset: 0, + tag: `${MPARTICLE_TAG}-init`, + comment: '//', + }); + result = withInit.contents; + } + + return result; +} + +/** + * Known transitive dependencies that need dynamic linking for each kit + * These are dependencies of mParticle kits that must also be dynamic frameworks + */ +const KIT_TRANSITIVE_DEPENDENCIES: Record = { + 'mParticle-Rokt': ['Rokt-Widget'], + // Add other kit dependencies here as needed + // "mParticle-Amplitude": [], + // "mParticle-Braze": [], +}; + +/** + * Get all pods that need dynamic framework linking + */ +function getDynamicFrameworkPods(iosKits?: string[]): string[] { + const pods = ['mParticle-Apple-SDK']; + + if (iosKits) { + for (const kit of iosKits) { + pods.push(kit); + // Add transitive dependencies for this kit + const transitiveDeps = KIT_TRANSITIVE_DEPENDENCIES[kit]; + if (transitiveDeps) { + pods.push(...transitiveDeps); + } + } + } + + return [...new Set(pods)]; // Remove duplicates +} + +/** + * Add kit pods and pre_install hook to Podfile + */ +const withMParticlePodfile: ConfigPlugin = ( + config, + props +) => { + return withDangerousMod(config, [ + 'ios', + async config => { + const podfilePath = path.join( + config.modRequest.platformProjectRoot, + 'Podfile' + ); + + if (!fs.existsSync(podfilePath)) { + return config; + } + + let podfileContent = fs.readFileSync(podfilePath, 'utf-8'); + + // Add pre_install hook for dynamic framework linking if not already present + if (!podfileContent.includes('mParticle-Apple-SDK')) { + // Get all pods that need dynamic linking (including transitive dependencies) + const dynamicPods = getDynamicFrameworkPods(props.iosKits); + const podConditions = dynamicPods + .map(pod => `pod.name == '${pod}'`) + .join(' || '); + + const preInstallHook = ` +# mParticle dynamic framework linking (added by react-native-mparticle expo plugin) +pre_install do |installer| + installer.pod_targets.each do |pod| + if ${podConditions} + def pod.build_type; + Pod::BuildType.new(:linkage => :dynamic, :packaging => :framework) + end + end + end +end +`; + + // Add pre_install hook after platform declaration + const platformRegex = /platform :ios.*\n/; + if (platformRegex.test(podfileContent)) { + podfileContent = podfileContent.replace( + platformRegex, + `$&${preInstallHook}` + ); + } + } + + // Add kit pods if specified + if (props.iosKits && props.iosKits.length > 0) { + const kitPods = props.iosKits.map(kit => ` pod '${kit}'`).join('\n'); + + // Check if kits are already added + const kitsAlreadyAdded = props.iosKits.every(kit => + podfileContent.includes(`pod '${kit}'`) + ); + + if (!kitsAlreadyAdded) { + // Add kit pods inside the main target block + // Look for use_react_native! and add after it + const useReactNativeRegex = /(use_react_native!\([^)]*\))/s; + if (useReactNativeRegex.test(podfileContent)) { + podfileContent = podfileContent.replace( + useReactNativeRegex, + `$1\n\n # mParticle kits (added by react-native-mparticle expo plugin)\n${kitPods}` + ); + } + } + } + + fs.writeFileSync(podfilePath, podfileContent); + + return config; + }, + ]); +}; + +/** + * Apply all iOS-specific mParticle configurations + */ +export const withMParticleIOS: ConfigPlugin = ( + config, + props +) => { + config = withMParticleAppDelegate(config, props); + config = withMParticlePodfile(config, props); + + return config; +}; diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json new file mode 100644 index 0000000..6375b28 --- /dev/null +++ b/plugin/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "expo-module-scripts/tsconfig.plugin", + "compilerOptions": { + "outDir": "build", + "rootDir": "src" + }, + "include": ["./src"] +}