From 14e71583b4ebcf291de785010824285b9d09e67c Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Wed, 13 May 2026 17:00:06 -0400 Subject: [PATCH 1/9] feat(rokt): support mParticle Apple SDK 9.2 Update the React Native wrapper for the published mParticle Apple SDK and Rokt kit 9.2.0 release. This pins the iOS dependency range, adds the new Rokt close and session APIs, and keeps URL callback handling in native iOS lifecycle code instead of exposing a React Native API. The Expo plugin now supports iOS custom base URLs before startup and injects native Rokt URL forwarding for standard generated AppDelegate handlers. The sample apps and documentation cover the migration, Shoppable Ads setup, and platform behavior. --- .trunk/trunk.yaml | 2 +- ExpoTestApp/App.tsx | 39 + ExpoTestApp/README.md | 97 ++- ExpoTestApp/app.json | 9 + MIGRATING.md | 142 +++ README.md | 81 ++ .../mparticle/react/rokt/MPRoktModuleImpl.kt | 19 + .../com/mparticle/react/rokt/MPRoktModule.kt | 19 + .../com/mparticle/react/NativeMPRoktSpec.kt | 10 + .../com/mparticle/react/rokt/MPRoktModule.kt | 19 + ios/RNMParticle/RNMPRokt.mm | 55 ++ js/codegenSpecs/NativeMParticle.ts | 5 +- js/codegenSpecs/rokt/NativeMPRokt.ts | 6 + js/rokt/rokt.ts | 12 + plugin/src/withMParticle.ts | 7 + plugin/src/withMParticleIOS.ts | 154 +++- react-native-mparticle.podspec | 4 +- sample/README.md | 18 +- sample/__mocks__/react-native-mparticle.js | 61 +- sample/index.js | 809 ++++++++++-------- sample/ios/Podfile | 2 +- 21 files changed, 1149 insertions(+), 421 deletions(-) create mode 100644 MIGRATING.md diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index bb89142..a5688d1 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -44,7 +44,7 @@ lint: # ESLint 9+ defaults to flat config only; this repo uses .eslintrc.js (ESLint 8 style). # Trunk runs ESLint in an isolated env without the repo's node_modules; bundle the same # plugins/parser as package.json so @typescript-eslint/* resolves (CI + local). - - eslint@8.57.1: + - eslint@8.57.0: packages: - '@typescript-eslint/eslint-plugin@5.62.0' - '@typescript-eslint/parser@5.62.0' diff --git a/ExpoTestApp/App.tsx b/ExpoTestApp/App.tsx index 3668573..add4475 100644 --- a/ExpoTestApp/App.tsx +++ b/ExpoTestApp/App.tsx @@ -257,6 +257,31 @@ export default function App() { }); }; + const handleRoktClose = () => { + MParticle.Rokt.close() + .then(() => { + addLog('Rokt close called'); + setStatus('Rokt close called'); + }) + .catch((error: any) => { + addLog(`Rokt close error: ${JSON.stringify(error)}`); + }); + }; + + const handleRoktSession = () => { + const sessionId = `rn-expo-${Date.now()}`; + + MParticle.Rokt.setSessionId(sessionId) + .then(() => MParticle.Rokt.getSessionId()) + .then((currentSessionId: string | null) => { + addLog(`Rokt session ID: ${currentSessionId ?? 'none'}`); + setStatus(`Rokt session ID: ${currentSessionId ?? 'none'}`); + }) + .catch((error: any) => { + addLog(`Rokt session error: ${JSON.stringify(error)}`); + }); + }; + return ( @@ -368,6 +393,20 @@ export default function App() { > Shoppable Ads + + + Close Rokt + + + + Rokt Session + {/* Rokt Embedded Placeholder */} diff --git a/ExpoTestApp/README.md b/ExpoTestApp/README.md index 3edb204..fc9545c 100644 --- a/ExpoTestApp/README.md +++ b/ExpoTestApp/README.md @@ -25,6 +25,14 @@ This app tests the Expo config plugin integration for the mParticle React Native { "expo": { "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "deploymentTarget": "15.6" + } + } + ], [ "react-native-mparticle", { @@ -34,6 +42,7 @@ This app tests the Expo config plugin integration for the mParticle React Native "androidApiSecret": "YOUR_ANDROID_API_SECRET", "logLevel": "verbose", "environment": "development", + "iosCustomBaseURL": "https://cname.example.com", "iosKits": ["mParticle-Rokt"], "androidKits": ["android-rokt-kit"] } @@ -81,6 +90,8 @@ The app also includes Rokt placement testing via the mParticle Rokt kit: - **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. - **Shoppable Ads**: Calls `MParticle.Rokt.selectShoppableAds` with a staging placement identifier and checkout-style attributes (see implementation guide below). +- **Close Rokt**: Calls `MParticle.Rokt.close()`. +- **Rokt Session**: Calls `MParticle.Rokt.setSessionId()` and `MParticle.Rokt.getSessionId()`. The Rokt section also demonstrates: @@ -118,29 +129,76 @@ Listen for `RoktCallback` and `RoktEvents` on `RoktEventManager` to observe load **Android:** `selectShoppableAds` is not implemented on Android yet; the native module logs a warning and does not run the Shoppable Ads flow. Plan for iOS-only behavior until Android support ships. -#### iOS native: `RoktStripePaymentExtension` (payment extensions) +#### iOS native: `RoktPaymentExtension` (payment extensions) -Shoppable Ads flows that use Apple Pay / Stripe integration expect a **payment extension** to be registered on mParticle’s Rokt interface after the SDK starts. +Shoppable Ads flows that use Apple Pay expect a **payment extension** to be registered on mParticle’s Rokt interface after the SDK starts. In `ios/MParticleExpoTest/AppDelegate.swift`, the test app: -1. Imports the Stripe payment extension module provided with the Rokt / kit stack: `import RoktStripePaymentExtension`. -2. After `MParticle.sharedInstance().start(with: mParticleOptions)`, constructs `RoktStripePaymentExtension(applePayMerchantId: "...")` with your **Apple Pay merchant ID** (replace `merchant.dummy` with your real `merchant.*` identifier from Apple Developer). -3. Registers it: `MParticle.sharedInstance().rokt.register(paymentExtension)`. +1. Installs the Rokt kit with `iosKits`: `["mParticle-Rokt"]` or, manually, `pod 'mParticle-Rokt', '~> 9.2'`. +2. Imports the payment extension module: `import RoktPaymentExtension`. +3. After `MParticle.sharedInstance().start(with: mParticleOptions)`, constructs `RoktPaymentExtension(applePayMerchantId: "...")` with your **Apple Pay merchant ID** (replace `merchant.dummy` with your real `merchant.*` identifier from Apple Developer). +4. Registers it: `MParticle.sharedInstance().rokt.registerPaymentExtension(paymentExtension)`. ```swift -import RoktStripePaymentExtension +import RoktPaymentExtension // After MParticle.sharedInstance().start(with: mParticleOptions): -if let paymentExtension = RoktStripePaymentExtension(applePayMerchantId: "merchant.your.id") { - MParticle.sharedInstance().rokt.register(paymentExtension) +if let paymentExtension = RoktPaymentExtension(applePayMerchantId: "merchant.your.id") { + MParticle.sharedInstance().rokt.registerPaymentExtension(paymentExtension) } ``` **Important:** - The Expo config plugin **does not** generate the payment extension block today. After `expo prebuild`, add or merge this code into `AppDelegate.swift` (inside the same app launch path as mParticle init). If you regenerate native projects with `--clean`, re-apply this snippet. -- Ensure the **mParticle Rokt kit** (and transitive Rokt dependencies) are installed so `RoktStripePaymentExtension` resolves—same as configuring `iosKits`: `["mParticle-Rokt"]` in `app.json`. +- Use `iosKits`: `["mParticle-Rokt"]` for standard Rokt placements. Add `RoktPaymentExtension` to `iosKits` only when you are wiring the native payment extension registration. +- In manually managed iOS apps, use `pod 'mParticle-Rokt', '~> 9.2'` for standard placements and add `pod 'RoktPaymentExtension', '~> 2.0'` for the payment-extension install path. + +#### URL callback forwarding + +Forward redirect URLs to Rokt from native iOS URL handlers before other linking handlers. This is intentionally not exposed as a React Native JavaScript API because iOS payment-extension redirects must be handled synchronously from the OS URL callback. + +The Expo config plugin injects this AppDelegate forwarding when `iosKits` includes `["mParticle-Rokt"]` and the generated AppDelegate uses Expo's standard URL handler. Verify the generated AppDelegate after `npm run prebuild` if your app has custom URL handling. + +Swift `AppDelegate`: + +```swift +func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + if MParticle.sharedInstance().rokt.handleURLCallback(with: url) { + return true + } + + return RCTLinkingManager.application(app, open: url, options: options) +} +``` + +SwiftUI: + +```swift +WindowGroup { + ContentView() + .onOpenURL { url in + _ = MParticle.sharedInstance().rokt.handleURLCallback(with: url) + } +} +``` + +Swift `SceneDelegate`: + +```swift +func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { + return + } + + if MParticle.sharedInstance().rokt.handleURLCallback(with: url) { + return + } + + RCTLinkingManager.application(UIApplication.shared, open: url, options: [:]) +} +``` All activity is logged in the Activity Log section at the bottom of the screen. @@ -160,6 +218,14 @@ Check `ios/MParticleExpoTest/AppDelegate.swift` for: import mParticle_Apple_SDK ``` +- Rokt URL callback forwarding when `iosKits` includes `mParticle-Rokt`: + + ```swift + if MParticle.sharedInstance().rokt.handleURLCallback(with: url) { + return true + } + ``` + - MParticleOptions initialization in `didFinishLaunchingWithOptions`: ```swift @@ -168,10 +234,13 @@ Check `ios/MParticleExpoTest/AppDelegate.swift` for: mParticleOptions.environment = .development let identifyRequest = MPIdentityApiRequest.withEmptyUser() mParticleOptions.identifyRequest = identifyRequest + let networkOptions = MPNetworkOptions() + networkOptions.customBaseURL = URL(string: "https://cname.example.com") + mParticleOptions.networkOptions = networkOptions MParticle.sharedInstance().start(with: mParticleOptions) ``` -For Shoppable Ads with Apple Pay / Stripe, you may also need to register `RoktStripePaymentExtension` after `start`—see **Implementation guide: Shoppable Ads (`selectShoppableAds`) and iOS payment extensions** above. +For Shoppable Ads with Apple Pay, you may also need to register `RoktPaymentExtension` after `start` - see **Implementation guide: Shoppable Ads (`selectShoppableAds`) and iOS payment extensions** above. #### Objective-C AppDelegate (Legacy) @@ -192,6 +261,9 @@ For older Expo SDK versions, check `ios/MParticleExpoTest/AppDelegate.mm` for: mParticleOptions.environment = MPEnvironmentDevelopment; MPIdentityApiRequest *identifyRequest = [MPIdentityApiRequest requestWithEmptyUser]; mParticleOptions.identifyRequest = identifyRequest; + MPNetworkOptions *networkOptions = [[MPNetworkOptions alloc] init]; + networkOptions.customBaseURL = [NSURL URLWithString:@"https://cname.example.com"]; + mParticleOptions.networkOptions = networkOptions; [[MParticle sharedInstance] startWithOptions:mParticleOptions]; ``` @@ -204,7 +276,7 @@ Check `ios/Podfile` for: ```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' + if pod.name == 'mParticle-Apple-SDK' || pod.name == 'mParticle-Apple-SDK-ObjC' || pod.name == 'mParticle-Apple-SDK-Swift' || pod.name == 'mParticle-Rokt' || pod.name == 'RoktPaymentExtension' || pod.name == 'Rokt-Widget' || pod.name == 'RoktContracts' def pod.build_type; Pod::BuildType.new(:linkage => :dynamic, :packaging => :framework) end @@ -216,7 +288,7 @@ Check `ios/Podfile` for: - Kit pods (if specified): ```ruby - pod 'mParticle-Rokt' + pod 'mParticle-Rokt', '~> 9.2' ``` ### Verify Android Integration @@ -293,4 +365,5 @@ dependencies { | `dataPlanId` | string | Data plan ID for validation | | `dataPlanVersion` | number | Data plan version | | `iosKits` | string[] | iOS kit pod names (e.g., `["mParticle-Rokt"]`) | +| `iosCustomBaseURL` | string | iOS custom base URL for global CNAME setup | | `androidKits` | string[] | Android kit dependencies (e.g., `["android-rokt-kit"]`) | diff --git a/ExpoTestApp/app.json b/ExpoTestApp/app.json index d7a5398..06d4cc7 100644 --- a/ExpoTestApp/app.json +++ b/ExpoTestApp/app.json @@ -27,6 +27,14 @@ "favicon": "./assets/favicon.png" }, "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "deploymentTarget": "15.6" + } + } + ], [ "react-native-mparticle", { @@ -37,6 +45,7 @@ "logLevel": "verbose", "useEmptyIdentifyRequest": true, "environment": "development", + "iosCustomBaseURL": "https://cname.example.com", "iosKits": ["mParticle-Rokt"], "androidKits": ["android-rokt-kit"] } diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 0000000..51eec10 --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,142 @@ +# Migration Guides + +This document provides migration guidance for changes in `react-native-mparticle`. +Release versions and changelog entries are generated by the Release Draft workflow. + +## Migrating to the mParticle Apple SDK 9.2.0 Rokt update + +This update aligns the React Native wrapper with `mParticle-Apple-SDK` and +`mParticle-Rokt` `9.2.0`. The Apple Rokt kit now resolves `Rokt-Widget` `~> 5.2` +and `RoktContracts` `~> 2.0`. + +### Dependency Changes + +For standard Rokt placements on iOS, use: + +```ruby +pod 'mParticle-Rokt', '~> 9.2' +``` + +For iOS Shoppable Ads payment-extension flows, add the payment extension +alongside the standard Rokt kit: + +```ruby +pod 'RoktPaymentExtension', '~> 2.0' +``` + +Do not add `Rokt-Widget` directly to this React Native wrapper's podspec. Apps +receive it transitively through `mParticle-Rokt`. + +### React Native Rokt API + +The wrapper exposes these Rokt APIs to JavaScript: + +```ts +MParticle.Rokt.close(): Promise +MParticle.Rokt.setSessionId(sessionId: string): Promise +MParticle.Rokt.getSessionId(): Promise +``` + +`close()` is supported on iOS and Android. Session APIs are backed by the iOS +mParticle Rokt kit; Android currently resolves safely without changing the +session until the Android mParticle Rokt kit exposes equivalent public APIs. + +### URL Callback Handling + +Do not forward Shoppable Ads payment redirect URLs from React Native `Linking`. +The Rokt URL callback is an iOS lifecycle concern because it must synchronously +return from the native URL handler. + +Forward incoming iOS URLs to mParticle's native Rokt interface before falling +back to React Native Linking. + +#### Swift AppDelegate + +```swift +func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + if MParticle.sharedInstance().rokt.handleURLCallback(with: url) { + return true + } + + return RCTLinkingManager.application(app, open: url, options: options) +} +``` + +#### Swift SceneDelegate + +```swift +func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { + return + } + + if MParticle.sharedInstance().rokt.handleURLCallback(with: url) { + return + } + + RCTLinkingManager.application(UIApplication.shared, open: url, options: [:]) +} +``` + +#### SwiftUI + +```swift +WindowGroup { + ContentView() + .onOpenURL { url in + _ = MParticle.sharedInstance().rokt.handleURLCallback(with: url) + } +} +``` + +#### Objective-C AppDelegate + +```objective-c +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options { + if ([[[MParticle sharedInstance] rokt] handleURLCallback:url]) { + return YES; + } + + return [RCTLinkingManager application:application openURL:url options:options]; +} +``` + +### Expo Config Plugin + +Use `iosKits: ["mParticle-Rokt"]` for standard Rokt placements: + +```json +[ + "react-native-mparticle", + { + "iosApiKey": "YOUR_IOS_API_KEY", + "iosApiSecret": "YOUR_IOS_API_SECRET", + "iosKits": ["mParticle-Rokt"] + } +] +``` + +The plugin pins generated `mParticle-Rokt` pods to `~> 9.2`. When `iosKits` +contains `mParticle-Rokt`, the plugin also injects AppDelegate URL callback +forwarding for Expo's standard generated URL handler. If your app uses a custom +AppDelegate, SceneDelegate, or SwiftUI lifecycle, apply the native snippet +manually. + +For global CNAME setup, configure `iosCustomBaseURL`: + +```json +{ + "iosCustomBaseURL": "https://cname.example.com" +} +``` + +The plugin applies this through `MPNetworkOptions.customBaseURL` before +mParticle starts. There is no runtime JavaScript setter because the Rokt kit +reads this setting during initialization. + +### Notes + +- Native payment extension registration remains native-side. +- The React Native API intentionally does not expose `handleURLCallback`. diff --git a/README.md b/README.md index 1816424..e70fde3 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ npx expo run:android | `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']`) | +| `iosCustomBaseURL` | string | No | iOS custom base URL for global CNAME setup | | `androidKits` | string[] | No | Android kit artifact names (e.g., `['android-rokt-kit']`) | | `useEmptyIdentifyRequest` | boolean | No | Use empty user identify request at init (default: `true`) | @@ -100,6 +101,7 @@ npx expo run:android "androidApiSecret": "YOUR_ANDROID_API_SECRET", "environment": "development", "logLevel": "verbose", + "iosCustomBaseURL": "https://cname.example.com", "iosKits": ["mParticle-Rokt", "mParticle-Amplitude"], "androidKits": ["android-rokt-kit", "android-amplitude-kit"] } @@ -114,6 +116,7 @@ npx expo run:android **iOS:** - Adds mParticle SDK initialization to `AppDelegate` (supports both Swift and Objective-C) +- Sets `MPNetworkOptions.customBaseURL` before startup when `iosCustomBaseURL` is configured - Configures `pre_install` hook in Podfile for dynamic framework linking - Adds specified kit pod dependencies @@ -219,6 +222,12 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions lau mParticleOptions.onAttributionComplete = { (attributionResult, error) in NSLog(@"Attribution Complete. attributionResults = %@", attributionResult.linkInfo) } + + // Optional global CNAME setup. Configure before start. + let networkOptions = MPNetworkOptions() + networkOptions.customBaseURL = URL(string: "https://cname.example.com") + mParticleOptions.networkOptions = networkOptions + MParticle.sharedInstance().start(with: mParticleOptions) return true } @@ -260,12 +269,84 @@ Next, you'll need to start the SDK: NSLog(@"Attribution Complete. attributionResults = %@", attributionResult.linkInfo) } + // Optional global CNAME setup. Configure before start. + MPNetworkOptions *networkOptions = [[MPNetworkOptions alloc] init]; + networkOptions.customBaseURL = [NSURL URLWithString:@"https://cname.example.com"]; + mParticleOptions.networkOptions = networkOptions; + [[MParticle sharedInstance] startWithOptions:mParticleOptions]; return YES; } ``` +### Rokt iOS Setup + +For standard Rokt placements, add the mParticle Rokt kit: + +```ruby +pod 'mParticle-Rokt', '~> 9.2' +``` + +For iOS Shoppable Ads flows that need the payment extension stack, add the payment extension alongside the mParticle Rokt kit: + +```ruby +pod 'RoktPaymentExtension', '~> 2.0' +``` + +In Expo apps, use `iosKits: ["mParticle-Rokt"]` for standard Rokt placements. Add `RoktPaymentExtension` to `iosKits` only when you are wiring the native payment extension registration. + +For iOS Shoppable Ads redirect-based payment flows, forward incoming URLs to Rokt in native iOS lifecycle code before falling back to React Native Linking. Do not forward these URLs from React Native `Linking`; the callback needs to return synchronously from the OS URL handler. + +Expo prebuild injects this AppDelegate forwarding when `iosKits` includes `mParticle-Rokt` and the generated AppDelegate uses Expo's standard URL handler. Verify the generated file after prebuild if your app customizes AppDelegate. + +```swift +func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + if MParticle.sharedInstance().rokt.handleURLCallback(with: url) { + return true + } + + return RCTLinkingManager.application(app, open: url, options: options) +} +``` + +```swift +WindowGroup { + ContentView() + .onOpenURL { url in + _ = MParticle.sharedInstance().rokt.handleURLCallback(with: url) + } +} +``` + +```swift +func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { + return + } + + if MParticle.sharedInstance().rokt.handleURLCallback(with: url) { + return + } + + RCTLinkingManager.application(UIApplication.shared, open: url, options: [:]) +} +``` + +```objective-c +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options { + if ([[[MParticle sharedInstance] rokt] handleURLCallback:url]) { + return YES; + } + + return [RCTLinkingManager application:application openURL:url options:options]; +} +``` + +See [MIGRATING.md](./MIGRATING.md) for release-specific migration guidance. + 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: diff --git a/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt b/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt index c46983b..1649fc9 100644 --- a/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt +++ b/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableMap @@ -56,6 +57,24 @@ class MPRoktModuleImpl( MParticle.getInstance()?.Rokt()?.purchaseFinalized(placementId, catalogItemId, success) } + fun close(promise: Promise) { + MParticle.getInstance()?.Rokt()?.close() + promise.resolve(null) + } + + fun setSessionId( + sessionId: String, + promise: Promise, + ) { + Logger.warning("setSessionId is not supported on Android") + promise.resolve(null) + } + + fun getSessionId(promise: Promise) { + Logger.warning("getSessionId is not supported on Android") + promise.resolve(null) + } + fun setRoktEventHandler(roktEventHandler: MpRoktEventCallback) { this.roktEventHandler = roktEventHandler } diff --git a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt index 87c537d..d6617cf 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt @@ -1,5 +1,6 @@ package com.mparticle.react.rokt +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap @@ -68,6 +69,24 @@ class MPRoktModule( impl.purchaseFinalized(placementId, catalogItemId, success) } + @ReactMethod + override fun close(promise: Promise) { + impl.close(promise) + } + + @ReactMethod + override fun setSessionId( + sessionId: String, + promise: Promise, + ) { + impl.setSessionId(sessionId, promise) + } + + @ReactMethod + override fun getSessionId(promise: Promise) { + impl.getSessionId(promise) + } + /** * Process placeholders from ReadableMap to a map of Widgets for use with Rokt. * This method handles the Fabric-specific view resolution. diff --git a/android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt b/android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt index b43c9e4..fa29fdf 100644 --- a/android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt +++ b/android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt @@ -1,5 +1,6 @@ package com.mparticle.react +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReadableMap @@ -32,4 +33,13 @@ abstract class NativeMPRoktSpec( catalogItemId: String, success: Boolean, ) + + abstract fun close(promise: Promise) + + abstract fun setSessionId( + sessionId: String, + promise: Promise, + ) + + abstract fun getSessionId(promise: Promise) } diff --git a/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt b/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt index 5093f72..ad005c3 100644 --- a/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt +++ b/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt @@ -1,5 +1,6 @@ package com.mparticle.react.rokt +import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap @@ -66,6 +67,24 @@ class MPRoktModule( impl.purchaseFinalized(placementId, catalogItemId, success) } + @ReactMethod + override fun close(promise: Promise) { + impl.close(promise) + } + + @ReactMethod + override fun setSessionId( + sessionId: String, + promise: Promise, + ) { + impl.setSessionId(sessionId, promise) + } + + @ReactMethod + override fun getSessionId(promise: Promise) { + impl.getSessionId(promise) + } + private fun safeUnwrapPlaceholders( placeholders: ReadableMap?, nativeViewHierarchyManager: NativeViewHierarchyManager, diff --git a/ios/RNMParticle/RNMPRokt.mm b/ios/RNMParticle/RNMPRokt.mm index d45b970..eac229a 100644 --- a/ios/RNMParticle/RNMPRokt.mm +++ b/ios/RNMParticle/RNMPRokt.mm @@ -16,6 +16,7 @@ #import #endif #import +#import #import #import #import @@ -209,6 +210,60 @@ - (void)selectShoppableAds:(NSString *)identifier }]; } +#ifdef RCT_NEW_ARCH_ENABLED +- (void)close:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + [self closeWithResolve:resolve reject:reject]; +} + +- (void)setSessionId:(NSString *)sessionId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + [self setSessionIdWithString:sessionId resolve:resolve reject:reject]; +} + +- (void)getSessionId:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject +{ + [self getSessionIdWithResolve:resolve reject:reject]; +} +#else +RCT_EXPORT_METHOD(close:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + [self closeWithResolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(setSessionId:(NSString *)sessionId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + [self setSessionIdWithString:sessionId resolve:resolve reject:reject]; +} + +RCT_EXPORT_METHOD(getSessionId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + [self getSessionIdWithResolve:resolve reject:reject]; +} +#endif + +- (void)closeWithResolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [[[MParticle sharedInstance] rokt] close]; + resolve(nil); +} + +- (void)setSessionIdWithString:(NSString *)sessionId + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + [[[MParticle sharedInstance] rokt] setSessionId:sessionId ?: @""]; + resolve(nil); +} + +- (void)getSessionIdWithResolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject +{ + NSString *sessionId = [[[MParticle sharedInstance] rokt] getSessionId]; + resolve(sessionId ?: [NSNull null]); +} + RCT_EXPORT_METHOD(purchaseFinalized : (NSString *)placementId catalogItemId : ( NSString *)catalogItemId success : (BOOL)success) { [[[MParticle sharedInstance] rokt] purchaseFinalized:placementId diff --git a/js/codegenSpecs/NativeMParticle.ts b/js/codegenSpecs/NativeMParticle.ts index c3b2bc3..5a96266 100644 --- a/js/codegenSpecs/NativeMParticle.ts +++ b/js/codegenSpecs/NativeMParticle.ts @@ -150,10 +150,7 @@ export interface Spec extends TurboModule { setUserAttributeArray(mpid: string, key: string, value: Array): void; getUserAttributes( mpid: string, - callback: ( - error: CallbackError | null, - result: UserAttributes - ) => void + callback: (error: CallbackError | null, result: UserAttributes) => void ): void; setUserTag(mpid: string, tag: string): void; incrementUserAttribute(mpid: string, key: string, value: number): void; diff --git a/js/codegenSpecs/rokt/NativeMPRokt.ts b/js/codegenSpecs/rokt/NativeMPRokt.ts index 54fe306..001215c 100644 --- a/js/codegenSpecs/rokt/NativeMPRokt.ts +++ b/js/codegenSpecs/rokt/NativeMPRokt.ts @@ -33,6 +33,12 @@ export interface Spec extends TurboModule { attributes: { [key: string]: string }, roktConfig?: RoktConfigType ): void; + + close(): Promise; + + setSessionId(sessionId: string): Promise; + + getSessionId(): Promise; } export default TurboModuleRegistry.getEnforcing('RNMPRokt'); diff --git a/js/rokt/rokt.ts b/js/rokt/rokt.ts index 11c0dd9..fc4faf0 100644 --- a/js/rokt/rokt.ts +++ b/js/rokt/rokt.ts @@ -47,6 +47,18 @@ export abstract class Rokt { MPRokt.purchaseFinalized(placementId, catalogItemId, success); } + static close(): Promise { + return MPRokt.close(); + } + + static setSessionId(sessionId: string): Promise { + return MPRokt.setSessionId(sessionId); + } + + static getSessionId(): Promise { + return MPRokt.getSessionId(); + } + static createRoktConfig(colorMode?: ColorMode, cacheConfig?: CacheConfig) { return new RoktConfig(colorMode ?? 'system', cacheConfig); } diff --git a/plugin/src/withMParticle.ts b/plugin/src/withMParticle.ts index 8ef9980..8602f69 100644 --- a/plugin/src/withMParticle.ts +++ b/plugin/src/withMParticle.ts @@ -56,6 +56,13 @@ export interface MParticlePluginProps { */ iosKits?: string[]; + /** + * iOS custom base URL for global CNAME setup. + * This is applied before mParticle starts. + * @example 'https://your-cname.example.com' + */ + iosCustomBaseURL?: string; + /** * Android kit artifact names to include (version auto-detected from core SDK) * @example ['android-rokt-kit', 'android-amplitude-kit'] diff --git a/plugin/src/withMParticleIOS.ts b/plugin/src/withMParticleIOS.ts index 2e43a3a..e83b6c3 100644 --- a/plugin/src/withMParticleIOS.ts +++ b/plugin/src/withMParticleIOS.ts @@ -91,6 +91,25 @@ function getObjcEnvironment( } } +function getIOSCustomBaseURL(props: MParticlePluginProps): string | null { + const customBaseURL = props.iosCustomBaseURL?.trim(); + if (!customBaseURL) { + return null; + } + + if (!/^https:\/\/[^\s]+$/.test(customBaseURL)) { + throw new Error( + 'react-native-mparticle iosCustomBaseURL must be a valid https URL' + ); + } + + return customBaseURL; +} + +function hasIOSRoktKit(props: MParticlePluginProps): boolean { + return props.iosKits?.includes('mParticle-Rokt') ?? false; +} + /** * Generate mParticle initialization code for Swift AppDelegate */ @@ -134,6 +153,17 @@ function generateSwiftInitCode(props: MParticlePluginProps): string { lines.push('mParticleOptions.identifyRequest = identifyRequest'); } + const iosCustomBaseURL = getIOSCustomBaseURL(props); + if (iosCustomBaseURL) { + lines.push('let networkOptions = MPNetworkOptions()'); + lines.push( + `networkOptions.customBaseURL = URL(string: ${JSON.stringify( + iosCustomBaseURL + )})` + ); + lines.push('mParticleOptions.networkOptions = networkOptions'); + } + lines.push('MParticle.sharedInstance().start(with: mParticleOptions)'); return lines.join('\n '); @@ -183,6 +213,19 @@ function generateObjcInitCode(props: MParticlePluginProps): string { lines.push('mParticleOptions.identifyRequest = identifyRequest;'); } + const iosCustomBaseURL = getIOSCustomBaseURL(props); + if (iosCustomBaseURL) { + lines.push( + 'MPNetworkOptions *networkOptions = [[MPNetworkOptions alloc] init];' + ); + lines.push( + `networkOptions.customBaseURL = [NSURL URLWithString:@${JSON.stringify( + iosCustomBaseURL + )}];` + ); + lines.push('mParticleOptions.networkOptions = networkOptions;'); + } + lines.push('[[MParticle sharedInstance] startWithOptions:mParticleOptions];'); return lines.join('\n '); @@ -198,25 +241,35 @@ const withMParticleAppDelegate: ConfigPlugin = ( ) => { return withAppDelegate(config, config => { const { contents, language } = config.modResults; - - // Check if mParticle is already initialized - if ( + let appDelegateContents = contents; + const hasMParticleInitialization = contents.includes('MParticleOptions') || - contents.includes('mParticleOptions') - ) { - return config; - } + contents.includes('mParticleOptions'); if (language === 'swift') { - config.modResults.contents = addMParticleToSwiftAppDelegate( - contents, - props - ); + if (!hasMParticleInitialization) { + appDelegateContents = addMParticleToSwiftAppDelegate( + appDelegateContents, + props + ); + } + if (hasIOSRoktKit(props)) { + appDelegateContents = + addRoktURLCallbackToSwiftAppDelegate(appDelegateContents); + } + config.modResults.contents = appDelegateContents; } else if (language === 'objc' || language === 'objcpp') { - config.modResults.contents = addMParticleToObjcAppDelegate( - contents, - props - ); + if (!hasMParticleInitialization) { + appDelegateContents = addMParticleToObjcAppDelegate( + appDelegateContents, + props + ); + } + if (hasIOSRoktKit(props)) { + appDelegateContents = + addRoktURLCallbackToObjcAppDelegate(appDelegateContents); + } + config.modResults.contents = appDelegateContents; } else { console.warn( `[react-native-mparticle] Unsupported AppDelegate language: ${language}. ` + @@ -228,6 +281,62 @@ const withMParticleAppDelegate: ConfigPlugin = ( }); }; +function addRoktURLCallbackToSwiftAppDelegate(contents: string): string { + if (contents.includes('rokt.handleURLCallback(with: url)')) { + return contents; + } + + const openURLReturn = + /return super\.application\(app, open: url, options: options\) \|\| RCTLinkingManager\.application\(app, open: url, options: options\)/; + if (!openURLReturn.test(contents)) { + console.warn( + '[react-native-mparticle] Could not find Swift AppDelegate URL handler. ' + + 'Forward iOS Rokt payment callback URLs to MParticle.sharedInstance().rokt.handleURLCallback(with:) manually.' + ); + return contents; + } + + const withCallback = mergeContents({ + src: contents, + newSrc: + '\n if MParticle.sharedInstance().rokt.handleURLCallback(with: url) {\n return true\n }\n', + anchor: openURLReturn, + offset: 0, + tag: `${MPARTICLE_TAG}-rokt-url-callback`, + comment: '//', + }); + + return withCallback.contents; +} + +function addRoktURLCallbackToObjcAppDelegate(contents: string): string { + if (contents.includes('handleURLCallback:url')) { + return contents; + } + + const openURLReturn = + /return \[RCTLinkingManager application:application openURL:url options:options\];|return \[super application:application openURL:url options:options\] \|\| \[RCTLinkingManager application:application openURL:url options:options\];/; + if (!openURLReturn.test(contents)) { + console.warn( + '[react-native-mparticle] Could not find Objective-C AppDelegate URL handler. ' + + 'Forward iOS Rokt payment callback URLs to [[[MParticle sharedInstance] rokt] handleURLCallback:] manually.' + ); + return contents; + } + + const withCallback = mergeContents({ + src: contents, + newSrc: + '\n if ([[[MParticle sharedInstance] rokt] handleURLCallback:url]) {\n return YES;\n }\n', + anchor: openURLReturn, + offset: 0, + tag: `${MPARTICLE_TAG}-rokt-url-callback`, + comment: '//', + }); + + return withCallback.contents; +} + /** * Add mParticle import and initialization to Swift AppDelegate */ @@ -344,11 +453,17 @@ const KIT_TRANSITIVE_DEPENDENCIES: Record = { 'RoktUXHelper', 'DcuiSchema', ], + RoktPaymentExtension: ['RoktContracts'], // Add other kit dependencies here as needed // "mParticle-Amplitude": [], // "mParticle-Braze": [], }; +const KIT_VERSION_REQUIREMENTS: Record = { + 'mParticle-Rokt': "'~> 9.2'", + RoktPaymentExtension: "'~> 2.0'", +}; + /** * Get all pods that need dynamic framework linking */ @@ -373,6 +488,13 @@ function getDynamicFrameworkPods(iosKits?: string[]): string[] { return [...new Set(pods)]; // Remove duplicates } +function getKitPodDeclaration(kit: string): string { + const versionRequirement = KIT_VERSION_REQUIREMENTS[kit]; + return versionRequirement + ? ` pod '${kit}', ${versionRequirement}` + : ` pod '${kit}'`; +} + /** * Add kit pods and pre_install hook to Podfile */ @@ -427,7 +549,7 @@ end // Add kit pods if specified if (props.iosKits && props.iosKits.length > 0) { - const kitPods = props.iosKits.map(kit => ` pod '${kit}'`).join('\n'); + const kitPods = props.iosKits.map(getKitPodDeclaration).join('\n'); // Check if kits are already added const kitsAlreadyAdded = props.iosKits.every(kit => diff --git a/react-native-mparticle.podspec b/react-native-mparticle.podspec index c7d69ce..09c588e 100644 --- a/react-native-mparticle.podspec +++ b/react-native-mparticle.podspec @@ -25,6 +25,6 @@ Pod::Spec.new do |s| s.dependency "React-Core" end - s.dependency 'mParticle-Apple-SDK-ObjC', '~> 9.0' - s.dependency 'RoktContracts', '~> 0.1' + s.dependency 'mParticle-Apple-SDK-ObjC', '~> 9.2' + s.dependency 'RoktContracts', '~> 2.0' end diff --git a/sample/README.md b/sample/README.md index 8fb273c..dd1d07b 100644 --- a/sample/README.md +++ b/sample/README.md @@ -64,6 +64,16 @@ cd sample/ios pod install ``` +The sample Podfile pins the standard Rokt kit with: + +```ruby +pod 'mParticle-Rokt', '~> 9.2' +``` + +Add `pod 'RoktPaymentExtension', '~> 2.0'` when validating the iOS Shoppable Ads payment-extension install path. + +Forward iOS Shoppable Ads payment redirect URLs from native AppDelegate, SceneDelegate, or SwiftUI `onOpenURL` code to `MParticle.sharedInstance().rokt.handleURLCallback(with:)`. This is not a React Native `Linking` API because the URL callback must be handled synchronously in native iOS code. The sample app includes buttons for Rokt close/session APIs. + ## Running the Sample App ### iOS @@ -89,10 +99,10 @@ When making changes to the mParticle React Native SDK: 1. Make your changes to the SDK source code 2. Rebuild and reinstall the package: - ```bash - # From root directory - yarn dev:link - ``` + ```bash + # From root directory + yarn dev:link + ``` 3. Restart the sample app to see your changes diff --git a/sample/__mocks__/react-native-mparticle.js b/sample/__mocks__/react-native-mparticle.js index a721023..6cd97b8 100644 --- a/sample/__mocks__/react-native-mparticle.js +++ b/sample/__mocks__/react-native-mparticle.js @@ -9,20 +9,20 @@ class User { constructor(userId) { this.userId = userId; } - + getMpid() { return this.userId; } - + setUserAttribute() {} getUserAttributes(callback) { - callback({ testAttribute: 'testValue' }); + callback({testAttribute: 'testValue'}); } setUserTag() {} incrementUserAttribute() {} removeUserAttribute() {} getUserIdentities(callback) { - callback({ email: 'test@example.com' }); + callback({email: 'test@example.com'}); } getFirstSeen(callback) { callback(Date.now() - 86400000); @@ -37,12 +37,12 @@ class IdentityRequest { this.email = email; return this; } - + setCustomerID(customerId) { this.customerId = customerId; return this; } - + setUserIdentity(userIdentity, identityType) { this[identityType] = userIdentity; return this; @@ -54,23 +54,23 @@ class Identity { var currentUser = new User('mockUserId123'); completion(currentUser); } - + static login(request, completion) { completion(null, 'mockUserId123', 'mockPreviousUserId456'); } - + static logout(request, completion) { completion(null, 'mockUserId123'); } - + static identify(request, completion) { completion(null, 'mockUserId123', 'mockPreviousUserId456'); } - + static modify(request, completion) { completion(null, 'mockUserId123', 'mockPreviousUserId456'); } - + static aliasUsers(request, completion) { completion(true, null); } @@ -92,7 +92,11 @@ class TransactionAttributes { } class CommerceEvent { - static createProductActionEvent(productActionType, products, transactionAttributes) { + static createProductActionEvent( + productActionType, + products, + transactionAttributes, + ) { return new CommerceEvent(); } } @@ -102,17 +106,17 @@ class AliasRequest { this.sourceMpid = mpid; return this; } - + destinationMpid(mpid) { this.destinationMpid = mpid; return this; } - + startTime(time) { this.startTime = time; return this; } - + endTime(time) { this.endTime = time; return this; @@ -121,7 +125,12 @@ class AliasRequest { // Mock Rokt const Rokt = { - selectPlacements: () => Promise.resolve() + selectPlacements: () => Promise.resolve(), + selectShoppableAds: () => Promise.resolve(), + purchaseFinalized: () => Promise.resolve(), + close: () => Promise.resolve(), + setSessionId: () => Promise.resolve(), + getSessionId: () => Promise.resolve(null), }; // Constants @@ -134,20 +143,20 @@ const EventType = { UserPreference: 6, Social: 7, Other: 8, - Media: 9 + Media: 9, }; const UserIdentityType = { Email: 7, CustomerId: 1, - Alias: 8 + Alias: 8, }; const ProductActionType = { AddToCart: 1, RemoveFromCart: 2, Checkout: 3, - Purchase: 7 + Purchase: 7, }; // Main mock object @@ -161,24 +170,24 @@ const MParticle = { CommerceEvent, AliasRequest, Rokt, - + // Constants EventType, UserIdentityType, ProductActionType, - + // Methods logEvent: () => {}, logCommerceEvent: () => {}, logPushRegistration: () => {}, - getSession: (callback) => callback({ sessionId: 'mockSessionId123' }), + getSession: callback => callback({sessionId: 'mockSessionId123'}), setOptOut: () => {}, - getOptOut: (callback) => callback(false), + getOptOut: callback => callback(false), isKitActive: (kitId, callback) => callback(true), - getAttributions: (callback) => callback({ attributionResults: 'mock results' }), + getAttributions: callback => callback({attributionResults: 'mock results'}), upload: () => {}, setUploadInterval: () => {}, - setLocation: () => {} + setLocation: () => {}, }; -module.exports = MParticle; \ No newline at end of file +module.exports = MParticle; diff --git a/sample/index.js b/sample/index.js index 2a003f7..483c4f8 100644 --- a/sample/index.js +++ b/sample/index.js @@ -4,7 +4,7 @@ * @flow */ -import React, { Component } from 'react'; +import React, {Component} from 'react'; import { AppRegistry, StyleSheet, @@ -22,385 +22,484 @@ import { } from 'react-native'; import MParticle from 'react-native-mparticle'; -const { RoktLayoutView } = MParticle; +const {RoktLayoutView} = MParticle; const eventManagerEmitter = new NativeEventEmitter(MParticle.RoktEventManager); export default class MParticleSample extends Component { - constructor(props) { - super(props); - this.placeholder1 = React.createRef(); - this.state = { - isShowingText: true, - optedOut: true, - attributionResults: "{value: no attributionResults}", - session: '', - isKitActive: true, - customIdentifier: 'MSDKBottomSheetLayout', - }; - - this._toggleOptOut = this._toggleOptOut.bind(this) - this._getAttributionResults = this._getAttributionResults.bind(this) - this._isKitActive = this._isKitActive.bind(this) - this.render = this.render.bind(this) - - // Example Login - var request = new MParticle.IdentityRequest(); - request.email = 'testing1@gmail.com'; - request.customerId = "123" - MParticle.Identity.login(request, (error, userId, previousUserId) => { - if (error) { - console.debug(error); - } - - // Only create alias request if there's a previous user - if (previousUserId) { - var previousUser = new MParticle.User(previousUserId); - previousUser.getFirstSeen((firstSeen) => { - previousUser.getLastSeen((lastSeen) => { - var aliasRequest = new MParticle.AliasRequest() - .sourceMpid(previousUser.getMpid()) - .destinationMpid(userId) - .startTime(firstSeen - 1000) - .endTime(lastSeen - 1000) - console.log("AliasRequest = " + JSON.stringify(aliasRequest)); - MParticle.Identity.aliasUsers(aliasRequest, (success, error) => { - if (error) { - console.log("Alias error = " + error); - } - console.log("Alias result: " + success); - }); - - var aliasRequest2 = new MParticle.AliasRequest() - .sourceMpid(previousUser.getMpid()) - .destinationMpid(userId) - console.log("AliasRequest2 = " + JSON.stringify(aliasRequest2)); - MParticle.Identity.aliasUsers(aliasRequest2, (success, error) => { - if (error) { - console.log("Alias 2 error = " + error); - } - console.log("Alias 2 result: " + success); - }); - }) - }) - } else { - console.log("No previous user found, skipping alias request"); - } - - var user = new MParticle.User(userId); - console.debug("User Attributes = " + user.userAttributes); - MParticle.Identity.logout({}, (error, userId) => { - if (error) { - console.debug("Logout error" + error); - } - var request = new MParticle.IdentityRequest(); - request.email = 'testing2@gmail.com'; - request.customerId = '456' - MParticle.Identity.modify(request, (error) => { - if (error) { - console.debug("Modify error = " + error) - } - }); - }); - }); + constructor(props) { + super(props); + this.placeholder1 = React.createRef(); + this.state = { + isShowingText: true, + optedOut: true, + attributionResults: '{value: no attributionResults}', + session: '', + isKitActive: true, + customIdentifier: 'MSDKBottomSheetLayout', + }; + + this._toggleOptOut = this._toggleOptOut.bind(this); + this._getAttributionResults = this._getAttributionResults.bind(this); + this._isKitActive = this._isKitActive.bind(this); + this._roktClose = this._roktClose.bind(this); + this._roktSession = this._roktSession.bind(this); + this.render = this.render.bind(this); + + // Example Login + var request = new MParticle.IdentityRequest(); + request.email = 'testing1@gmail.com'; + request.customerId = '123'; + MParticle.Identity.login(request, (error, userId, previousUserId) => { + if (error) { + console.debug(error); + } - var i = 0; - // Toggle the state every few seconds, 10 times - var intervalId = setInterval(() => { - MParticle.logEvent('Test event', MParticle.EventType.Other, { 'Test key': 'Test value', 'Test Boolean': true, 'Test Int': 1235, 'Test Double': 123.123 }) - this.setState((previousState) => { - return {isShowingText: !previousState.isShowingText} - }) - MParticle.Identity.getCurrentUser((currentUser) => { - //currentUser.setUserTag('regular'); + // Only create alias request if there's a previous user + if (previousUserId) { + var previousUser = new MParticle.User(previousUserId); + previousUser.getFirstSeen(firstSeen => { + previousUser.getLastSeen(lastSeen => { + var aliasRequest = new MParticle.AliasRequest() + .sourceMpid(previousUser.getMpid()) + .destinationMpid(userId) + .startTime(firstSeen - 1000) + .endTime(lastSeen - 1000); + console.log('AliasRequest = ' + JSON.stringify(aliasRequest)); + MParticle.Identity.aliasUsers(aliasRequest, (success, error) => { + if (error) { + console.log('Alias error = ' + error); + } + console.log('Alias result: ' + success); }); - var request = new MParticle.IdentityRequest(); - request.email = 'testing1@gmail.com'; - request.customerId = "vlknasdlknv" - request.setUserIdentity('12345', MParticle.UserIdentityType.Alias); - - 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.logPushRegistration("afslibvnoewtibnsgb", "vdasvadsdsav"); - console.debug("interval") - i++; - if (i >= 10) { - clearInterval(intervalId); - } - }, 5000); - } - componentDidMount() { - MParticle.getSession(session => this.setState({ session })) - if (eventManagerEmitter) { - // Save subscriptions so we can remove them later - this.roktCallbackListener = eventManagerEmitter.addListener( - 'RoktCallback', - data => { - console.log('roktCallback received: ' + data.callbackValue); - }, - ); - - this.roktEventsListener = eventManagerEmitter.addListener('RoktEvents', data => { - console.log(`*** ROKT EVENT *** ${JSON.stringify(data)}`); + var aliasRequest2 = new MParticle.AliasRequest() + .sourceMpid(previousUser.getMpid()) + .destinationMpid(userId); + console.log('AliasRequest2 = ' + JSON.stringify(aliasRequest2)); + MParticle.Identity.aliasUsers(aliasRequest2, (success, error) => { + if (error) { + console.log('Alias 2 error = ' + error); + } + console.log('Alias 2 result: ' + success); }); - } else { - console.warn('RoktEventManager not available, skipping event listeners'); - } - } + }); + }); + } else { + console.log('No previous user found, skipping alias request'); + } - componentWillUnmount() { - // Remove event listeners to avoid duplicate subscriptions - if (this.roktCallbackListener) { - this.roktCallbackListener.remove(); + var user = new MParticle.User(userId); + console.debug('User Attributes = ' + user.userAttributes); + MParticle.Identity.logout({}, (error, userId) => { + if (error) { + console.debug('Logout error' + error); } - if (this.roktEventsListener) { - this.roktEventsListener.remove(); - } - } + var request = new MParticle.IdentityRequest(); + request.email = 'testing2@gmail.com'; + request.customerId = '456'; + MParticle.Identity.modify(request, error => { + if (error) { + console.debug('Modify error = ' + error); + } + }); + }); + }); - _toggleOptOut() { - MParticle.getOptOut((optedOut) => { - MParticle.setOptOut(!optedOut) - console.debug("setOptout" + optedOut) - this.setState((previousState) => { - console.debug("returning state") - return { optedOut: !optedOut }; - }) - }) - } + var i = 0; + // Toggle the state every few seconds, 10 times + var intervalId = setInterval(() => { + MParticle.logEvent('Test event', MParticle.EventType.Other, { + 'Test key': 'Test value', + 'Test Boolean': true, + 'Test Int': 1235, + 'Test Double': 123.123, + }); + this.setState(previousState => { + return {isShowingText: !previousState.isShowingText}; + }); + MParticle.Identity.getCurrentUser(currentUser => { + //currentUser.setUserTag('regular'); + }); + var request = new MParticle.IdentityRequest(); + request.email = 'testing1@gmail.com'; + request.customerId = 'vlknasdlknv'; + request.setUserIdentity('12345', MParticle.UserIdentityType.Alias); - _getAttributionResults() { - MParticle.getAttributions((_attributionResults) => { - this.setState((previousState) => { - return {attributionResults: _attributionResults} - }) - }) - } + 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, + ); - _isKitActive() { - MParticle.isKitActive(80, (active) => { - this.setState((previousState) => { - return {isKitActive: active} - }) - }) - } + MParticle.logCommerceEvent(event); + MParticle.logPushRegistration('afslibvnoewtibnsgb', 'vdasvadsdsav'); + console.debug('interval'); + i++; + if (i >= 10) { + clearInterval(intervalId); + } + }, 5000); + } - _incrementAttribute() { - MParticle.Identity.getCurrentUser((currentUser) => { - currentUser.incrementUserAttribute("incrementedAttribute", 1) - }) - } + componentDidMount() { + MParticle.getSession(session => this.setState({session})); - _roktSelectOverlayPlacements() { - this._roktSelectPlacements('MSDKOverlayLayout') - } + if (eventManagerEmitter) { + // Save subscriptions so we can remove them later + this.roktCallbackListener = eventManagerEmitter.addListener( + 'RoktCallback', + data => { + console.log('roktCallback received: ' + data.callbackValue); + }, + ); - _roktSelectBottomSheetPlacements() { - this._roktSelectPlacements('MSDKBottomSheetLayout') + this.roktEventsListener = eventManagerEmitter.addListener( + 'RoktEvents', + data => { + console.log(`*** ROKT EVENT *** ${JSON.stringify(data)}`); + }, + ); + } else { + console.warn('RoktEventManager not available, skipping event listeners'); } + } - _roktSelectEmbeddedPlacements() { - this._roktSelectPlacements('MSDKEmbeddedLayout') + componentWillUnmount() { + // Remove event listeners to avoid duplicate subscriptions + if (this.roktCallbackListener) { + this.roktCallbackListener.remove(); } + if (this.roktEventsListener) { + this.roktEventsListener.remove(); + } + } - _roktSelectPlacements(identifier) { - // Platform-specific attributes - const iosAttributes = { - "email": "ios-user@example.com", - "platform": "ios", - "userId": "ios-54321", - "deviceType": "mobile", - }; - - const androidAttributes = { - "email": "android-user@example.com", - "platform": "android", - "userId": "android-67890", - "deviceType": "mobile", - }; - - // Select attributes based on platform - const attributes = Platform.OS === 'ios' ? iosAttributes : androidAttributes; - console.log(`Platform detected: ${Platform.OS}, using ${Platform.OS === 'ios' ? 'iOS' : 'Android'} attributes:`, attributes); - const cacheConfig = MParticle.Rokt.createCacheConfig(30, attributes); - const config = MParticle.Rokt.createRoktConfig('system', cacheConfig); - const placeholderMap = { - 'Location1': findNodeHandle(this.placeholder1.current), - } - MParticle.Rokt.selectPlacements(identifier, attributes, placeholderMap, config, null).then((result) => { - console.debug("Rokt selectPlacements result: " + JSON.stringify(result)) - }).catch((error) => { - console.debug("Rokt selectPlacements error: " + JSON.stringify(error)) + _toggleOptOut() { + MParticle.getOptOut(optedOut => { + MParticle.setOptOut(!optedOut); + console.debug('setOptout' + optedOut); + this.setState(previousState => { + console.debug('returning state'); + return {optedOut: !optedOut}; + }); + }); + } + + _getAttributionResults() { + MParticle.getAttributions(_attributionResults => { + this.setState(previousState => { + return {attributionResults: _attributionResults}; + }); + }); + } + + _isKitActive() { + MParticle.isKitActive(80, active => { + this.setState(previousState => { + return {isKitActive: active}; + }); + }); + } + + _incrementAttribute() { + MParticle.Identity.getCurrentUser(currentUser => { + currentUser.incrementUserAttribute('incrementedAttribute', 1); + }); + } + + _roktSelectOverlayPlacements() { + this._roktSelectPlacements('MSDKOverlayLayout'); + } + + _roktSelectBottomSheetPlacements() { + this._roktSelectPlacements('MSDKBottomSheetLayout'); + } + + _roktSelectEmbeddedPlacements() { + this._roktSelectPlacements('MSDKEmbeddedLayout'); + } + + _roktClose() { + MParticle.Rokt.close() + .then(() => { + console.debug('Rokt close called'); }) - } - render() { - let display = this.state.isShowingText ? 'Sending Event' : ' ' - let optedOut = this.state.optedOut ? 'true' : 'false' - let optAction = this.state.optedOut ? 'In' : 'Out' - let kitActive = this.state.isKitActive ? 'true' : 'false' - return ( - - - - - mParticle - React Native SDK Sample - - - {display} - - - Opted Out = {optedOut} - -